From 9ad294f406f1536cdb2096bcf7d62f281ba1fb99 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 13 Jul 2026 10:09:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=BA=E5=B7=A5=E5=A4=8D?= =?UTF-8?q?=E6=A0=B8=E5=BF=85=E5=A1=AB=E5=AD=97=E6=AE=B5=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ReservationManualReviewResolutionPanel.vue | 75 ++++++++++++++++--- .../tests/reservationTaskDetailPanel.spec.ts | 19 +++++ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/client/src/components/reservation/ReservationManualReviewResolutionPanel.vue b/client/src/components/reservation/ReservationManualReviewResolutionPanel.vue index 6602590..bd0fb7a 100644 --- a/client/src/components/reservation/ReservationManualReviewResolutionPanel.vue +++ b/client/src/components/reservation/ReservationManualReviewResolutionPanel.vue @@ -141,6 +141,7 @@ import type { } from '@/types/reservation' import { normalizeStableCode } from '@/utils/reservationDisplay' import { + isReservationFieldRequired, isReservationManualReviewFieldWritable, readReservationFieldValue, } from '@/utils/reservationFieldRules' @@ -184,11 +185,7 @@ const formErrorMessage = ref('') const fieldOverrideInputs = ref>({}) const isResolved = computed(() => normalizeStableCode(props.reviewStatus) === 'RESOLVED') const reviewStatusLabel = computed(() => props.reviewStatus || t('task.manualResolution.pendingStatus')) -const supportedMissingFields = computed(() => - extractMissingFields(props.manualReview) - .map((reference) => resolveMissingField(reference)) - .filter((item): item is ManualResolutionFieldItem => item !== null), -) +const supportedMissingFields = computed(() => buildReviewFieldItems()) const unsupportedMissingFields = computed(() => extractMissingFields(props.manualReview).filter((reference) => resolveMissingField(reference) === null), ) @@ -253,17 +250,58 @@ function resolveMissingField(reference: string): ManualResolutionFieldItem | nul item.legacy_field_path === referenceFieldPath, ) if (field && isReservationManualReviewFieldWritable(field)) { - return { - key: field.field_pointer ?? field.field_path, - pointer: field.field_pointer ?? pointer, - fieldPath: field.field_path, - label: field.display_name, - field, - } + return reviewFieldItemFromField(field, pointer) } return null } +function buildReviewFieldItems(): ManualResolutionFieldItem[] { + const items: ManualResolutionFieldItem[] = [] + const seenKeys = new Set() + extractMissingFields(props.manualReview).forEach((reference) => { + const item = resolveMissingField(reference) + if (item) { + appendReviewFieldItem(items, seenKeys, item) + } + }) + props.fields + .filter((field) => isReservationManualReviewFieldWritable(field)) + .filter((field) => isReservationFieldRequired(field)) + .filter((field) => isEmptyReviewFieldValue(readReservationFieldValue(field, props.fieldValues))) + .forEach((field) => appendReviewFieldItem(items, seenKeys, reviewFieldItemFromField(field))) + return items +} + +function reviewFieldItemFromField( + field: ReservationTaskFieldResult, + fallbackPointer?: string, +): ManualResolutionFieldItem { + return { + key: field.field_pointer ?? field.field_path, + pointer: field.field_pointer ?? fallbackPointer, + fieldPath: field.field_path, + label: field.display_name, + field, + } +} + +function appendReviewFieldItem( + items: ManualResolutionFieldItem[], + seenKeys: Set, + item: ManualResolutionFieldItem, +): void { + const dedupeKeys = [ + item.pointer, + item.fieldPath, + item.field?.legacy_field_path, + ].filter((key): key is string => Boolean(key)) + if (dedupeKeys.some((key) => seenKeys.has(key))) { + return + } + dedupeKeys.forEach((key) => seenKeys.add(key)) + items.push(item) +} + function readCurrentFieldValue(item: ManualResolutionFieldItem): string { const value = item.field ? readReservationFieldValue(item.field, props.fieldValues) @@ -275,6 +313,19 @@ function fieldPointerToFieldPath(pointer: string): string { return pointer.replace(/^\//, '').replace(/\//g, '.') } +function isEmptyReviewFieldValue(value: unknown): boolean { + if (value === null || value === undefined) { + return true + } + if (typeof value === 'string') { + return value.trim() === '' + } + if (Array.isArray(value)) { + return value.length === 0 + } + return false +} + function formatJson(value: unknown): string { return JSON.stringify(value ?? {}, null, 2) } diff --git a/client/src/tests/reservationTaskDetailPanel.spec.ts b/client/src/tests/reservationTaskDetailPanel.spec.ts index 81a475f..873bbf4 100644 --- a/client/src/tests/reservationTaskDetailPanel.spec.ts +++ b/client/src/tests/reservationTaskDetailPanel.spec.ts @@ -920,6 +920,17 @@ describe('ReservationTaskDetailPanel', () => { input_editable: 'N', value: null, }), + createRequiredField({ + field_path: 'extracted_fields.room_items.0.room_quantity', + field_pointer: '/extracted_fields/room_items/0/room_quantity', + legacy_field_path: 'extracted_fields.room_quantity', + display_name: '房量', + control_type: 'number', + edit_scope: 'normal_and_manual_review', + write_target: 'draft_payload_and_review_resolution', + raw_readonly: false, + value: null, + }), ], }), ) @@ -974,12 +985,15 @@ describe('ReservationTaskDetailPanel', () => { expect(wrapper.text()).toContain('人工复核解阻') expect(wrapper.text()).toContain('/extracted_fields/room_items/0/pms_room_type_code') expect(wrapper.text()).toContain('PMS 房型代码') + expect(wrapper.text()).toContain('/extracted_fields/room_items/0/room_quantity') + expect(wrapper.text()).toContain('房量') expect(wrapper.text()).not.toContain('人工转换') expect(wrapper.text()).not.toContain('保存草稿') expect(wrapper.text()).not.toContain('确认任务') expect(wrapper.text()).not.toContain('OPERA 模拟操作') await wrapper.find('input[name="manual_resolution_field_override_0"]').setValue('RM3') + await wrapper.find('input[name="manual_resolution_field_override_1"]').setValue('2') await wrapper.find('textarea[name="manual_resolution_reason"]').setValue('确认 PMS 房型代码') await findButton(wrapper, '提交复核结果').trigger('click') await flushPromises() @@ -993,6 +1007,11 @@ describe('ReservationTaskDetailPanel', () => { field_path: 'extracted_fields.room_items.0.pms_room_type_code', value: 'RM3', }, + { + field_pointer: '/extracted_fields/room_items/0/room_quantity', + field_path: 'extracted_fields.room_items.0.room_quantity', + value: '2', + }, ], }) expect(service.fetchReservationTaskDetail).toHaveBeenCalledTimes(2)