diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 0d7e2404..e2ca48b7 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -45,7 +45,10 @@ export interface DispatchCandidateQuery { orderNo?: string fbaWarehouseCode?: string sourceStage?: 'PLAN' | 'RECEIVED' + draftToken?: string } +export interface DispatchDraftContainerDto { id: string; capacity: number; palletIds: string[] } +export interface DispatchDraftDto { draftToken: string; containers?: DispatchDraftContainerDto[] } export interface PreArrangeDispatchPayload { planPalletIds: string[] @@ -107,13 +110,26 @@ export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) { return unwrapApiResponse(response) } -export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string) { +export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string, draftContainerId?: string) { const response = await overseasHttp.post, ApiEnvelope>( - '/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken }, + '/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken, ...(draftContainerId ? { draftContainerId } : {}) }, ) return unwrapApiResponse(response) } +export async function listDispatchDrafts() { + const response = await overseasHttp.get, ApiEnvelope>('/platform/v1/last-leg/dispatches/drafts') + return unwrapApiResponse(response) +} +export async function createDispatchDraftContainer(draftToken: string, capacity: number) { + const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken, capacity }) + return unwrapApiResponse(response) +} +export async function deleteDispatchDraftContainer(draftToken: string, containerId: string) { + const response = await overseasHttp.delete, ApiEnvelope>(`/platform/v1/last-leg/dispatches/drafts/${encodeURIComponent(draftToken)}/containers/${encodeURIComponent(containerId)}`) + return unwrapApiResponse(response) +} + export async function unlockDispatchDraftPallet(planPalletId: string, draftToken: string) { const response = await overseasHttp.post, ApiEnvelope>( '/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId, draftToken }, diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index 1622bd6d..0cd243a2 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -8,7 +8,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 { listDispatchCandidates, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' +import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' import type { InboundOrderListItem } from '../types' interface LoadingContainer { id: string; capacity: number; palletIds: string[] } @@ -21,6 +21,7 @@ const saving = ref(false) const loadError = ref(null) const mutationError = ref(null) const candidates = ref([]) +const palletSnapshots = ref>({}) const activeOrderKey = ref('') const containers = ref([]) const busyPalletIds = ref([]) @@ -57,7 +58,7 @@ const orderGroups = computed(() => { }) const activePallets = computed(() => orderGroups.value.find((item) => item.key === activeOrderKey.value)?.pallets || []) const assignedIds = computed(() => containers.value.flatMap((item) => item.palletIds)) -const assigned = computed(() => assignedIds.value.map((id) => candidates.value.find((item) => palletId(item) === id)).filter((item): item is DispatchPalletCandidateDto => Boolean(item))) +const assigned = computed(() => assignedIds.value.map((id) => palletSnapshot(id)).filter((item): item is DispatchPalletCandidateDto => Boolean(item))) const assignedPackages = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0)) const assignedVolume = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0)) @@ -68,16 +69,25 @@ function dateTime(value: unknown) { const raw = String(value || '').trim(); retu function orderType(value: unknown) { return ({ FIRST_LEG: '头程订单', RELABEL_APPLICATION: '换标订单', RETURN_RELABEL: '退换标单', LOCAL_LOOSEBOARD: '本土散板', WAREHOUSE_TRANSFER: '仓储转运单', ONE_CLICK_DROPSHIP: '一件代发', REJECTED: '拒收订单', EXPRESS: '快递订单' } as Record)[String(value || '')] || text(value) } function cartonMarks(item: DispatchPalletCandidateDto) { return String(item.cartonMarks || '').split(',').map((value) => value.trim()).filter(Boolean) } function containerFor(id: string) { return containers.value.find((item) => item.palletIds.includes(id)) } +function palletSnapshot(id: string) { return palletSnapshots.value[id] } function makeDraftToken() { return globalThis.crypto?.randomUUID?.() || `dispatch-${Date.now()}-${Math.random().toString(16).slice(2)}` } function ensureDraftToken() { if (!draftToken.value.trim()) draftToken.value = makeDraftToken() return draftToken.value } +function rememberPallets(items: DispatchPalletCandidateDto[]) { + palletSnapshots.value = { ...palletSnapshots.value, ...Object.fromEntries(items.map((item) => [palletId(item), item])) } +} +async function hydrateDraftPallets() { + const restored = await listDispatchCandidates({ draftToken: ensureDraftToken() }) + rememberPallets(restored) +} async function load() { loading.value = true; loadError.value = null try { - candidates.value = await listDispatchCandidates({ planIds: planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined }) + candidates.value = await listDispatchCandidates({ planIds: planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined, draftToken: ensureDraftToken() }) + rememberPallets(candidates.value) activeOrderKey.value = orderGroups.value[0]?.key || '' } catch (error) { candidates.value = []; activeOrderKey.value = '' @@ -92,7 +102,8 @@ async function addContainer() { confirmButtonText: '确定', cancelButtonText: '取消', inputValue: '20', inputType: 'number', inputValidator: (value) => Number.isInteger(Number(value)) && Number(value) > 0 && Number(value) <= 500 ? true : '请输入 1 至 500 的整数', }) - containers.value.push({ id: `${Date.now()}-${containers.value.length + 1}`, capacity: Number(result.value), palletIds: [] }) + const created = await createDispatchDraftContainer(ensureDraftToken(), Number(result.value)) + containers.value.push({ id: created.id, capacity: created.capacity, palletIds: created.palletIds || [] }) } catch { /* user cancelled */ } } @@ -106,13 +117,12 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string) if (anchor?.customerId && anchor.customerId !== item.customerId) { ElMessage.warning('一次排车只能装同一客户的托盘'); return } if (anchor?.overseasWarehouseId && anchor.overseasWarehouseId !== item.overseasWarehouseId) { ElMessage.warning('一次排车只能装同一海外仓的托盘'); return } busyPalletIds.value.push(id); mutationError.value = null - try { await lockDispatchDraftPallet(id, ensureDraftToken()); target.palletIds.push(id) } + try { await lockDispatchDraftPallet(id, ensureDraftToken(), target.id); target.palletIds.push(id) } catch (error) { mutationError.value = getApiErrorDetails(error, '托盘锁定失败,请刷新后重试') } finally { busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) } } -async function removePallet(item: DispatchPalletCandidateDto) { - const id = palletId(item) +async function removePallet(id: string) { const owner = containerFor(id) if (!owner) return busyPalletIds.value.push(id); mutationError.value = null @@ -123,10 +133,9 @@ async function removePallet(item: DispatchPalletCandidateDto) { async function deleteContainer(container: LoadingContainer) { for (const id of [...container.palletIds]) { - const item = candidates.value.find((candidate) => palletId(candidate) === id) - if (item) await removePallet(item) + await removePallet(id) } - if (!container.palletIds.length) containers.value = containers.value.filter((item) => item.id !== container.id) + if (!container.palletIds.length) { await deleteDispatchDraftContainer(ensureDraftToken(), container.id); containers.value = containers.value.filter((item) => item.id !== container.id) } } function dragStart(event: DragEvent, item: DispatchPalletCandidateDto) { event.dataTransfer?.setData('text/plain', palletId(item)) } function dropInto(event: DragEvent, containerId: string) { @@ -156,9 +165,9 @@ async function submit() { watch(() => props.modelValue, (open) => { if (!open) return - draftToken.value = makeDraftToken(); finalized.value = false; containers.value = [] + draftToken.value = makeDraftToken(); finalized.value = false; containers.value = []; palletSnapshots.value = {} query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; mutationError.value = null - void load() + void (async () => { const drafts = await listDispatchDrafts().catch(() => []); const draft = drafts[0]; if (draft?.draftToken) { draftToken.value = draft.draftToken; containers.value = (draft.containers || []).map((c) => ({ id: c.id, capacity: c.capacity, palletIds: c.palletIds || [] })); await hydrateDraftPallets().catch(() => undefined) } await load() })() }, { immediate: true }) watch([warehouseTab, () => query.orderType], () => { @@ -222,7 +231,7 @@ watch([warehouseTab, () => query.orderType], () => {
装车托盘 {{ index + 1 }}托数 {{ container.palletIds.length }}/{{ container.capacity }}
-
{{ text(candidates.find((item) => palletId(item) === id)?.palletNo) }}{{ text(candidates.find((item) => palletId(item) === id)?.orderNo) }}移除
+
{{ text(palletSnapshot(id)?.palletNo || id) }}{{ text(palletSnapshot(id)?.orderNo) }}移除
拖入托盘,移入后立即锁定
diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 9d2f4517..408cabf2 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -227,7 +227,10 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(dialog).toContain("sourceStage: 'RECEIVED'") expect(dialog).toContain('一次排车只能装同一客户的托盘') expect(dialog).toContain('function ensureDraftToken()') - expect(dialog).toContain('lockDispatchDraftPallet(id, ensureDraftToken())') + expect(dialog).toContain('lockDispatchDraftPallet(id, ensureDraftToken(), target.id)') + expect(dialog).toContain('function hydrateDraftPallets()') + expect(dialog).toContain('@click="removePallet(id)"') + expect(dialog).not.toContain('removePallet(candidates.find') expect(dialog).toContain('洛杉矶') expect(dialog).toContain('纽约') expect(dialog).toContain("value: 'ALL', label: '全部'")