From 4f766a76bddcfa34d1120b52cd9ecf8fbf47fc52 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 23 Oct 2026 12:06:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appointment-calendar/WorkDispatchForm.test.ts | 2 +- .../appointment-calendar/WorkDispatchForm.vue | 2 +- src/modules/loading-lists/AGENTS.md | 2 +- .../overseas-warehouse/api/dispatches.test.ts | 4 ++-- .../components/DispatchAppointments.vue | 8 ++++---- .../components/DispatchVehicleMaintenanceForm.vue | 2 -- .../components/DispatchWorkflowDialog.layout.test.ts | 2 +- .../components/DispatchWorkflowDialog.test.ts | 11 +++++------ .../components/DispatchWorkflowDialog.vue | 12 ++++++------ src/modules/overseas-warehouse/dispatchContracts.ts | 2 +- src/modules/trucking/AGENTS.md | 3 +-- 11 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/modules/appointment-calendar/WorkDispatchForm.test.ts b/src/modules/appointment-calendar/WorkDispatchForm.test.ts index 1d70a290..b83ffb54 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.test.ts +++ b/src/modules/appointment-calendar/WorkDispatchForm.test.ts @@ -89,7 +89,7 @@ it('reads remaining physical cargo only after an explicit choice while retaining }) it('creates the whole dispatch with native source versions and exact batch allocations', async () => { const wrapper = render(); await flushPromises(); await button(wrapper, '创建整车排车单').trigger('click'); await flushPromises() - expect(createDispatch).toHaveBeenCalledWith(expect.objectContaining({ warehouseId: 'origin-1', cargo: [natives[0]!.selection], batchAllocations: row().allocations, vehicle: null, driver: null })) + expect(createDispatch).toHaveBeenCalledWith(expect.objectContaining({ publishLooseboardDemand: false, warehouseId: 'origin-1', cargo: [natives[0]!.selection], batchAllocations: row().allocations, vehicle: null, driver: null })) expect(wrapper.text()).toContain('DS/001'); expect(wrapper.emitted('updated')).toHaveLength(1); expect(await guard()).toBe(true) await button(wrapper, '继续维护本车').trigger('click'); expect(wrapper.emitted('external')).toEqual([['/overseas-warehouse/dispatches/DS%2F001']]); wrapper.unmount() }) diff --git a/src/modules/appointment-calendar/WorkDispatchForm.vue b/src/modules/appointment-calendar/WorkDispatchForm.vue index 86aa7823..e78f7757 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.vue +++ b/src/modules/appointment-calendar/WorkDispatchForm.vue @@ -124,7 +124,7 @@ async function save() { ensureOwner() if (!writable.value || error.value || createdNo.value || !chosen.value.length || chosen.value.some(row => reasons(row).length || !row.native) || physicalCount(chosen.value) > capacity.value) throw new DispatchInputError('请核对本仓实物资格、来源批次与计划托位') candidatePage.assertCurrent(chosen.value.map(row => row.native!)) - const body = { publishLooseboardDemand: true, idempotencyKey: crypto.randomUUID(), warehouseId: warehouseId.value, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null, cargo: expandSelectionGroups(chosen.value.map(row => row.native!)), batchAllocations: chosen.value.flatMap(row => row.allocations) } + const body = { publishLooseboardDemand: false, idempotencyKey: crypto.randomUUID(), warehouseId: warehouseId.value, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null, cargo: expandSelectionGroups(chosen.value.map(row => row.native!)), batchAllocations: chosen.value.flatMap(row => row.allocations) } const proof = await createDispatch(body); ensureOwner() validateCommandResult(proof, { dispatchNo: '', action: 'CREATE', body: { idempotencyKey: body.idempotencyKey } }) createdNo.value = proof.dispatchNo; selected.value = []; baseline.value = '[]'; emit('updated') diff --git a/src/modules/loading-lists/AGENTS.md b/src/modules/loading-lists/AGENTS.md index ad0e0a6e..11ec87eb 100644 --- a/src/modules/loading-lists/AGENTS.md +++ b/src/modules/loading-lists/AGENTS.md @@ -20,7 +20,7 @@ ## 关键边界 - 预装详情进入后先调 `/loading-plans/{planId}/lifecycle`,再按返回状态决定可操作项;不自行推断生命周期阶段。 -- 托盘码格式为“真实柜号-FBA 地址内部编号-三位序号”,清单只取服务端按 FBA 仓分组的冻结结果,前端不生成、不改码、不重新编号;一件代发托盘不进入该清单。 +- 托盘码格式为“柜次号-FBA 地址内部编号-三位序号”,清单只取服务端按 FBA 仓分组的冻结结果,前端不生成、不改码、不重新编号;一件代发托盘不进入该清单。 - 确认预装和创建 PDA 任务前必须补齐真实集装箱号;历史上已建缺号任务可在预装详情首次补录,保存后恢复 PDA 可进入状态。 - 柜级公共费用只按服务端柜内客户/订单事实分摊,创建、审核、驳回、调整、作废均整组原子,金额必须守恒;待确认时禁止调整或重复作废。 - 独立清单详情与货柜详情页签共用同一套清单 API 与详情组件;改展示或打印时同步检查 `container-loading` 的装柜清单页签。 diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts index 947deb60..fe350f93 100644 --- a/src/modules/overseas-warehouse/api/dispatches.test.ts +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -35,7 +35,7 @@ describe('native whole-vehicle dispatch API', () => { const saved = await api.getDispatch('PC/1') expect(saved.vehicleInfo.maintenance).toEqual(maintenance) expect(saved.vehicleInfo.vehicle!.evidenceFileIds).toEqual(['9007199254740993']) - await api.updateDispatch('PC/1', { recordVersion: saved.recordVersion, idempotencyKey: 'maintenance-key', vehicle: saved.vehicleInfo.vehicle, driver: saved.vehicleInfo.driver, maintenance, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null, publishLooseboardDemand: true }) + await api.updateDispatch('PC/1', { recordVersion: saved.recordVersion, idempotencyKey: 'maintenance-key', vehicle: saved.vehicleInfo.vehicle, driver: saved.vehicleInfo.driver, maintenance, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', expect.objectContaining({ maintenance, vehicle: expect.objectContaining({ evidenceFileIds: ['9007199254740993'] }) })) expect(vi.mocked(http.put).mock.calls[0]![1]).not.toHaveProperty('departedAt') }) @@ -120,7 +120,7 @@ describe('native whole-vehicle dispatch API', () => { expect(http.post).not.toHaveBeenCalled(); await invoke(0); expect(vi.mocked(http.post).mock.calls[0]![1]).toEqual(expect.objectContaining({ recordVersion: 0 })) }) it('updates vehicle resources and timing with the explicit dispatch version', async () => { - const request: UpdateDispatch = { publishLooseboardDemand: true, ...body, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null } + const request: UpdateDispatch = { ...body, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null } await api.updateDispatch('PC/1', request); expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', request) await expect(api.updateDispatch('PC/1', { ...request, recordVersion: undefined as unknown as Version })).rejects.toThrow('版本') }) diff --git a/src/modules/overseas-warehouse/components/DispatchAppointments.vue b/src/modules/overseas-warehouse/components/DispatchAppointments.vue index 60f39b88..a2e605e3 100644 --- a/src/modules/overseas-warehouse/components/DispatchAppointments.vue +++ b/src/modules/overseas-warehouse/components/DispatchAppointments.vue @@ -1,11 +1,11 @@ diff --git a/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue b/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue index 62f4f7bf..0aaf092e 100644 --- a/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue +++ b/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue @@ -11,7 +11,6 @@ const driver = defineModel('driver', { required: true }) const maintenance = defineModel('maintenance', { required: true }) const plannedDepartureAt = defineModel('plannedDepartureAt', { required: true }) const estimatedArrivalAt = defineModel('estimatedArrivalAt', { required: true }) -const publishLooseboardDemand = defineModel('publishLooseboardDemand', { required: true }) const tab = ref('info') const deliveryMethods = ['FBA自提', '客户自提', '卡派打托', '卡派地板', '快递', '外配'] const pickupMethods = ['一提一卸', '一提多卸'] @@ -49,7 +48,6 @@ function confirmedTime(appointment: DispatchDestinationAppointment) { - 发布公共散板需求
目的仓预约号确认送达时间货件号
{{ appointmentFacts.get(appointment.destinationKey)?.destinationWarehouseCode || '—' }}{{ confirmedTime(appointment) }}{{ appointmentFacts.get(appointment.destinationKey)?.shipmentNumbers?.join('、') || '—' }}
diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts index a6344596..b8b9e9ca 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts @@ -161,7 +161,7 @@ describe('dispatch workflow native layout', () => { await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() dialog.vm.$emit('confirm'); await flushPromises() const { vehicleStatus: _status, ...vehicle } = resources().vehicle - expect(api.updateDispatch).toHaveBeenCalledExactlyOnceWith('PC/1', { recordVersion: 0, publishLooseboardDemand: true, idempotencyKey: expect.any(String), vehicle: { ...vehicle, plate: 'CA-NEW', capacity: { ...vehicle.capacity, palletSpaces: 22 } }, driver: { ...resources().driver, name: '新司机' }, maintenance: { plannedDeliveryDate: '2026-11-02', deliveryMethod: '外配', pickupMethod: '一提多卸', loadType: '混装', fleetEmail: 'fleet@example.test', vehicleType: '厢式货车', appointmentAccount: 'fleet-account', destinationAppointments: [] }, plannedDepartureAt: '2026-11-01T10:00:00Z', estimatedArrivalAt: null, remark: '真实备注' }) + expect(api.updateDispatch).toHaveBeenCalledExactlyOnceWith('PC/1', { recordVersion: 0, idempotencyKey: expect.any(String), vehicle: { ...vehicle, plate: 'CA-NEW', capacity: { ...vehicle.capacity, palletSpaces: 22 } }, driver: { ...resources().driver, name: '新司机' }, maintenance: { plannedDeliveryDate: '2026-11-02', deliveryMethod: '外配', pickupMethod: '一提多卸', loadType: '混装', fleetEmail: 'fleet@example.test', vehicleType: '厢式货车', appointmentAccount: 'fleet-account', destinationAppointments: [] }, plannedDepartureAt: '2026-11-01T10:00:00Z', estimatedArrivalAt: null, remark: '真实备注' }) }) it('saves all three resource photos before submitting review with the refreshed version', async () => { const { dialog } = await render('EDIT') diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts index 6820938b..89433e9a 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts @@ -12,7 +12,7 @@ vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) }) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('./DispatchResourceFields.vue', () => ({ default: { template: '
' } })) vi.mock('../api/dispatches', () => ({ markDispatchException: vi.fn(), getDispatch: vi.fn(), getDispatchItems: vi.fn(), getDispatchRoute: vi.fn(), reorderDispatchRoute: vi.fn(), removeDispatchItem: vi.fn(), getDispatchLoading: vi.fn(), getDispatchCommand: vi.fn(), listDispatches: vi.fn(), listDispatchCandidates: vi.fn(), completeDispatchLoading: vi.fn(), reviewDispatch: vi.fn(), reviewDispatchLoading: vi.fn(), returnDispatchToReview: vi.fn(), cancelDispatch: vi.fn(), updateDispatch: vi.fn(), addDispatchCargo: vi.fn(), submitDispatchReview: vi.fn() })) -interface State { exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(submitReviewAfterSave?: boolean): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string; publishLooseboardDemand: boolean }; detail: DispatchDetail } +interface State { exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(submitReviewAfterSave?: boolean): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string }; detail: DispatchDetail } async function open(action: DispatchAction, allowed = true) { vi.mocked(api.getDispatch).mockResolvedValue(detail({ actions: { [action]: { allowed, blockCode: null, blockReason: allowed ? null : '前置审核未通过' } } })) const wrapper = mount(DispatchWorkflowDialog, { props: { modelValue: true, dispatchNo: 'PC/1', action }, global: { plugins: [ElementPlus] } }); await flushPromises() @@ -260,14 +260,13 @@ it('allows a new save-and-review click after review submission fails',async()=>{ expect(api.getDispatchCommand).not.toHaveBeenCalled();wrapper.unmount() }) -it('persists the explicit publication choice with the existing vehicle save', async () => { +it('keeps publication fixed when saving vehicle maintenance', async () => { const { wrapper, state } = await open('EDIT') - expect(state.form.publishLooseboardDemand).toBe(true) - expect(wrapper.getComponent({ name: 'ElDialogContent' }).text()).toContain('发布公共散板需求') - state.form.publishLooseboardDemand = false + expect(wrapper.getComponent({ name: 'ElDialogContent' }).text()).not.toContain('发布公共散板需求') vi.mocked(api.updateDispatch).mockImplementation(async (_, body) => command('EDIT', body.idempotencyKey)) await state.submit() - expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({ recordVersion: 0, publishLooseboardDemand: false })) + expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({ recordVersion: 0 })) + expect(vi.mocked(api.updateDispatch).mock.calls[0]![1]).not.toHaveProperty('publishLooseboardDemand') wrapper.unmount() }) diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue index a9c55cb0..093ebb9f 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue @@ -53,7 +53,7 @@ const isReview = computed(() => ['DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(c const approvalBlock = computed(() => isReview.value && detail.value?.actions[currentAction.value]?.blockCode === 'APPROVAL_BLOCKED' ? detail.value.actions[currentAction.value]?.blockReason : null) const reviewTime = (value: string | null | undefined) => value ? new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', dateStyle: 'short', timeStyle: 'short', hour12: false }).format(new Date(value)) : '未填写' const exception = reactive({ exceptionType: '' as DispatchExceptionType | '', returnDirection: '' as DispatchReturnDirection | '', description: '', imageFileIds: [] as string[] }) -const form = reactive({ publishLooseboardDemand: true, plannedDepartureAt: '', estimatedArrivalAt: '', maintenance: blankVehicleMaintenance(), remark: '', decision: 'APPROVED' as 'APPROVED' | 'REJECTED', reason: '' }) +const form = reactive({ plannedDepartureAt: '', estimatedArrivalAt: '', maintenance: blankVehicleMaintenance(), remark: '', decision: 'APPROVED' as 'APPROVED' | 'REJECTED', reason: '' }) /** 并发加货后补入新目的仓,保留客服已填写的同仓编号。 */ function fillDestinationAppointments(value: DispatchDetail, keepInput = false) { const entered = new Map(keepInput ? form.maintenance.destinationAppointments.map(row => [row.destinationKey, row.appointmentNo]) : []) @@ -90,7 +90,7 @@ async function load() { vehicle.value = oldVehicle ? { mode: oldVehicle.mode, vehicleId: oldVehicle.vehicleId, plate: oldVehicle.plate, carrierName: oldVehicle.carrierName, capacity: oldVehicle.capacity ? { ...oldVehicle.capacity } : null, evidenceFileIds: [...oldVehicle.evidenceFileIds] } : blankVehicle() driver.value = value.vehicleInfo.driver ? structuredClone(value.vehicleInfo.driver) : blankDriver() if (!driver.value.evidence) driver.value.evidence = blankDriver().evidence - Object.assign(form, { publishLooseboardDemand: value.publishLooseboardDemand, plannedDepartureAt: value.dispatchInfo.plannedDepartureAt || '', estimatedArrivalAt: value.dispatchInfo.estimatedArrivalAt || '', remark: value.documents.remark || '', reason: '', decision: 'APPROVED' }) + Object.assign(form, { plannedDepartureAt: value.dispatchInfo.plannedDepartureAt || '', estimatedArrivalAt: value.dispatchInfo.estimatedArrivalAt || '', remark: value.documents.remark || '', reason: '', decision: 'APPROVED' }) if (currentOperation.value === 'ROUTE') { const data = await api.getDispatchRoute(no) if (sequence !== generation) return @@ -125,7 +125,7 @@ function payload(): object { case 'MARK_EXCEPTION': if (!exception.exceptionType) throw new DispatchInputError('请选择异常类型') return { ...common, exceptionType: exception.exceptionType, returnDirection: exception.returnDirection || null, description: exception.description.trim() || null, imageFileIds: [...exception.imageFileIds] } satisfies MarkDispatchException - case 'EDIT': return { ...common, publishLooseboardDemand: form.publishLooseboardDemand, vehicle: vehicle.value.mode === 'MASTER' && !vehicle.value.vehicleId ? null : vehicle.value, driver: driver.value.mode === 'MASTER' && !driver.value.driverId ? null : driver.value, maintenance: vehicleMaintenancePayload(form.maintenance), plannedDepartureAt: form.plannedDepartureAt || null, estimatedArrivalAt: form.estimatedArrivalAt || null, remark: form.remark || null } satisfies UpdateDispatch + case 'EDIT': return { ...common, vehicle: vehicle.value.mode === 'MASTER' && !vehicle.value.vehicleId ? null : vehicle.value, driver: driver.value.mode === 'MASTER' && !driver.value.driverId ? null : driver.value, maintenance: vehicleMaintenancePayload(form.maintenance), plannedDepartureAt: form.plannedDepartureAt || null, estimatedArrivalAt: form.estimatedArrivalAt || null, remark: form.remark || null } satisfies UpdateDispatch case 'DISPATCH_REVIEW': case 'LOADING_REVIEW': if (form.decision === 'APPROVED' && approvalBlock.value) throw new DispatchInputError(approvalBlock.value) if (form.decision === 'REJECTED' && !form.reason.trim()) throw new DispatchInputError('驳回时必须填写原因') @@ -246,7 +246,7 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],