From 39b9a6a1a0b5ac89acd05544e1cc2226d0e30d07 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 28 Oct 2026 12:01:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A2=84=E7=BA=A6=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E4=B8=8E=E6=8E=92=E8=BD=A6=E8=A1=A8=E5=8D=95=E5=B9=B6?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=A1=E8=B4=B9=E5=92=8C=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentCalendarDialogs.native.test.ts | 21 +++++--- .../AppointmentConfirmationCargo.test.ts | 4 +- .../AppointmentConfirmationCargo.vue | 3 +- .../AppointmentCreatePanel.vue | 4 +- .../CargoVerificationForm.test.ts | 11 +++- .../CargoVerificationForm.vue | 2 +- .../WorkCreateAppointmentForm.test.ts | 4 +- .../WorkCreateAppointmentForm.vue | 2 +- .../WorkReceivingPanel.test.ts | 2 +- .../cargoVerificationApi.ts | 2 +- .../InboundMeasurementPanel.test.ts | 54 +++++++++++++++++-- .../measurement/InboundMeasurementPanel.vue | 27 ++++++++-- .../appointmentForm.test.ts | 4 +- .../overseas-warehouse/appointmentForm.ts | 2 +- .../DeliveryDocumentsDialog.test.ts | 19 ++++--- .../components/DeliveryDocumentsDialog.vue | 32 ++++++----- ...spatchAppointmentAllocationsDialog.test.ts | 13 +++++ .../DispatchAppointmentAllocationsDialog.vue | 4 +- .../components/FbaAppointmentFields.test.ts | 26 +++++++++ .../components/FbaAppointmentFields.vue | 18 +++---- .../overseas-warehouse/dispatchContracts.ts | 2 +- .../pages/DeliveryDialogs.test.ts | 10 ++-- .../pages/UnifiedOrdersView.requests.test.ts | 31 ++++++++++- .../pages/UnifiedOrdersView.vue | 7 +-- src/shared/components/QueryToolbar.test.ts | 39 ++++++++++++++ src/shared/components/QueryToolbar.vue | 4 +- src/styles/adminDensity.test.ts | 7 +-- 27 files changed, 277 insertions(+), 77 deletions(-) create mode 100644 src/modules/overseas-warehouse/components/FbaAppointmentFields.test.ts diff --git a/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts index 918ca3af..e0b9f2b0 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts @@ -11,6 +11,7 @@ import { batchContext } from './batch.test-support' import type { ScheduleEvent } from './schedule' import { getWorkDetail, type WorkAction } from './workApi' import { workDetail, workItem } from './work.test-support' +import { getCargoVerification } from './cargoVerificationApi' const routing = vi.hoisted(() => ({ query: { date: '2026-10-19' } as Record, push: vi.fn(), replace: vi.fn(), denied: new Set() })) vi.mock('vue-router', () => ({ useRoute: () => routing, useRouter: () => routing, onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() })) @@ -19,6 +20,7 @@ vi.mock('./scheduleApi', () => ({ getCalendarSources: vi.fn(), getWarehouseArriv vi.mock('./tripCalendar', async original => ({ ...await original(), listTripCalendar: vi.fn() })) vi.mock('./planningApi', async original => ({ ...await original(), listPlanningCargo: vi.fn(), previewPlanningBatch: vi.fn(), resolvePlanningSource: vi.fn(), createSourceAppointment: vi.fn() })) vi.mock('./workApi', async original => ({ ...await original(), getWorkDetail: vi.fn() })) +vi.mock('./cargoVerificationApi', () => ({ getCargoVerification: vi.fn(), saveCargoVerification: vi.fn() })) enableAutoUnmount(afterEach) afterEach(() => { vi.unstubAllGlobals() }) const warehouse: WarehouseArrival = { sourceContainerBookingId: '9007199254740999', containerNo: 'TEST-CONT', warehouseName: '美国仓', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 3, planCount: 0, canEdit: true } @@ -35,6 +37,7 @@ beforeEach(() => { vi.mocked(saveWarehouseArrival).mockResolvedValue({}) vi.mocked(listPlanningCargo).mockResolvedValue({ records: [cargo], total: 1, page: 1, size: 20, pages: 1 }) vi.mocked(resolvePlanningSource).mockResolvedValue({ source, coverage: { status: 'NONE', missingFields: [], changeReviewRequired: false, appointments: [] } }) + vi.mocked(getCargoVerification).mockResolvedValue({ source, verification: null, allocatedPackages: 0, unknownAllocationCount: 0, effectivePackages: 20, effectiveShipmentReferences: ['FBA-TEST'], availablePackages: 20, sourceChanged: false, allowedActions: [], blockReasons: [] }) vi.mocked(previewPlanningBatch).mockResolvedValue(preview) vi.mocked(createSourceAppointment).mockResolvedValue(batchContext().appointment) vi.mocked(getWorkDetail).mockResolvedValue(workDetail()) @@ -52,6 +55,7 @@ async function create(wrapper: Awaited>) { const content = creationBody() await content.get('input[type="checkbox"]').setValue(true) await content.findAll('button').find(button => button.text().startsWith('下一步'))!.trigger('click'); await flushPromises() + await content.get('input[placeholder="填写收货方提供的确认号"]').setValue('CONFIRM-NEW'); await flushPromises() return dialog } function creationBody() { return new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('新增FBA送仓预约'))!) } @@ -72,17 +76,19 @@ it('keeps the arrival context and typed local time across native fullscreen and await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() expect(saveWarehouseArrival).toHaveBeenCalledExactlyOnceWith(warehouse.sourceContainerBookingId, { estimatedWarehouseArrivalAt: '2026-10-22T09:30:00', expectedVersion: 3 }, expect.any(String)) }) -it('locks the real arrival input after an unknown outcome and reuses its immutable request', async () => { +it('keeps the real arrival input editable after a failed attempt and submits the edited time with a fresh key', async () => { vi.mocked(saveWarehouseArrival).mockRejectedValueOnce(new Error('connection lost')) const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! dialog.getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-10-22T09:30'); await flushPromises() await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() - expect(dialog.get('input').attributes('disabled')).toBeDefined() - expect(dialog.text()).toContain('上次提交结果尚未确认') - expect(dialog.get('.el-dialog__footer .el-button--primary').text()).toBe('重试确认保存结果') + expect(dialog.get('input').attributes('disabled')).toBeUndefined() + expect(dialog.get('.el-dialog__footer .el-button--primary').text()).toBe('保存') + dialog.getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-10-23T09:30'); await flushPromises() await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() - expect(vi.mocked(saveWarehouseArrival).mock.calls[1]).toEqual(vi.mocked(saveWarehouseArrival).mock.calls[0]) + const calls = vi.mocked(saveWarehouseArrival).mock.calls + expect(calls[1]![2]).not.toBe(calls[0]![2]) + expect(calls[1]![1]).toEqual({ estimatedWarehouseArrivalAt: '2026-10-23T09:30:00', expectedVersion: 3 }) }) it('clears the Element arrival picker without saving an empty required time', async () => { const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() @@ -114,7 +120,7 @@ it('preserves real FBA fields and entered values when expanding the create dialo expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) expect(date.props('modelValue')).toBe('2026-10-25') await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() - expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: 'FBA:LAX9' }], details: expect.objectContaining({ plannedDate: '2026-10-25', plannedAt: null, appointmentStatus: 'PENDING' }), idempotencyKey: expect.any(String) })) + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: 'FBA:LAX9' }], details: expect.objectContaining({ plannedDate: '2026-10-25', plannedAt: null, appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-NEW', appointmentAt: null }), idempotencyKey: expect.any(String) })) expect(creationBody().get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) expect(creationBody().text()).toContain('AP-001') }) @@ -141,9 +147,10 @@ it('uses the same real continuous form through the original work detail deep lin const wrapper = await open(); await vi.dynamicImportSettled(); await flushPromises() const content = new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('原业务预约办理'))!) await content.get('.primary-next-action').trigger('click'); await flushPromises() + await content.get('input[placeholder="填写收货方提供的确认号"]').setValue('CONFIRM-NEW'); await flushPromises() expect(wrapper.getComponent({ name: 'FbaAppointmentFields' }).props('continuous')).toBe(true) expect(previewPlanningBatch).toHaveBeenCalledExactlyOnceWith(action.target.kind === 'FBA_APPOINTMENT' ? action.target.sources : []) await content.findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() - expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }], details: expect.objectContaining({ plannedAt: null, appointmentStatus: 'PENDING' }) })) + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }], details: expect.objectContaining({ plannedAt: null, appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-NEW', appointmentAt: null }) })) expect(content.text()).toContain('本次保存已完成') }) diff --git a/src/modules/appointment-calendar/AppointmentConfirmationCargo.test.ts b/src/modules/appointment-calendar/AppointmentConfirmationCargo.test.ts index 931cac25..60e3234b 100644 --- a/src/modules/appointment-calendar/AppointmentConfirmationCargo.test.ts +++ b/src/modules/appointment-calendar/AppointmentConfirmationCargo.test.ts @@ -12,7 +12,7 @@ vi.mock('./planningApi', () => ({ resolvePlanningOrder: vi.fn(), resolvePlanning vi.mock('./cargoVerificationApi', () => ({ getCargoVerification: vi.fn(), saveCargoVerification: vi.fn() })) const source: ResolvedPlanningSource['source'] = { sourceKey: 'BOOKING:BK-TEST', customerId: 'customer-1', orderId: '9007199254740993', inboundCode: 'IN-TEST', bookingNo: 'BK-TEST', containerNo: null, originWarehouseId: 'warehouse-1', originWarehouseName: '测试仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: 20, sourceVersion: 'BOOKING:1/ORDER:1', canFollowup: true, blockReasons: [] } const resolved: ResolvedPlanningSource = { source, coverage: { status: 'PENDING', appointments: [], missingFields: ['FBA_SHIPMENT_REFERENCES'], changeReviewRequired: false } } -const context: CargoVerificationContext = { source, verification: null, allocatedPackages: 20, unknownAllocationCount: 0, effectivePackages: 20, availablePackages: 0, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [] } +const context: CargoVerificationContext = { source, verification: null, allocatedPackages: 20, unknownAllocationCount: 0, effectivePackages: 20, effectiveShipmentReferences: [], availablePackages: 0, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [] } const lookup = { orderId: source.orderId!, customerId: source.customerId, destinationKey: source.destinationKey } function render(active = true, disabled = false) { return mount(AppointmentConfirmationCargo, { props: { sources: [lookup], active, disabled }, global: { stubs: { @@ -52,8 +52,10 @@ it('leaves pending arrangements lazy and reads only when formal confirmation is }) it('does not ask for known materials to be entered again', async () => { vi.mocked(resolvePlanningOrder).mockResolvedValue({ ...resolved, coverage: { ...resolved.coverage, missingFields: [] } }) + vi.mocked(getCargoVerification).mockResolvedValue({ ...context, effectiveShipmentReferences: ['FBA-ORIGINAL', 'TRACK-ORIGINAL'] }) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('资料齐全,无需重复填写') + expect(wrapper.text()).toContain('FBA-ORIGINAL、TRACK-ORIGINAL') expect(wrapper.findAll('button').some(button => button.text() === '在此补齐资料')).toBe(false) expect(wrapper.emitted('ready')?.at(-1)).toEqual([true]); wrapper.unmount() }) diff --git a/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue b/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue index dd32444a..57027ebc 100644 --- a/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue +++ b/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue @@ -68,11 +68,12 @@ onBeforeUnmount(() => { ++generation; unregister?.() }) 重新核对资料

本票总箱数用于校验预约货量;FBA 货件号或追踪号用于识别送的是哪批货。外部约号只代表仓库的预约确认号,不能代替货件号。

- +
操作入仓码 / 外接登记号本票总箱数需要补充的资料
+
操作入仓码 / 外接登记号本票总箱数FBA 货件号 / 追踪号需要补充的资料
在此补齐资料— {{ row.resolved.source.inboundCode || row.resolved.source.bookingNo || '未提供业务编号' }} {{ row.cargo.effectivePackages ?? '待补充' }}{{ row.cargo.effectiveShipmentReferences.join('、') || '—' }} {{ issues(row).join(';') || '资料齐全,无需重复填写' }}
diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.vue b/src/modules/appointment-calendar/AppointmentCreatePanel.vue index d23e1427..607e8786 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.vue +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.vue @@ -131,8 +131,8 @@ onBeforeUnmount(() => { ++generation; ++selectionGeneration; ++destinationsReque 计划送仓日期{{ createdForm.plannedDate || '—' }}本次登记{{ ({ PENDING: '等待对方确认', SUCCESS: '已取得对方正式确认', FAILED: '未约成', CANCELLED: '已取消' })[createdForm.appointmentStatus] }} 计划开始时段{{ createdForm.plannedAt || '—' }}计划结束时段{{ createdForm.plannedEndAt || '—' }} 外部约号{{ createdForm.confirmationNo || '—' }} - 对方确认开始时间{{ createdForm.appointmentAt || '—' }}对方确认结束时间{{ createdForm.appointmentEndAt || '—' }} - 确认开始时差{{ createdForm.startOffset || '—' }}确认结束时差{{ createdForm.endOffset || '—' }} + 对方确认开始时间{{ createdForm.appointmentAt || '—' }}对方确认结束时间{{ createdForm.appointmentEndAt || '—' }} + 确认开始时差{{ createdForm.startOffset || '—' }}确认结束时差{{ createdForm.endOffset || '—' }} {{ createdForm.appointmentStatus === 'FAILED' ? '原因' : '备注' }}{{ createdForm.appointmentRemark || '—' }}

预约已保存,请继续在原业务安排车辆。

diff --git a/src/modules/appointment-calendar/CargoVerificationForm.test.ts b/src/modules/appointment-calendar/CargoVerificationForm.test.ts index daee0cb2..1a5bd982 100644 --- a/src/modules/appointment-calendar/CargoVerificationForm.test.ts +++ b/src/modules/appointment-calendar/CargoVerificationForm.test.ts @@ -10,7 +10,7 @@ vi.mock('./cargoVerificationApi', () => ({ getCargoVerification: vi.fn(), saveCa const source = { sourceKey: 'ORDER:9007199254740993', customerId: 'customer-1', destinationKey: 'FBA:ONT8' } const context: CargoVerificationContext = { source: { ...source, orderId: '9007199254740993', inboundCode: 'IN-TEST-001', bookingNo: null, containerNo: 'TEST-CN-001', originWarehouseId: 'warehouse-1', originWarehouseName: '测试仓', destinationWarehouseCode: 'ONT8', packages: null, sourceVersion: null, canFollowup: true, blockReasons: [] }, - verification: null, allocatedPackages: 0, unknownAllocationCount: 0, effectivePackages: null, availablePackages: null, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [], + verification: null, allocatedPackages: 0, unknownAllocationCount: 0, effectivePackages: null, effectiveShipmentReferences: [], availablePackages: null, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [], } function render() { return mount(CargoVerificationForm, { props: { source }, global: { stubs: { ElButton: { props: ['nativeType'], template: '' }, LoadErrorState: { props: ['message'], template: '
{{ message }}
' }, ApiErrorMeta: true } } }) } beforeEach(() => { @@ -26,6 +26,15 @@ it('saves independent cargo verification before any appointment exists, preservi expect(vi.mocked(saveCargoVerification).mock.calls[0]![1]).not.toHaveProperty('appointmentStatus') expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount() }) +it('prefills existing order shipment and tracking numbers without creating a verification record', async () => { + vi.mocked(getCargoVerification).mockResolvedValue({ ...context, effectivePackages: 20, effectiveShipmentReferences: ['FBA-ORIGINAL', 'TRACK-ORIGINAL'] }) + const wrapper = render(); await flushPromises() + expect((wrapper.get('textarea[aria-label="本票货件号"]').element as HTMLTextAreaElement).value).toBe('FBA-ORIGINAL\nTRACK-ORIGINAL') + expect(saveCargoVerification).not.toHaveBeenCalled() + await wrapper.get('form').trigger('submit'); await flushPromises() + expect(saveCargoVerification).toHaveBeenCalledWith(source, expect.objectContaining({ shipmentReferences: ['FBA-ORIGINAL', 'TRACK-ORIGINAL'] })) + wrapper.unmount() +}) it('uses the same key and payload after an unknown save outcome', async () => { vi.mocked(saveCargoVerification).mockRejectedValueOnce(new Error('network lost')) const wrapper = render(); await flushPromises() diff --git a/src/modules/appointment-calendar/CargoVerificationForm.vue b/src/modules/appointment-calendar/CargoVerificationForm.vue index 5b02ec3d..93a1ae5f 100644 --- a/src/modules/appointment-calendar/CargoVerificationForm.vue +++ b/src/modules/appointment-calendar/CargoVerificationForm.vue @@ -27,7 +27,7 @@ async function load(preserve = false) { context.value = await getCargoVerification(props.source) if (!preserve) { const record = context.value.verification - Object.assign(form, { packages: context.value.effectivePackages == null ? '' : String(context.value.effectivePackages), references: record?.shipmentReferences.join('\n') || '', remark: record?.remark || '' }) + Object.assign(form, { packages: context.value.effectivePackages == null ? '' : String(context.value.effectivePackages), references: context.value.effectiveShipmentReferences.join('\n'), remark: record?.remark || '' }) baseline.value = JSON.stringify(form) } } catch (cause) { error.value = getApiErrorDetails(cause, '核实资料读取失败') } diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts b/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts index 17ede40a..dc203826 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts @@ -26,7 +26,7 @@ it('shows readable preview requirements without exposing missing-field machine c }) async function confirmRegistration(wrapper: ReturnType) { const fields = wrapper.getComponent({ name: 'FbaAppointmentFields' }) - Object.assign(fields.props('model'), { confirmationNo: 'EXT-CONFIRMED', appointmentAt: '2026-10-16T09:30:00' }) + Object.assign(fields.props('model'), { confirmationNo: 'EXT-CONFIRMED' }) fields.vm.$emit('confirmationReady', true) await flushPromises() } @@ -35,7 +35,7 @@ it('previews and creates the exact canonical external source and customer with r await confirmRegistration(wrapper) expect(previewPlanningBatch).toHaveBeenCalledWith(sources) await wrapper.get('input').setValue('2026-10-16'); await wrapper.get('button').trigger('click'); await flushPromises() - expect(createSourceAppointment).toHaveBeenCalledWith(expect.objectContaining({ sources, originWarehouseId: 'origin-1', fbaWarehouseCode: 'ONT8', details: expect.objectContaining({ plannedDate: '2026-10-16', plannedAt: null, plannedEndAt: null, appointmentStatus: 'SUCCESS', confirmationNo: 'EXT-CONFIRMED' }) })) + expect(createSourceAppointment).toHaveBeenCalledWith(expect.objectContaining({ sources, originWarehouseId: 'origin-1', fbaWarehouseCode: 'ONT8', details: expect.objectContaining({ plannedDate: '2026-10-16', plannedAt: null, plannedEndAt: null, appointmentAt: null, appointmentEndAt: null, startOffset: null, endOffset: null, appointmentStatus: 'SUCCESS', confirmationNo: 'EXT-CONFIRMED' }) })) expect(wrapper.emitted('saved')).toHaveLength(1); expect(wrapper.text()).not.toContain('external-1'); wrapper.unmount() }) it('retains the same immutable request after an unknown outcome and blocks leaving', async () => { diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue index 0e8da62c..2ee59e90 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue @@ -63,7 +63,7 @@ onMounted(load)

{{ workReason(field) }}

- + diff --git a/src/modules/appointment-calendar/WorkReceivingPanel.test.ts b/src/modules/appointment-calendar/WorkReceivingPanel.test.ts index fa91ccef..798a38e3 100644 --- a/src/modules/appointment-calendar/WorkReceivingPanel.test.ts +++ b/src/modules/appointment-calendar/WorkReceivingPanel.test.ts @@ -452,7 +452,7 @@ it('shows authoritative subset blockers and ignores a late response for a previo }) function verifiedCargo(): CargoVerificationContext { - return { source: { sourceKey: 'BOOKING:2', customerId: 'customer-b', orderId: null, inboundCode: 'IN-B', bookingNo: null, containerNo: null, originWarehouseId: 'origin-1', originWarehouseName: '本仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: 40, sourceVersion: 'source-latest', canFollowup: true, blockReasons: [] }, verification: { version: 2, sourceVersion: 'source-latest', destinationKey: 'FBA:ONT8', sourcePackages: 40, packageCount: 40, shipmentReferences: ['FBA-NEW-B'], remark: null, operatorName: '核实人员', verifiedAt: '2026-10-16T10:00:00Z' }, allocatedPackages: 30, unknownAllocationCount: 0, effectivePackages: 40, availablePackages: 10, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [] } + return { source: { sourceKey: 'BOOKING:2', customerId: 'customer-b', orderId: null, inboundCode: 'IN-B', bookingNo: null, containerNo: null, originWarehouseId: 'origin-1', originWarehouseName: '本仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: 40, sourceVersion: 'source-latest', canFollowup: true, blockReasons: [] }, verification: { version: 2, sourceVersion: 'source-latest', destinationKey: 'FBA:ONT8', sourcePackages: 40, packageCount: 40, shipmentReferences: ['FBA-NEW-B'], remark: null, operatorName: '核实人员', verifiedAt: '2026-10-16T10:00:00Z' }, allocatedPackages: 30, unknownAllocationCount: 0, effectivePackages: 40, effectiveShipmentReferences: ['FBA-NEW-B'], availablePackages: 10, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [] } } async function enterRefresh(wrapper: ReturnType) { await flushPromises(); await wrapper.findAll('button').filter(row => row.text() === '核对并采用本客户最新货物资料')[1]!.trigger('click'); await flushPromises() diff --git a/src/modules/appointment-calendar/cargoVerificationApi.ts b/src/modules/appointment-calendar/cargoVerificationApi.ts index 39b637fb..b521ea13 100644 --- a/src/modules/appointment-calendar/cargoVerificationApi.ts +++ b/src/modules/appointment-calendar/cargoVerificationApi.ts @@ -1,7 +1,7 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { PlanningSource } from './planningApi' export interface CargoVerification { version: number; sourceVersion: string | null; destinationKey: string; sourcePackages: number | null; packageCount: number | null; shipmentReferences: string[]; remark: string | null; operatorName: string | null; verifiedAt: string } -export interface CargoVerificationContext { source: PlanningSource; verification: CargoVerification | null; allocatedPackages: number; unknownAllocationCount: number; effectivePackages: number | null; availablePackages: number | null; sourceChanged: boolean; allowedActions: string[]; blockReasons: string[] } +export interface CargoVerificationContext { source: PlanningSource; verification: CargoVerification | null; allocatedPackages: number; unknownAllocationCount: number; effectivePackages: number | null; effectiveShipmentReferences: string[]; availablePackages: number | null; sourceChanged: boolean; allowedActions: string[]; blockReasons: string[] } export interface CargoVerificationSource { sourceKey: string; customerId: string; destinationKey?: string } export interface SaveCargoVerification { destinationKey: string; expectedSourceVersion: string | null; expectedVersion: number; packageCount: number | null; shipmentReferences: string[]; remark: string | null; idempotencyKey: string } const path = (key: string) => `/platform/v1/last-leg/appointment-planning/sources/${encodeURIComponent(key)}/cargo-verification` diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts index 137c5943..f62d82aa 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts @@ -13,7 +13,7 @@ import { submitInboundMeasurement, updateConfirmedPackageCount, } from './api' -import type { InboundMeasurementDetail } from './types' +import type { InboundMeasurementDetail, SaveInboundMeasurementItemRequest } from './types' import InboundMeasurementPanel from './InboundMeasurementPanel.vue' vi.mock('./api', () => ({ @@ -152,6 +152,10 @@ async function mountPanel(permissions = [ return wrapper } +function measurementDraft(wrapper: Awaited>) { + return (wrapper.vm.$ as unknown as { setupState: { form: { items: SaveInboundMeasurementItemRequest[] } } }).setupState.form +} + enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) @@ -317,6 +321,50 @@ describe('InboundMeasurementPanel', () => { expect(wrapper.find('.measurement-declaration').element.parentElement).toBe(wrapper.find('.measurement-summary-section').element.parentElement) }) + it('shows raw measured volume rather than customer weighted billing volume in the rows and comparison', async () => { + const value = measurement({ originalVolumeCbm: 3.3, originalPackageCount: 22, actualPackageCount: 22, actualVolumeCbm: 7.205, volumeWeightKg: 1203.235 }) + Object.assign(value.items[0]!, { + lengthCm: 62, widthCm: 52, heightCm: 52, packageCount: 22, + coefficientMode: 'MULTIPLY', chargeRuleId: '6001', + calculatedOuterLengthCm: 77.5, calculatedOuterWidthCm: 65, calculatedOuterHeightCm: 65, + unitVolumeCbm: 0.3275, totalVolumeCbm: 7.205, + }) + vi.mocked(getInboundMeasurement).mockResolvedValueOnce(value) + const wrapper = await mountPanel() + const comparisonVolume = () => wrapper.get('.measurement-comparison tbody tr:last-child').findAll('td').map(cell => cell.text()) + + expect(comparisonVolume()).toEqual(['3.300', '3.688', '差异 +0.388 m³']) + expect(wrapper.get('.measurement-table').text()).toContain('3.688') + expect(wrapper.get('.measurement-table').text()).not.toContain('7.205') + expect(wrapper.get('.measurement-extra-facts').text()).toContain('计费材积重') + + await wrapper.get('input[aria-label="仓库修改外箱长"]').setValue('80') + expect(comparisonVolume()[1]).toBe('3.688') + await wrapper.get('.measurement-count-editor input').setValue('11') + expect(comparisonVolume()).toEqual(['3.300', '1.844', '差异 -1.456 m³']) + expect(wrapper.get('.measurement-table').text()).toContain('1.844') + expect(value.actualVolumeCbm).toBe(7.205) + expect(saveInboundMeasurementDraft).not.toHaveBeenCalled() + expect(updateConfirmedPackageCount).not.toHaveBeenCalled() + }) + + it('sums measured volumes across rows and does not substitute billing totals for incomplete dimensions', async () => { + const value = measurement({ actualVolumeCbm: 9.999 }) + value.items.push({ ...value.items[0]!, id: '9102', itemNo: '002', boxCode: 'RK202608040001#10-2', lengthCm: 10, widthCm: 20, heightCm: 30, packageCount: 2, totalVolumeCbm: 8.888 }) + vi.mocked(getInboundMeasurement).mockResolvedValueOnce(value) + const wrapper = await mountPanel() + expect(wrapper.get('.measurement-comparison tbody tr:last-child').findAll('td')[1]!.text()).toBe('0.612') + + value.items[1]!.lengthCm = 0 + vi.mocked(getInboundMeasurement).mockResolvedValueOnce(value) + await wrapper.setProps({ inboundId: '7002' }) + await flushPromises() + const cells = wrapper.get('.measurement-comparison tbody tr:last-child').findAll('td') + expect(cells[1]!.text()).toBe('未测量') + expect(cells[2]!.text()).toBe('待测量') + expect(wrapper.get('.measurement-table').text()).not.toContain('8.888') + }) + it('shows pending comparison rather than a negative count when no boxes have been measured', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ items: [], actualPackageCount: null, actualGrossWeightKg: null, actualVolumeCbm: null, @@ -424,7 +472,7 @@ describe('InboundMeasurementPanel', () => { it('retains server-calculated facts in the draft without the removed expandable row', async () => { const wrapper = await mountPanel() expect(wrapper.find('.el-table__expand-icon').exists()).toBe(false) - expect(wrapper.vm.form.items[0]).toMatchObject({ + expect(measurementDraft(wrapper).items[0]).toMatchObject({ boxSpecification: '50×40×30cm', coefficientMode: 'ACTUAL', calculatedOuterLengthCm: 50, calculatedOuterWidthCm: 40, calculatedOuterHeightCm: 30, calculationRuleVersion: 'DIMENSION_ACTUAL_V1', @@ -621,7 +669,7 @@ describe('InboundMeasurementPanel', () => { it('retains the formal deviceMeasuredAt response field in the measurement draft', async () => { const wrapper = await mountPanel() - expect(wrapper.vm.form.items[0].measuredAt).toBe('2026-08-04T10:30:00+08:00') + expect(measurementDraft(wrapper).items[0]!.measuredAt).toBe('2026-08-04T10:30:00+08:00') const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default) expect(source).toContain('measuredAt: item.deviceMeasuredAt') expect(source).not.toContain('measuredAt: item.measuredAt') diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue index 35a038fa..cd04ee59 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue @@ -120,11 +120,28 @@ const mayMaintain = computed(() => canEdit.value && countEditableProcess.value & const manualEntryEditableProcess = computed(() => ['PREPARING', 'WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || '')) const mayCreateManualEntry = computed(() => canManualEntry.value && manualEntryEditableProcess.value && !loading.value && !saving.value && !submitting.value && !isDirty.value) const confirmedPackageCount = computed(() => form.items.reduce((sum, item) => sum + (Number.isFinite(item.packageCount) ? item.packageCount : 0), 0)) -// No scan rows means no measured count yet, rather than a measured zero. +// 实测展示只使用原始尺寸与确认箱数,不套用计费规则或仓库外箱修正,也不回写计费快照。 +function measuredVolumeCbm(item: Pick) { + if (![item.lengthCm, item.widthCm, item.heightCm].every(value => Number.isFinite(value) && value > 0) + || !Number.isInteger(item.packageCount) || item.packageCount < 1) return null + const volume = item.lengthCm * item.widthCm * item.heightCm * item.packageCount / 1_000_000 + return Number.isFinite(volume) ? volume : null +} +const measuredTotalVolumeCbm = computed(() => { + if (!form.items.length) return null + let total = 0 + for (const item of form.items) { + const volume = measuredVolumeCbm(item) + if (volume == null) return null + total += volume + } + return total +}) +// 尚无扫描行时保持未测量,不把缺失的实测事实显示为零。 const comparisonMetrics = computed(() => [ { label: '箱数', declared: detail.value?.originalPackageCount, actual: form.items.length ? confirmedPackageCount.value : null, digits: 0, unit: '' }, { label: '毛重(kg)', declared: detail.value?.originalGrossWeightKg, actual: detail.value?.actualGrossWeightKg, digits: 3, unit: ' kg' }, - { label: '总体积(m³)', declared: detail.value?.originalVolumeCbm, actual: detail.value?.actualVolumeCbm, digits: 3, unit: ' m³' }, + { label: '总体积(m³)', declared: detail.value?.originalVolumeCbm, actual: measuredTotalVolumeCbm.value, digits: 3, unit: ' m³' }, ]) const expectedPackageCount = computed(() => detail.value?.originalPackageCount ?? null) @@ -734,8 +751,8 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
货物净重 {{ formatNumber(detail.actualNetWeightKg) }} kg - 材积重 {{ formatNumber(detail.volumeWeightKg) }} kg - 平台补充数据 + 计费材积重 {{ formatNumber(detail.volumeWeightKg) }} kg + 平台补充数据
@@ -827,7 +844,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock - +