From 28eb43893ae17c4242ac1d4c3496d04782173bf7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 5 Oct 2026 03:30:06 +0800 Subject: [PATCH] fix(dispatch): submit and preserve independent loading groups --- .../overseas-warehouse/api/dispatches.test.ts | 19 +- .../overseas-warehouse/api/dispatches.ts | 21 +++ .../components/SmartDispatchDialog.test.ts | 174 +++++++++++++++++- .../components/SmartDispatchDialog.vue | 66 ++++++- 4 files changed, 267 insertions(+), 13 deletions(-) diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts index 711a70e2..1affac69 100644 --- a/src/modules/overseas-warehouse/api/dispatches.test.ts +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -1,10 +1,27 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches' +import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches' vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } })) describe('dispatch overseas API', () => { + it('submits all explicit draft groups once and preserves container and source identities', async () => { + const payload = { draftToken: 'original-draft', groups: [ + { draftContainerId: '190000000000000099', planPalletIds: ['11', '13'], returnReceiptIds: [] }, + { draftContainerId: '190000000000000100', planPalletIds: ['12'], returnReceiptIds: ['22'] }, + ] } + const result = payload.groups.map((group, index) => ({ draftContainerId: group.draftContainerId, dispatch: { id: String(index), dispatchNo: `DS-${index}` } })) + vi.mocked(http.post).mockResolvedValue({ code: '200', data: result }) + expect(await preArrangeDraftDispatch(payload)).toEqual(result) + expect(http.post).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/dispatches/drafts/pre-arrange', payload) + }) + it('propagates a batch rejection without falling back to a flat or repeated command', async () => { + vi.mocked(http.post).mockResolvedValue({ code: 'A0409', message: '草稿来源已变化', data: null }) + await expect(preArrangeDraftDispatch({ draftToken: 'original-draft', groups: [ + { draftContainerId: 'container', planPalletIds: ['11'], returnReceiptIds: [] }, + ] })).rejects.toThrow('草稿来源已变化') + expect(http.post).toHaveBeenCalledTimes(1) + }) it('locks and submits actual return receipts without pretending they are plan pallets', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: null }) await lockDispatchDraftPallet('RETURN:190000000000000099', 'draft-return', 'container-1') diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 4d27f0a4..9c272b87 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -71,6 +71,20 @@ export interface PreArrangeDispatchPayload { relabelNo?: string } +export interface PreArrangeDispatchDraftGroup { + draftContainerId: string + planPalletIds: string[] + returnReceiptIds: string[] +} +export interface PreArrangeDispatchDraftPayload { + draftToken: string + groups: PreArrangeDispatchDraftGroup[] +} +export interface PreArrangeDispatchDraftResult { + draftContainerId: string + dispatch: DispatchSummaryDto +} + export interface DispatchSummaryDto { id: string dispatchNo: string @@ -202,6 +216,13 @@ export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) { return unwrapApiResponse(response) } +export async function preArrangeDraftDispatch(payload: PreArrangeDispatchDraftPayload) { + const response = await http.post, ApiEnvelope>( + '/platform/v1/last-leg/dispatches/drafts/pre-arrange', payload, + ) + return unwrapApiResponse(response) +} + function draftSource(selection: string) { return selection.startsWith('RETURN:') ? { sourceType: 'RETURN_RECEIPT', returnReceiptId: selection.slice(7) } diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.test.ts b/src/modules/overseas-warehouse/components/SmartDispatchDialog.test.ts index e151d12e..9691e345 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.test.ts +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.test.ts @@ -3,12 +3,12 @@ import { defineComponent, h } from 'vue' import { beforeEach, expect, it, vi } from 'vitest' import { ElMessage } from 'element-plus' import SmartDispatchDialog from './SmartDispatchDialog.vue' -import { listDispatchCandidates, listDispatchDrafts, preArrangeDispatch, type DispatchPalletCandidateDto } from '../api/dispatches' +import { listDispatchCandidates, listDispatchDrafts, preArrangeDispatch, preArrangeDraftDispatch, unlockDispatchDraftPallet, deleteDispatchDraftContainer, type DispatchPalletCandidateDto, type DispatchDraftDto } from '../api/dispatches' vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn(), error: vi.fn() } })) vi.mock('../api/dispatches', () => ({ - listDispatchCandidates: vi.fn(), listDispatchDrafts: vi.fn(), preArrangeDispatch: vi.fn(), + listDispatchCandidates: vi.fn(), listDispatchDrafts: vi.fn(), preArrangeDispatch: vi.fn(), preArrangeDraftDispatch: vi.fn(), createDispatchDraftContainer: vi.fn(), deleteDispatchDraftContainer: vi.fn(), lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(), })) @@ -27,7 +27,173 @@ const returned: DispatchPalletCandidateDto = { orderId: '2', orderNo: 'LM-B', re beforeEach(() => vi.resetAllMocks()) +function mountDraft() { + return shallowMount(SmartDispatchDialog, { props: { modelValue: true, orders: [] }, global: { + stubs: { AppDialog: shell, ElButton: button, LoadErrorState: retry, ElDialog: true, + ElInput: true, ElOption: true, ElSelect: true, ElTable: true, ElTableColumn: true, + ElTag: true, ElDropdown: true, ElDropdownMenu: true, ElDropdownItem: true, + ElEmpty: true, ElRadio: true, ElRadioGroup: true }, + } }) +} +const secondPlan = { ...plan, orderId: '3', orderNo: 'LM-C', planPalletId: '13', palletNo: 'PAL-C' } +const thirdPlan = { ...plan, orderId: '4', orderNo: 'LM-D', planPalletId: '14', palletNo: 'PAL-D' } +const twoGroups: DispatchDraftDto = { draftToken: 'original-draft', containers: [ + { id: 'container-1', capacity: 30, palletIds: ['11', '13'] }, + { id: 'container-2', capacity: 30, palletIds: ['14', 'RETURN:22'] }, + { id: 'empty-container', capacity: 15, palletIds: [] }, +] } +const expectedBatch = { draftToken: 'original-draft', groups: [ + { draftContainerId: 'container-1', planPalletIds: ['11', '13'], returnReceiptIds: [] }, + { draftContainerId: 'container-2', planPalletIds: ['14'], returnReceiptIds: ['22'] }, +] } +const goodResult = [{ draftContainerId: 'container-2', dispatch: { id: '102', dispatchNo: 'DS-2' } }, + { draftContainerId: 'container-1', dispatch: { id: '101', dispatchNo: 'DS-1' } }] +async function submitDraft(wrapper: ReturnType) { + await wrapper.findAll('button').find(node => node.text() === '预排车')!.trigger('click') + await flushPromises() +} + +it('keeps two explicit groups in one atomic request, omits empty containers and accepts reordered matching results', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned]) + vi.mocked(preArrangeDraftDispatch).mockResolvedValue(goodResult) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch) + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(wrapper.emitted('created')).toHaveLength(1) + expect(wrapper.emitted('update:modelValue')).toEqual([[false]]) + wrapper.unmount() +}) + +it.each([ + ['one collapsed result', goodResult.slice(0, 1)], + ['foreign container', [{ ...goodResult[0], draftContainerId: 'foreign' }, goodResult[1]]], + ['duplicate container', [goodResult[0], goodResult[0]]], + ['duplicate dispatch', [goodResult[0], { ...goodResult[1], dispatch: goodResult[0].dispatch }]], + ['missing business number', [goodResult[0], { ...goodResult[1], dispatch: { id: '101', dispatchNo: '' } }]], +])('retains the original groups and token after %s', async (_label, result) => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned]) + vi.mocked(preArrangeDraftDispatch).mockResolvedValue(result) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(wrapper.emitted('created')).toBeUndefined() + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + expect(wrapper.text()).toContain('托盘 4 · 箱数 8') + await submitDraft(wrapper) + expect(preArrangeDraftDispatch).toHaveBeenNthCalledWith(2, expectedBatch) + expect(preArrangeDispatch).not.toHaveBeenCalled() + wrapper.unmount() +}) + +it('retains all groups on batch rejection without issuing a flat request', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned]) + vi.mocked(preArrangeDraftDispatch).mockRejectedValue(new Error('草稿来源已变化')) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1) + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(wrapper.emitted('created')).toBeUndefined() + expect(wrapper.text()).toContain('托盘 4 · 箱数 8') + wrapper.unmount() +}) + +it('does not submit a physical pallet fallback when its actual plan source identity is missing', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [ + { id: 'container', capacity: 30, palletIds: ['101'] }, + ] }]) + vi.mocked(listDispatchCandidates).mockResolvedValue([{ ...plan, planPalletId: undefined }]) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('来源标识缺失')) + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() +}) + +it('does not send a second batch while the original command is pending', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned]) + let resolveCommand!: (result: typeof goodResult) => void + vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve })) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch) + expect(wrapper.emitted('created')).toBeUndefined() + resolveCommand(goodResult); await flushPromises() + expect(wrapper.emitted('created')).toHaveLength(1) + wrapper.unmount() +}) + +it('keeps group membership frozen while the submitted batch is awaiting its response', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned]) + let resolveCommand!: (result: typeof goodResult) => void + vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve })) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + await wrapper.findAll('button').find(node => node.text() === '移除')!.trigger('click') + await wrapper.findAll('button').find(node => node.attributes('aria-label') === '删除装车车型1')!.trigger('click') + await flushPromises() + expect(unlockDispatchDraftPallet).not.toHaveBeenCalled() + expect(deleteDispatchDraftContainer).not.toHaveBeenCalled() + expect(wrapper.text()).toContain('托盘 4 · 箱数 8') + resolveCommand(goodResult); await flushPromises() + expect(wrapper.emitted('created')).toHaveLength(1) + wrapper.unmount() +}) + +it('retains unresolved sources when retry hydration still cannot recover the complete original draft', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, returned]) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留')) + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() +}) + +it.each([ + { ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c2', capacity: 30, palletIds: ['11'] }] }, + { ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c1', capacity: 30, palletIds: ['13'] }] }, + { ...twoGroups, containers: [{ id: '', capacity: 30, palletIds: ['11'] }] }, +])('rejects duplicate sources or invalid container identities before writing', async draft => { + vi.mocked(listDispatchDrafts).mockResolvedValue([draft]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan]) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() +}) + +it('rejects a relabel order split across multiple containers before either API command', async () => { + const relabel: DispatchPalletCandidateDto = { ...plan, sourceStage: 'RELABEL', orderRefs: 'RL-1' } + vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [ + { id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c2', capacity: 30, palletIds: ['13'] }, + ] }]) + vi.mocked(listDispatchCandidates).mockResolvedValue([relabel, { ...secondPlan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }]) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() + expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('一组排车')) + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() +}) + +it('keeps one complete relabel order on the single-dispatch endpoint', async () => { + vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [ + { id: 'c1', capacity: 30, palletIds: ['11', '13'] }, + ] }]) + vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan].map(p => ({ ...p, sourceStage: 'RELABEL', orderRefs: 'RL-1' }))) + vi.mocked(preArrangeDispatch).mockResolvedValue({ id: '1', dispatchNo: 'DS-RL' }) + const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper) + expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['11', '13'], returnReceiptIds: [], relabelNo: 'RL-1' }) + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() + expect(wrapper.emitted('created')).toHaveLength(1) + wrapper.unmount() +}) + it('retains every restored selection on candidate failure and submits both sources only after retry hydration', async () => { + vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container', dispatch: { id: '1', dispatchNo: 'DS-1' } }]) vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [{ id: 'container', capacity: 30, palletIds: ['11', 'RETURN:22'] }] }]) vi.mocked(listDispatchCandidates) .mockResolvedValueOnce([plan]) @@ -45,13 +211,15 @@ it('retains every restored selection on candidate failure and submits both sourc await submit().trigger('click') await flushPromises() expect(preArrangeDispatch).not.toHaveBeenCalled() + expect(preArrangeDraftDispatch).not.toHaveBeenCalled() expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留')) expect(wrapper.emitted('update:modelValue')).toBeUndefined() await wrapper.get('[data-test="retry"]').trigger('click') await flushPromises() await submit().trigger('click') await flushPromises() - expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['11'], returnReceiptIds: ['22'], draftToken: 'original-draft' }) + expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith({ draftToken: 'original-draft', groups: [{ draftContainerId: 'container', planPalletIds: ['11'], returnReceiptIds: ['22'] }] }) + expect(preArrangeDispatch).not.toHaveBeenCalled() expect(listDispatchCandidates).toHaveBeenLastCalledWith({ draftToken: 'original-draft' }) expect(wrapper.emitted('created')).toHaveLength(1) wrapper.unmount() diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index 74f393a8..85488af2 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -10,7 +10,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' -import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' +import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' import { formatOverseasDateTime } from '../lastMilePresentation' import type { InboundOrderListItem } from '../types' import { dispatchVehicleSpecs } from '../dispatchVehicleSpecs' @@ -198,10 +198,12 @@ function reset() { } function addContainer() { + if (saving.value) return selectedVehicle.value = '' vehicleDialogOpen.value = true } async function confirmAddContainer() { + if (saving.value) return const spec = selectedVehicleSpec.value if (!spec) { ElMessage.warning('请选择车型') @@ -224,6 +226,7 @@ async function confirmAddContainer() { async function movePallet(item: DispatchPalletCandidateDto, containerId: string) { const id = palletId(item) + if (saving.value || busyPalletIds.value.includes(id)) return const target = containers.value.find((container) => container.id === containerId) if (!target || target.palletIds.includes(id)) return if (!String(item.overseasWarehouseId || '').trim()) { @@ -267,6 +270,7 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string) } async function removePallet(id: string) { + if (saving.value || busyPalletIds.value.includes(id)) return const owner = containerFor(id) if (!owner) return busyPalletIds.value.push(id) @@ -282,6 +286,7 @@ async function removePallet(id: string) { } async function deleteContainer(container: LoadingContainer) { + if (saving.value) return for (const id of [...container.palletIds]) { await removePallet(id) } @@ -300,10 +305,16 @@ function dropInto(event: DragEvent, containerId: string) { } async function close(value = false) { + if (saving.value) return emit('update:modelValue', value) } async function submit() { + if (saving.value) return + if (busyPalletIds.value.length) { + ElMessage.warning('托盘来源正在锁定,请稍后再预排车') + return + } if (loading.value || unresolvedAssignedIds.value.length) { ElMessage.warning('草稿中部分已选来源尚未恢复,请重试加载后再预排;原选择已保留') return @@ -312,6 +323,18 @@ async function submit() { ElMessage.warning('请至少移入一个托盘') return } + const selectedContainers = containers.value.filter(container => container.palletIds.length > 0) + if (new Set(assignedIds.value).size !== assignedIds.value.length + || selectedContainers.some(container => !container.id.trim()) + || new Set(selectedContainers.map(container => container.id)).size !== selectedContainers.length) { + ElMessage.warning('草稿中存在重复托盘或无效装车容器,请核对分组;原草稿已保留') + return + } + if (assigned.value.some(item => item.sourceType === 'RETURN_RECEIPT' + ? !String(item.returnReceiptId || '').trim() : !String(item.planPalletId || '').trim())) { + ElMessage.warning('所选托盘来源标识缺失,请重新加载核对;原草稿已保留') + return + } if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) { ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘') return @@ -326,14 +349,39 @@ async function submit() { ElMessage.warning('一张换标单的托盘需要单独排车') return } + if (relabelOrders.length && selectedContainers.length !== 1) { + ElMessage.warning('一张换标单仅支持一组排车,请将全部托盘移入同一个装车容器') + return + } saving.value = true mutationError.value = null try { - await preArrangeDispatch({ - planPalletIds: assigned.value.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(palletId), - returnReceiptIds: assigned.value.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)), - ...(relabelOrders.length ? { relabelNo: relabelOrders[0] } : { draftToken: ensureDraftToken() }), - }) + if (relabelOrders.length) { + await preArrangeDispatch({ + planPalletIds: assigned.value.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(palletId), + returnReceiptIds: assigned.value.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)), + relabelNo: relabelOrders[0], + }) + } else { + const groups = selectedContainers.map(container => { + const sources = container.palletIds.map(id => palletSnapshot(id)!) + return { + draftContainerId: container.id, + planPalletIds: sources.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(item => String(item.planPalletId)), + returnReceiptIds: sources.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)), + } + }) + const results = await preArrangeDraftDispatch({ draftToken: ensureDraftToken(), groups }) + const expectedIds = new Set(groups.map(group => group.draftContainerId)) + if (!Array.isArray(results) || results.length !== groups.length + || results.some(result => !result || typeof result.draftContainerId !== 'string' + || typeof result.dispatch?.dispatchNo !== 'string' || !result.dispatch.dispatchNo.trim()) + || new Set(results.map(result => result.draftContainerId)).size !== groups.length + || results.some(result => !expectedIds.has(result.draftContainerId)) + || new Set(results.map(result => result.dispatch.dispatchNo)).size !== groups.length) { + throw new Error('预排车返回的分组结果与原草稿不一致,原草稿已保留,请核对保存结果') + } + } emit('update:modelValue', false) emit('created') ElMessage.success('排车明细已同步到预排列表') @@ -541,14 +589,14 @@ watch([warehouseTab, () => query.orderType], () => { >托数 {{ container.palletIds.length }}/{{ container.capacity }} · 重量 {{ formatWeightLb(containerWeightLb(container)) }}/{{ container.maxWeightLb }}LB 超限10%删除 + >删除
{{ text(palletSnapshot(id)?.palletNo || '托盘号待补充') }}移除 + >移除
拖入托盘,移入后立即锁定
@@ -560,7 +608,7 @@ watch([warehouseTab, () => query.orderType], () => {