对齐 Parent Group 前端展示与复核

This commit is contained in:
andy committed 2026-07-12 13:35:01 +08:00
1 parent 92489af18e
commit bf796946f9
13 files changed
+575 -13

No files matched your search

@@ -88,7 +88,7 @@
<div>
<dt>{{ t('task.taskCardType') }}</dt>
<dd>
{{ formatReservationTaskCard(t, detail.task_card_type) }}
{{ formatReservationTaskCardLabel(t, detail) }}
<small class="th-code">{{ detail.task_card_type }}</small>
</dd>
</div>
@@ -191,6 +191,79 @@
<pre>{{ formatJson(manualReviewRecord) }}</pre>
</section>
<section
v-if="manualReviewEvidence"
class="manual-review-evidence"
>
<header>
<h2>{{ t('task.manualReviewEvidence') }}</h2>
<p>{{ manualReviewEvidence.visibleReason || t('task.manualReviewEvidenceFallback') }}</p>
</header>
<dl>
<div>
<dt>{{ t('task.manualResolution.reasonCode') }}</dt>
<dd class="th-code">
{{ manualReviewEvidence.reasonCode || '-' }}
</dd>
</div>
<div>
<dt>{{ t('task.manualReviewMissingFields') }}</dt>
<dd>{{ formatStringList(manualReviewEvidence.missingFields) }}</dd>
</div>
<div>
<dt>{{ t('task.manualReviewBlockingPoints') }}</dt>
<dd>{{ formatStringList(manualReviewEvidence.blockingPoints) }}</dd>
</div>
<div>
<dt>{{ t('task.manualReviewConflictingPoints') }}</dt>
<dd>{{ formatStringList(manualReviewEvidence.conflictingPoints) }}</dd>
</div>
<div>
<dt>{{ t('task.manualReviewSuggestedActions') }}</dt>
<dd>{{ formatStringList(manualReviewEvidence.suggestedHumanActions) }}</dd>
</div>
<div>
<dt>{{ t('task.manualReviewEvidenceToCheck') }}</dt>
<dd>{{ formatStringList(manualReviewEvidence.evidenceToCheck) }}</dd>
</div>
</dl>
<div class="manual-review-evidence__candidates">
<h3>{{ t('task.parentIdentityCandidates') }}</h3>
<div
v-if="manualReviewEvidence.parentIdentityCandidates.length"
class="candidate-grid"
>
<article
v-for="(candidate, index) in manualReviewEvidence.parentIdentityCandidates"
:key="`${candidate.title}-${candidate.summary}-${index}`"
class="candidate-item"
>
<strong>{{ candidate.title || t('task.parentIdentityCandidate') }}</strong>
<span class="th-code">{{ candidate.summary || '-' }}</span>
<small>{{ candidate.sourceSummary || '-' }}</small>
<dl
v-if="candidate.attributes.length"
class="candidate-item__attributes"
>
<div
v-for="attribute in candidate.attributes"
:key="attribute.key"
>
<dt>{{ attribute.key }}</dt>
<dd>{{ attribute.value }}</dd>
</div>
</dl>
</article>
</div>
<p
v-else
class="manual-review-evidence__empty"
>
{{ t('task.noParentIdentityCandidates') }}
</p>
</div>
</section>
<section
v-if="adapterContractErrors.length"
class="task-ai-diagnostics task-ai-diagnostics--error"
@@ -366,6 +439,7 @@ import {
formatReservationMaybeStableReason,
formatReservationSystemProcessCategory,
formatReservationTaskCard,
formatReservationTaskCardLabel,
formatReservationTaskRouteSummary,
isReservationFallbackManualReviewTask,
isReservationReadonlyDiagnosticTask,
@@ -404,6 +478,7 @@ const isPendingTypeKnownManualReviewDetail = computed(
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? null)
const manualReviewEvidence = computed(() => buildManualReviewEvidence(detail.value, manualReviewRecord.value))
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
const isFieldRendererReadOnly = computed(
@@ -659,6 +734,123 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
function formatJson(value: unknown): string {
return JSON.stringify(value ?? {}, null, 2)
}
interface ManualReviewEvidenceCandidate {
title: string | null
summary: string | null
sourceSummary: string | null
attributes: Array<{
key: string
value: string
}>
}
interface ManualReviewEvidence {
reasonCode: string | null
visibleReason: string | null
missingFields: string[]
blockingPoints: string[]
conflictingPoints: string[]
suggestedHumanActions: string[]
evidenceToCheck: string[]
parentIdentityCandidates: ManualReviewEvidenceCandidate[]
}
function buildManualReviewEvidence(
taskDetail: ReservationTaskDetailResult | null,
manualReview: ReservationRecord | null | undefined,
): ManualReviewEvidence | null {
if (!manualReview || normalizeStableCode(readString(manualReview.reason_code)) !== 'TARGET_OBJECT_UNCLEAR') {
return null
}
return {
reasonCode: readString(manualReview.reason_code),
visibleReason: readString(manualReview.visible_reason),
missingFields: readStringArray(manualReview.missing_fields),
blockingPoints: readStringArray(manualReview.blocking_points),
conflictingPoints: readStringArray(manualReview.conflicting_points),
suggestedHumanActions: readStringArray(manualReview.suggested_human_actions),
evidenceToCheck: readStringArray(manualReview.evidence_to_check),
parentIdentityCandidates: readParentIdentityCandidates(taskDetail, manualReview),
}
}
function readParentIdentityCandidates(
taskDetail: ReservationTaskDetailResult | null,
manualReview: ReservationRecord,
): ManualReviewEvidenceCandidate[] {
const contextCandidates = [
readRecord(taskDetail?.context_used)?.parent_identity_candidates,
readRecord(manualReview.context_used)?.parent_identity_candidates,
manualReview.parent_identity_candidates,
]
const candidateList = contextCandidates.find((item) => Array.isArray(item))
if (!Array.isArray(candidateList)) {
return []
}
return candidateList
.map((item) => readRecord(item))
.filter((item): item is ReservationRecord => item !== null)
.map((item) => {
const attributes = Object.entries(item)
.map(([key, value]) => ({
key,
value: stringifyCandidateValue(value),
}))
.filter((attribute) => attribute.value !== '')
return {
title: readFirstString(item, ['field', 'candidate_field', 'key', 'field_path']),
summary: readFirstString(item, ['value', 'raw_value', 'normalized_value', 'candidate_value']),
sourceSummary: readFirstString(item, ['evidence_source', 'source', 'evidence', 'origin']),
attributes,
}
})
}
function readRecord(value: unknown): ReservationRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null
}
return value as ReservationRecord
}
function readString(value: unknown): string | null {
return typeof value === 'string' && value.trim() ? value : null
}
function readFirstString(record: ReservationRecord, keys: string[]): string | null {
for (const key of keys) {
const value = readString(record[key])
if (value) {
return value
}
}
return null
}
function readStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return []
}
return value.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
}
function stringifyCandidateValue(value: unknown): string {
if (value === undefined || value === null) {
return ''
}
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}
return JSON.stringify(value)
}
function formatStringList(items: string[]): string {
return items.length ? items.join(' / ') : '-'
}
</script>
<style scoped>
@@ -751,6 +943,15 @@ function formatJson(value: unknown): string {
border-color: var(--th-color-danger);
}
.manual-review-evidence {
display: grid;
gap: 14px;
border: 1px solid var(--th-color-warning);
border-radius: var(--th-radius-md);
background: color-mix(in srgb, var(--th-color-warning) 8%, var(--th-color-white));
padding: 16px;
}
.source-only-result {
display: grid;
gap: 12px;
@@ -766,6 +967,10 @@ function formatJson(value: unknown): string {
.task-route-info dl,
.task-ai-diagnostics h2,
.task-ai-diagnostics pre,
.manual-review-evidence h2,
.manual-review-evidence h3,
.manual-review-evidence p,
.manual-review-evidence dl,
.source-only-result h2,
.source-only-result p,
.source-only-result dl {
@@ -775,11 +980,24 @@ function formatJson(value: unknown): string {
.task-evidence h2,
.task-route-info h2,
.task-ai-diagnostics h2,
.manual-review-evidence h2,
.source-only-result h2 {
color: var(--th-color-slate-900);
font-size: 15px;
}
.manual-review-evidence header {
display: grid;
gap: 6px;
}
.manual-review-evidence header p,
.manual-review-evidence__empty {
color: var(--th-color-slate-600);
font-size: 13px;
line-height: 1.6;
}
.source-only-result header {
display: grid;
gap: 6px;
@@ -793,7 +1011,8 @@ function formatJson(value: unknown): string {
}
.task-evidence dl,
.task-route-info dl {
.task-route-info dl,
.manual-review-evidence > dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
@@ -806,7 +1025,8 @@ function formatJson(value: unknown): string {
}
.task-evidence dt,
.task-route-info dt {
.task-route-info dt,
.manual-review-evidence > dl dt {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 700;
@@ -814,6 +1034,7 @@ function formatJson(value: unknown): string {
.task-evidence dd,
.task-route-info dd,
.manual-review-evidence > dl dd,
.source-only-result dd {
margin: 5px 0 0;
color: var(--th-color-slate-900);
@@ -821,6 +1042,65 @@ function formatJson(value: unknown): string {
overflow-wrap: anywhere;
}
.manual-review-evidence__candidates {
display: grid;
gap: 10px;
}
.manual-review-evidence__candidates h3 {
color: var(--th-color-slate-900);
font-size: 13px;
}
.candidate-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.candidate-item {
display: grid;
gap: 4px;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
background: var(--th-color-white);
padding: 10px;
}
.candidate-item strong {
color: var(--th-color-slate-900);
font-size: 13px;
}
.candidate-item small {
color: var(--th-color-slate-500);
font-size: 12px;
}
.candidate-item__attributes {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
margin: 4px 0 0;
}
.candidate-item__attributes div {
display: grid;
grid-template-columns: minmax(90px, 0.35fr) minmax(0, 1fr);
gap: 8px;
}
.candidate-item__attributes dt,
.candidate-item__attributes dd {
margin: 0;
color: var(--th-color-slate-500);
font-size: 11px;
}
.candidate-item__attributes dd {
color: var(--th-color-slate-700);
}
.source-only-result__raw {
grid-column: 1 / -1;
}
@@ -934,7 +1214,9 @@ function formatJson(value: unknown): string {
}
.task-evidence dl,
.task-route-info dl {
.task-route-info dl,
.manual-review-evidence > dl,
.candidate-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@@ -942,6 +1224,8 @@ function formatJson(value: unknown): string {
@media (max-width: 640px) {
.task-evidence dl,
.task-route-info dl,
.manual-review-evidence > dl,
.candidate-grid,
.source-only-result dl {
grid-template-columns: 1fr;
}
@@ -13,7 +13,7 @@
>
<span class="task-row__index">{{ task.queue_sequence ?? '-' }}</span>
<span class="task-row__main">
<strong>{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}</strong>
<strong>{{ formatReservationTaskCardLabel(t, task) }}</strong>
<small>{{ formatReservationTaskRouteSummary(t, task) }}</small>
<em :class="{ 'task-row__reason--blocked': !task.can_process }">
{{ task.can_process ? t('taskList.canProcess') : t('taskList.cannotProcess') }}
@@ -51,7 +51,7 @@ import ReservationStatusBadge from '@/components/reservation/ReservationStatusBa
import type { ReservationOrderTaskTimelineItem } from '@/types/reservation'
import {
formatReservationReadonlyReason,
formatReservationTaskCard,
formatReservationTaskCardLabel,
formatReservationTaskRouteSummary,
} from '@/utils/reservationDisplay'