适配 M002 V3 前端任务流与同卡复核

This commit is contained in:
andy committed 2026-07-12 00:33:15 +08:00
1 parent fd027f124c
commit 155db63028
17 files changed
+1593 -85

No files matched your search

@@ -17,7 +17,7 @@
<header class="task-detail-header">
<div>
<p class="task-eyebrow">
{{ formatReservationTaskCard(t, detail.task_card_type) }}
{{ formatReservationTaskRouteSummary(t, detail) }}
</p>
<h1>{{ t('task.title') }} {{ detail.task_id }}</h1>
<span class="task-contract">
@@ -25,7 +25,7 @@
</span>
<div class="task-links">
<RouterLink
v-if="!isSourceMessageOnlyDetail"
v-if="!isReadonlyDiagnosticDetail"
class="task-link"
:to="`/reservation/orders/${detail.order_id}`"
>
@@ -75,6 +75,52 @@
</dl>
</section>
<section class="task-route-info">
<h2>{{ t('task.routeInfo') }}</h2>
<dl>
<div>
<dt>{{ t('task.systemTaskType') }}</dt>
<dd>
{{ formatReservationTaskCard(t, detail.system_task_type) }}
<small class="th-code">{{ detail.system_task_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.taskCardType') }}</dt>
<dd>
{{ formatReservationTaskCard(t, detail.task_card_type) }}
<small class="th-code">{{ detail.task_card_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.resultType') }}</dt>
<dd>{{ detail.result_type ? formatReservationResultType(t, detail.result_type) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.aiTaskType') }}</dt>
<dd>{{ detail.ai_task_type || '-' }}</dd>
</div>
<div>
<dt>{{ t('task.taskSubtype') }}</dt>
<dd>{{ detail.task_subtype ? formatReservationRouteCode(t, detail.task_subtype) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.routeCode') }}</dt>
<dd class="th-code">
{{ detail.route_code || '-' }}
</dd>
</div>
<div>
<dt>{{ t('task.systemProcessCategory') }}</dt>
<dd>{{ detail.system_process_category ? formatReservationSystemProcessCategory(t, detail.system_process_category) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.reviewStatus') }}</dt>
<dd>{{ detail.review_status || '-' }}</dd>
</div>
</dl>
</section>
<section
v-if="isSourceMessageOnlyDetail"
class="source-only-result"
@@ -110,6 +156,24 @@
<pre>{{ sourceMessageOnlyResult.raw_answer || '-' }}</pre>
</dd>
</div>
<div class="source-only-result__raw">
<dt>{{ t('task.agentAssessment') }}</dt>
<dd>
<pre>{{ formatJson(sourceMessageOnlyResult.agent_assessment) }}</pre>
</dd>
</div>
<div class="source-only-result__raw">
<dt>{{ t('task.sourceNotification') }}</dt>
<dd>
<pre>{{ formatJson(sourceMessageOnlyResult.notification) }}</pre>
</dd>
</div>
<div class="source-only-result__raw">
<dt>{{ t('task.manualReviewData') }}</dt>
<dd>
<pre>{{ formatJson(sourceMessageOnlyResult.manual_review) }}</pre>
</dd>
</div>
</dl>
<p
v-else
@@ -119,6 +183,43 @@
</p>
</section>
<section
v-if="manualReviewRecord"
class="task-ai-diagnostics"
>
<h2>{{ t('task.manualReviewData') }}</h2>
<pre>{{ formatJson(manualReviewRecord) }}</pre>
</section>
<section
v-if="adapterContractErrors.length"
class="task-ai-diagnostics task-ai-diagnostics--error"
>
<h2>{{ t('task.adapterContractErrors') }}</h2>
<article
v-for="item in adapterContractErrors"
:key="item.transition_id || `${item.route_code}-${item.array_index}`"
>
<strong>{{ item.adapter_error_code || item.route_code || t('resultType.ADAPTER_CONTRACT_ERROR') }}</strong>
<p>{{ item.adapter_error_message || '-' }}</p>
<pre>{{ formatJson(item.payload_fragment) }}</pre>
</article>
</section>
<section
v-if="unhandledIntents.length"
class="task-ai-diagnostics"
>
<h2>{{ t('task.unhandledIntents') }}</h2>
<article
v-for="item in unhandledIntents"
:key="item.transition_id || `${item.route_code}-${item.array_index}`"
>
<strong>{{ item.route_code || item.task_subtype || t('resultType.UNHANDLED_CURRENT_INTENT') }}</strong>
<pre>{{ formatJson(item.payload_fragment) }}</pre>
</article>
</section>
<div
v-if="detail.availability.blocked"
class="task-alert"
@@ -149,8 +250,20 @@
<div class="task-detail-body">
<div class="task-card-main">
<ReservationManualReviewResolutionPanel
v-if="isTypeKnownManualReviewDetail"
:task-order-id="detail.order_id"
:review-status="detail.review_status"
:review-resolution="detail.review_resolution"
:manual-review="detail.manual_review"
:fields="detail.fields"
:field-values="fieldValues"
:busy="mutationBusy !== null"
@submit="submitManualReviewResolution"
/>
<ReservationManualReviewConversionPanel
v-if="isManualReviewTask && !isSourceMessageOnlyDetail"
v-if="isFallbackManualReviewDetail && !isTypeKnownManualReviewDetail && !isReadonlyDiagnosticDetail"
:busy="mutationBusy !== null"
@submit="submitManualReviewConversion"
/>
@@ -158,12 +271,12 @@
<ReservationTaskFieldRenderer
v-model="fieldValues"
:fields="detail.fields"
:read-only="detail.availability.read_only"
:read-only="isFieldRendererReadOnly"
:validation-errors="validationErrorMessages"
/>
<footer
v-if="canEditTask || canConfirmTask"
v-if="canSave || canConfirm"
class="task-actions"
>
<button
@@ -197,10 +310,10 @@
<aside class="task-side-stack">
<ReservationOperaPanel
v-if="!isSourceMessageOnlyDetail"
v-if="!isReadonlyDiagnosticDetail && !isPendingTypeKnownManualReviewDetail"
:task-id="detail.task_id"
:operations="detail.opera_operations"
:executable="detail.availability.executable && canExecuteOpera"
:executable="canExecuteOpera"
:show-actions="canExecuteOpera"
@operation-updated="replaceOperation"
/>
@@ -221,6 +334,7 @@ import { useI18n } from 'vue-i18n'
import ReservationAuditTimeline from '@/components/reservation/ReservationAuditTimeline.vue'
import ReservationManualReviewConversionPanel from '@/components/reservation/ReservationManualReviewConversionPanel.vue'
import ReservationManualReviewResolutionPanel from '@/components/reservation/ReservationManualReviewResolutionPanel.vue'
import ReservationOperaPanel from '@/components/reservation/ReservationOperaPanel.vue'
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
import ReservationTaskFieldRenderer from '@/components/reservation/ReservationTaskFieldRenderer.vue'
@@ -229,10 +343,12 @@ import {
confirmReservationTask,
fetchReservationTaskAudits,
fetchReservationTaskDetail,
resolveReservationManualReviewTask,
saveReservationTaskDraft,
} from '@/services/reservationService'
import type {
ReservationManualReviewConversionRequest,
ReservationManualReviewResolutionRequest,
ReservationOperaOperationResult,
ReservationRecord,
ReservationTaskAuditLogResult,
@@ -245,9 +361,17 @@ import {
type ReservationFieldValidationErrors,
} from '@/utils/reservationFieldRules'
import {
formatReservationResultType,
formatReservationRouteCode,
formatReservationMaybeStableReason,
formatReservationSystemProcessCategory,
formatReservationTaskCard,
isReservationSourceMessageOnlyTask,
formatReservationTaskRouteSummary,
isReservationFallbackManualReviewTask,
isReservationReadonlyDiagnosticTask,
isReservationSourceMessageNotificationTask,
isReservationTypeKnownManualReviewTask,
normalizeStableCode,
} from '@/utils/reservationDisplay'
import { formatReservationDateTime } from '@/utils/reservationFormat'
import { useAuthStore } from '@/stores/authStore'
@@ -267,23 +391,56 @@ const audits = ref<ReservationTaskAuditLogResult[]>([])
const fieldValues = ref<ReservationRecord>({})
const validationErrors = ref<ReservationFieldValidationErrors>({})
const validationSummaryActive = ref(false)
const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | null>(null)
const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | 'manual-resolve' | null>(null)
const isSourceMessageOnlyDetail = computed(() =>
isReservationSourceMessageOnlyTask(detail.value?.system_task_type, detail.value?.task_card_type),
isReservationSourceMessageNotificationTask(detail.value),
)
const isReadonlyDiagnosticDetail = computed(() => isReservationReadonlyDiagnosticTask(detail.value))
const isTypeKnownManualReviewDetail = computed(() => isReservationTypeKnownManualReviewTask(detail.value))
const isPendingTypeKnownManualReviewDetail = computed(
() => isTypeKnownManualReviewDetail.value && normalizeStableCode(detail.value?.review_status) !== 'RESOLVED',
)
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
const canEditTask = computed(() => authStore.hasPermission('RESERVATION_TASK_EDIT') && !isSourceMessageOnlyDetail.value)
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? null)
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
const isFieldRendererReadOnly = computed(
() =>
Boolean(detail.value?.availability.read_only) ||
Boolean(detail.value?.availability.blocked) ||
isReadonlyDiagnosticDetail.value ||
isPendingTypeKnownManualReviewDetail.value,
)
const canEditTask = computed(
() =>
authStore.hasPermission('RESERVATION_TASK_EDIT') &&
!isReadonlyDiagnosticDetail.value &&
!isPendingTypeKnownManualReviewDetail.value &&
!detail.value?.availability.blocked &&
!detail.value?.availability.read_only,
)
const canConfirmTask = computed(
() => authStore.hasPermission('RESERVATION_TASK_CONFIRM') && !isSourceMessageOnlyDetail.value,
() =>
authStore.hasPermission('RESERVATION_TASK_CONFIRM') &&
!isReadonlyDiagnosticDetail.value &&
!isPendingTypeKnownManualReviewDetail.value &&
!detail.value?.availability.blocked &&
!detail.value?.availability.read_only,
)
const canExecuteOpera = computed(
() => authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE') && !isSourceMessageOnlyDetail.value,
() =>
authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE') &&
!isReadonlyDiagnosticDetail.value &&
!isPendingTypeKnownManualReviewDetail.value &&
!detail.value?.availability.blocked &&
!detail.value?.availability.read_only &&
Boolean(detail.value?.availability.executable),
)
const canViewAudit = computed(() => authStore.hasPermission('RESERVATION_AUDIT_READ'))
const canSave = computed(() => canEditTask.value && Boolean(detail.value?.availability.editable))
const canConfirm = computed(() => canConfirmTask.value && Boolean(detail.value?.availability.confirmable))
const isManualReviewTask = computed(() => (detail.value ? taskHasManualReviewCode(detail.value) : false))
const validationErrorMessages = computed(() => toValidationErrorMessages(validationErrors.value))
watch(
@@ -368,6 +525,33 @@ async function submitManualReviewConversion(request: ReservationManualReviewConv
}
}
async function submitManualReviewResolution(request: ReservationManualReviewResolutionRequest): Promise<void> {
if (!detail.value) {
return
}
actionErrorMessage.value = ''
notice.value = ''
mutationBusy.value = 'manual-resolve'
try {
await resolveReservationManualReviewTask(detail.value.task_id, request)
const [nextDetail, nextAudits] = await Promise.all([
fetchReservationTaskDetail(detail.value.task_id),
canViewAudit.value
? fetchReservationTaskAudits(detail.value.task_id)
: Promise.resolve({
task_id: detail.value.task_id,
items: [],
}),
])
applyTaskData(nextDetail, nextAudits.items)
notice.value = t('task.manualResolution.succeeded')
} catch (error) {
actionErrorMessage.value = error instanceof Error ? error.message : String(error)
} finally {
mutationBusy.value = null
}
}
async function mutateTaskPayload(action: 'save' | 'confirm'): Promise<void> {
if (!detail.value) {
return
@@ -415,22 +599,6 @@ function applyTaskData(nextDetail: ReservationTaskDetailResult, nextAudits: Rese
validationSummaryActive.value = false
}
function taskHasManualReviewCode(taskDetail: ReservationTaskDetailResult): boolean {
const taskCodes = [
taskDetail.system_task_type,
taskDetail.task_card_type,
...taskDetail.fields.flatMap((field) => [field.result_type, field.task_type, field.task_subtype]),
]
return taskCodes.some((code) => {
const normalizedCode = normalizeStableCode(code)
return ['MANUAL_REVIEW', 'FALLBACK', 'FALLBACK_REVIEW'].includes(normalizedCode)
})
}
function normalizeStableCode(code: string | null | undefined): string {
return code?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
}
function validateBeforeConfirm(taskDetail: ReservationTaskDetailResult): boolean {
const nextErrors = validateReservationFieldValues(taskDetail.fields, fieldValues.value)
validationErrors.value = nextErrors
@@ -483,6 +651,10 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
),
}
}
function formatJson(value: unknown): string {
return JSON.stringify(value ?? {}, null, 2)
}
</script>
<style scoped>
@@ -553,7 +725,9 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
color: var(--th-color-slate-500);
}
.task-evidence {
.task-evidence,
.task-route-info,
.task-ai-diagnostics {
display: grid;
gap: 12px;
border: 1px solid var(--th-color-slate-200);
@@ -561,6 +735,18 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
padding: 16px;
}
.task-route-info {
background: var(--th-color-slate-50);
}
.task-ai-diagnostics {
background: var(--th-color-white);
}
.task-ai-diagnostics--error {
border-color: var(--th-color-danger);
}
.source-only-result {
display: grid;
gap: 12px;
@@ -572,6 +758,10 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
.task-evidence h2,
.task-evidence dl,
.task-route-info h2,
.task-route-info dl,
.task-ai-diagnostics h2,
.task-ai-diagnostics pre,
.source-only-result h2,
.source-only-result p,
.source-only-result dl {
@@ -579,6 +769,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
}
.task-evidence h2,
.task-route-info h2,
.task-ai-diagnostics h2,
.source-only-result h2 {
color: var(--th-color-slate-900);
font-size: 15px;
@@ -596,7 +788,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
line-height: 1.6;
}
.task-evidence dl {
.task-evidence dl,
.task-route-info dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
@@ -608,13 +801,15 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
gap: 12px;
}
.task-evidence dt {
.task-evidence dt,
.task-route-info dt {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 700;
}
.task-evidence dd,
.task-route-info dd,
.source-only-result dd {
margin: 5px 0 0;
color: var(--th-color-slate-900);
@@ -626,7 +821,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
grid-column: 1 / -1;
}
.source-only-result pre {
.source-only-result pre,
.task-ai-diagnostics pre {
max-height: 180px;
overflow: auto;
border: 1px solid var(--th-color-slate-200);
@@ -636,6 +832,24 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
white-space: pre-wrap;
}
.task-ai-diagnostics article {
display: grid;
gap: 8px;
border-top: 1px solid var(--th-color-slate-200);
padding-top: 10px;
}
.task-ai-diagnostics article:first-of-type {
border-top: 0;
padding-top: 0;
}
.task-ai-diagnostics article p {
margin: 0;
color: var(--th-color-slate-600);
font-size: 13px;
}
.task-alert,
.task-notice {
display: flex;
@@ -715,13 +929,15 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
grid-template-columns: 1fr;
}
.task-evidence dl {
.task-evidence dl,
.task-route-info dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.task-evidence dl,
.task-route-info dl,
.source-only-result dl {
grid-template-columns: 1fr;
}