diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 52509809..1e2ba78c 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -55,7 +55,7 @@ describe('ContainersView', () => { vi.mocked(getTerminalDrayage).mockResolvedValue({ ...drayage, allowedActions: [...drayage.allowedActions] }) vi.mocked(getTerminalDrayageHistory).mockResolvedValue([]) vi.mocked(selectTerminalDrayageFleet).mockResolvedValue({ ...drayage, fleetSupplierId: '502', fleetSupplierName: '新车队', version: 4, allowedActions: [...drayage.allowedActions] }) - vi.mocked(recordTerminalDrayageEmptyReturn).mockResolvedValue({ ...drayage, status: 'EMPTY_RETURNED', actualEmptyReturnAt: '2026-09-11T13:00:00', version: 4, emptyReturnWarning: { status: 'RESOLVED', deadlineAt: '2026-09-10T10:00:00', overdueHours: 0, overdueDays: 0 }, allowedActions: ['SELECT_FLEET'] }) + vi.mocked(recordTerminalDrayageEmptyReturn).mockReset().mockResolvedValue({ ...drayage, status: 'EMPTY_RETURNED', actualEmptyReturnAt: '2026-09-11T13:00:00', version: 4, emptyReturnWarning: { status: 'RESOLVED', deadlineAt: '2026-09-10T10:00:00', overdueHours: 0, overdueDays: 0 }, allowedActions: ['SELECT_FLEET'] }) vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER', 'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN'], terminalDrayage: drayage, oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER'], oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }) vi.mocked(getContainerOverview).mockResolvedValue({ container: { id: '9001', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'WAIT_LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: null, vesselName: null, version: 0 }, loadingPlanId: null, pda: null, documents: [], exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [], timeline: [] }) @@ -84,12 +84,54 @@ describe('ContainersView', () => { expect(buttons).toHaveLength(1) await buttons[0].trigger('click') await flushPromises() + const returnDialog = wrapper.findAllComponents(AppDialog).find((dialog) => dialog.props('title') === '确认还柜')! + returnDialog.findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-09-11T13:00:00') + await returnDialog.find('textarea').setValue('还柜凭证已核对') + const callsBefore = vi.mocked(listContainers).mock.calls.length + const page: Awaited> = await vi.mocked(listContainers).mock.results.at(-1)!.value + vi.mocked(listContainers).mockResolvedValueOnce({ ...page, records: page.records.map((row) => ({ ...row, allowedActions: [], terminalDrayage: { ...drayage, status: 'EMPTY_RETURNED', version: 4, allowedActions: [], emptyReturnWarning: { status: 'RESOLVED', deadlineAt: null, overdueHours: 0, overdueDays: 0 } } })) }) const confirm = [...document.body.querySelectorAll('button')].filter((item) => item.textContent?.includes('确认还柜')).at(-1) as HTMLButtonElement confirm.click() await flushPromises() expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledTimes(1) - expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledWith('9001', expect.objectContaining({ expectedVersion: 3, actualAt: expect.any(String) })) + expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledWith('9001', { expectedVersion: 3, actualAt: '2026-09-11T13:00:00', remark: '还柜凭证已核对' }) expect(wrapper.text()).toContain('已还柜') + expect(listContainers).toHaveBeenCalledTimes(callsBefore + 1) + expect(wrapper.findAll('.row-actions button').some((button) => button.text() === '确认还柜')).toBe(false) + wrapper.unmount() + }) + + it('cancels empty return without writing and preserves input on failure while preventing duplicate requests', async () => { + const router = createTestRouter() + await router.push('/'); await router.isReady() + const wrapper = mount(ContainersView, { props: { workbench: 'OVERSEAS' }, attachTo: document.body, global: { plugins: [ElementPlus, router] } }) + await flushPromises() + const open = async () => { + await wrapper.findAll('.row-actions button').find((button) => button.text() === '确认还柜')!.trigger('click') + await flushPromises() + return wrapper.findAllComponents(AppDialog).find((dialog) => dialog.props('title') === '确认还柜')! + } + const first = await open() + first.vm.$emit('update:modelValue', false) + await flushPromises() + expect(recordTerminalDrayageEmptyReturn).not.toHaveBeenCalled() + const dialog = await open() + const state = wrapper.vm as unknown as { drayageActionAt: string; drayageReason: string; drayageActionOpen: boolean; drayageActionError: { message: string } | null } + state.drayageActionAt = '2026-09-11T13:00:00' + state.drayageReason = '还柜凭证已核对' + let reject!: (reason: Error) => void + vi.mocked(recordTerminalDrayageEmptyReturn).mockReturnValueOnce(new Promise((_resolve, rejectRequest) => { reject = rejectRequest })) + dialog.vm.$emit('confirm') + dialog.vm.$emit('confirm') + expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledTimes(1) + expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledWith('9001', { expectedVersion: 3, actualAt: '2026-09-11T13:00:00', remark: '还柜凭证已核对' }) + reject(new Error('还柜时间不符合业务要求')) + await flushPromises() + expect(state.drayageActionOpen).toBe(true) + expect(state.drayageActionAt).toBe('2026-09-11T13:00:00') + expect(state.drayageReason).toBe('还柜凭证已核对') + expect(state.drayageActionError).not.toBeNull() + wrapper.unmount() }) it('hides the direct empty-return action without permission or without the server action', async () => { @@ -352,6 +394,8 @@ describe('ContainersView', () => { vm.openDrayageAction(vm.rows[0], 'RECORD_EMPTY_RETURN') vm.drayageActionAt = '2026-09-11T13:00:00' + const page: Awaited> = await vi.mocked(listContainers).mock.results.at(-1)!.value + vi.mocked(listContainers).mockResolvedValueOnce({ ...page, records: page.records.map((row) => ({ ...row, allowedActions: [], terminalDrayage: { ...drayage, status: 'EMPTY_RETURNED', version: 5, allowedActions: [], emptyReturnWarning: { status: 'RESOLVED', deadlineAt: null, overdueHours: 0, overdueDays: 0 } } })) }) await vm.saveDrayageAction() expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledWith('9001', { expectedVersion: 4, actualAt: '2026-09-11T13:00:00', remark: undefined }) expect(wrapper.text()).toContain('已还柜') diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index 425719f1..c8fc56c1 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -570,6 +570,7 @@ async function saveDrayageAction() { : await recordTerminalDrayageEmptyReturn(row.id, { expectedVersion: current.version, actualAt: drayageActionAt.value, remark: reason }) replaceDrayage(next); drayageActionOpen.value = false ElMessage.success(`${drayageActionText(drayageAction.value)}成功`) + await loadRows() } catch (error) { const details = getApiErrorDetails(error, `${drayageActionText(drayageAction.value)}失败`) if (details.code === 'A0409') {