From 0830d7a3a369bd8ea60b0462d16d4be58a80bea0 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 14 Oct 2026 11:43:32 +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 --- packing-acceptance.local.html | 16 ++++++++ .../api/cargoDiscards.test.ts | 15 +++++++ .../overseas-warehouse/api/cargoDiscards.ts | 7 +++- .../overseas-warehouse/api/lastMileOrders.ts | 2 + .../overseas-warehouse/api/relabelOrders.ts | 4 ++ .../pages/CargoDiscardsView.vue | 28 ++++++++++--- .../pages/LastMileOrderDetailView.vue | 22 +++++++++++ .../pages/OutboundsView.selection.test.ts | 39 ++++++++++++++++++- .../pages/OutboundsView.vue | 17 +++++++- .../pages/ReturnInventoryManagementView.vue | 39 +++++++++++++++++-- 10 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 packing-acceptance.local.html create mode 100644 src/modules/overseas-warehouse/api/cargoDiscards.test.ts diff --git a/packing-acceptance.local.html b/packing-acceptance.local.html new file mode 100644 index 00000000..cbb4fece --- /dev/null +++ b/packing-acceptance.local.html @@ -0,0 +1,16 @@ +本地合箱接口验收 +

本地合箱接口验收

复用当前平台会话,只调用正式业务接口。

+ +

+
diff --git a/src/modules/overseas-warehouse/api/cargoDiscards.test.ts b/src/modules/overseas-warehouse/api/cargoDiscards.test.ts
new file mode 100644
index 00000000..2b1e73fb
--- /dev/null
+++ b/src/modules/overseas-warehouse/api/cargoDiscards.test.ts
@@ -0,0 +1,15 @@
+import { beforeEach, expect, it, vi } from 'vitest'
+import { http } from '@/shared/api/http'
+import { downloadCargoDiscardPhoto, getCargoDiscard } from './cargoDiscards'
+
+vi.mock('@/shared/api/http', async importOriginal => ({ ...(await importOriginal()), http: { get: vi.fn() } }))
+beforeEach(() => vi.mocked(http.get).mockReset())
+it('reads submitted photos from detail and downloads a blob in the explicit customer scope', async () => {
+  const photos = [{ fileId: '101', palletNo: 'PALLET-A', fileName: 'discard.png', createdAt: '2026-10-14T10:00:00', submittedAt: '2026-10-14T10:01:00' }]
+  vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { summary: { discardNo: 'CD/1', customerId: '7' }, photos, items: [], histories: [] } })
+  expect((await getCargoDiscard('CD/1', '7')).photos).toEqual(photos)
+  const blob = new Blob(['image'], { type: 'image/png' })
+  vi.mocked(http.get).mockResolvedValueOnce(blob)
+  expect(await downloadCargoDiscardPhoto('CD/1', '101', '7')).toBe(blob)
+  expect(http.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/cargo-discards/CD%2F1/photos/files/101/download', { params: { customerId: '7' }, responseType: 'blob' })
+})
diff --git a/src/modules/overseas-warehouse/api/cargoDiscards.ts b/src/modules/overseas-warehouse/api/cargoDiscards.ts
index 1883512d..1600a67f 100644
--- a/src/modules/overseas-warehouse/api/cargoDiscards.ts
+++ b/src/modules/overseas-warehouse/api/cargoDiscards.ts
@@ -4,7 +4,8 @@ import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrder
 export interface CargoDiscardQuery { page: number; size: number; customerId?: string; discardNo?: string; inboundCode?: string; overseasWarehouseId?: string; status?: string }
 export interface CargoDiscardSummary { id: string; customerId?: string | null; discardNo: string; inboundCodes?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null; totalPackages?: number | null; totalFee?: number | string | null; currency?: string | null; status?: string | null; createdBy?: string | null; createdAt?: string | null; reviewedAt?: string | null }
 export interface CargoDiscardItem { id: string; orderId?: string | null; inboundCode?: string | null; skuCode?: string | null; productName?: string | null; availablePackagesSnapshot?: number | null; discardPackages?: number | null }
-export interface CargoDiscardDetail { summary: CargoDiscardSummary; feePerPackage?: number | string | null; remark?: string | null; reviewedBy?: string | null; reviewRemark?: string | null; items: CargoDiscardItem[]; histories: { fromStatus?: string | null; toStatus?: string | null; operatorId?: string | null; remark?: string | null; createdAt?: string | null }[] }
+export interface CargoDiscardPhoto { fileId: string; palletNo: string; fileName: string; createdAt: string; submittedAt: string }
+export interface CargoDiscardDetail { photos: CargoDiscardPhoto[]; summary: CargoDiscardSummary; feePerPackage?: number | string | null; remark?: string | null; reviewedBy?: string | null; reviewRemark?: string | null; items: CargoDiscardItem[]; histories: { fromStatus?: string | null; toStatus?: string | null; operatorId?: string | null; remark?: string | null; createdAt?: string | null }[] }
 export interface CargoDiscardCandidate { orderId?: string | null; sourceType: string; returnCartonId?: string | null; cartonMark?: string | null; inboundCode?: string | null; inboundOrderNo?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null; availablePackages?: number; skus: { skuCode: string; productName?: string | null; availablePackages?: number }[] }
 
 export async function listCargoDiscards(query: CargoDiscardQuery) {
@@ -24,3 +25,7 @@ export async function completeCargoDiscard(discardNo: string, customerId?: strin
   return unwrapApiResponse(response)
 }
 
+
+export function downloadCargoDiscardPhoto(discardNo: string, fileId: string, customerId: string) {
+  return http.get(`/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download`, { params: { customerId }, responseType: 'blob' })
+}
diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts
index 096574a6..53c39ca1 100644
--- a/src/modules/overseas-warehouse/api/lastMileOrders.ts
+++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts
@@ -360,6 +360,8 @@ export interface LastMileOrderDetailDto {
     items?: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number }>
     receipts: Array<{ receiptNo: string; unitType: string; unitCode?: string; palletCode?: string; receivedQuantity: number; boxCount: number; signedAt: string; length?: number | null; width?: number | null; height?: number | null; weight?: number | null }>
     cartons?: Array<{ receiptNo: string; palletCode?: string; cartonMark: string; countStatus: string; countedAt?: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }>; length?: number | null; width?: number | null; height?: number | null; weight?: number | null }>
+    packingDetails?: import('./returnInventory').ReturnCartonMergeHistory[]
+    currentCartons?: Array<{ carton: import('./returnInventory').ReturnCartonDetail; ownership: Array<{ returnNo: string; skuCode: string; quantity: number }>; orderQuantity: number; labelStatus: string; canRelabel: boolean; blockReason?: string }>
   } | null
   order: LastMileOrderDto
   manualNodes?: ManualNodesResponse['nodes']
diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts
index 6a68a983..b604f431 100644
--- a/src/modules/overseas-warehouse/api/relabelOrders.ts
+++ b/src/modules/overseas-warehouse/api/relabelOrders.ts
@@ -160,6 +160,10 @@ export interface ReturnPackingPlanDto extends Omit }>
+  completionCartons?: import('./returnInventory').ReturnCartonLabel[]
+  transfers?: import('./returnInventory').ReturnCartonMergeHistory[]
+  completedBy?: string
 }
 
 export async function createReturnPackingPlan(payload: ReturnPackingPlanPayload) {
diff --git a/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue b/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue
index fbe8d43c..1cb31117 100644
--- a/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue
+++ b/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue
@@ -1,6 +1,6 @@
 
 
 
 
 
+
+
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue
index 0633d7bb..7e3ecc76 100644
--- a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue
+++ b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue
@@ -274,6 +274,28 @@ onMounted(load)
               
             
             
+            

合箱记录

+ + + + + + + +

当前关联实物箱

+ + + +

来源单:{{ current.carton.sourceReturnNo }} · 状态:{{ current.carton.status }} · 我方箱唛:{{ current.labelStatus === 'CONFIRMED' ? '已确认贴标' : '待贴标确认' }}

+ + + + + +

{{ current.canRelabel ? '后续贴标按此当前箱唛选择整箱;跨来源单同 SKU 的部分贴标须先现场分拣确认。' : current.blockReason }}

+
+
+ diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts index 6ad71fc8..18e19759 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts +++ b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import OutboundsView from './OutboundsView.vue' import { listPlatformTenants } from '@/modules/iam' -const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn(), returnCandidates: vi.fn(), stock: vi.fn(), cartons: vi.fn(), carton: vi.fn() })) +const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn(), returnCandidates: vi.fn(), stock: vi.fn(), cartons: vi.fn(), carton: vi.fn(), transfers: vi.fn(), bindDestination: vi.fn() })) vi.mock('../api/returnInventory', () => ({ listReturnStock: api.stock, listReturnCartons: api.cartons, getReturnCarton: api.carton })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) @@ -14,6 +14,7 @@ vi.mock('../api/outbounds', () => ({ listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }), listOneClickOutboundCandidates: api.candidates, createOneClickDropshipOutbound: api.create, + listWarehouseTransferOutboundCandidates: api.transfers, bindWarehouseTransferDestination: api.bindDestination, cancelOutbound: vi.fn(), createReturnOutbound: vi.fn(), listReturnOutboundCandidates: api.returnCandidates, })) @@ -84,6 +85,8 @@ describe('outbound candidate search and selection', () => { vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined }) api.candidates.mockReset().mockResolvedValue(fixtures.map(item => ({ ...item }))) api.create.mockReset().mockResolvedValue({}) + api.transfers.mockReset().mockResolvedValue([]) + api.bindDestination.mockReset().mockResolvedValue([]) api.warehouses.mockReset().mockResolvedValue({ records: [ { id: 'warehouse-a', warehouseName: '洛杉矶仓', warehouseCode: 'US-LAX-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' }, { id: 'warehouse-b', warehouseName: '纽约仓', warehouseCode: 'US-NY-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' }, @@ -91,6 +94,40 @@ describe('outbound candidate search and selection', () => { }) afterEach(() => { wrapper?.unmount(); vi.unstubAllGlobals() }) + it('groups pallet candidates by customer and transfer and binds each selected transfer once', async () => { + const transfer = { customerId: 'customer-a', transferNo: 'WT-TEST', overseasWarehouseId: 'warehouse-a', transferStatus: 'WAIT_FBA_DESTINATION', needRelabel: false } + api.transfers.mockResolvedValue([ + { ...transfer, packageCount: 4, volumeCbm: 0.15 }, + { ...transfer, packageCount: 6, volumeCbm: 0.08 }, + { ...transfer, customerId: 'customer-b', packageCount: 2, volumeCbm: 0.1 }, + ]) + api.warehouses.mockResolvedValue({ records: [ + { id: 'warehouse-a', warehouseName: '洛杉矶仓', warehouseCode: 'US-LAX-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' }, + { id: 'fba-a', warehouseName: 'ABE5', warehouseCode: 'ABE5', warehouseType: 'FBA', status: 'ACTIVE' }, + ] }) + await openCreate() + const type = wrapper.findComponent({ name: 'ElRadioGroup' }) + type.vm.$emit('update:modelValue', 'WAREHOUSE_TRANSFER_OUTBOUND') + type.vm.$emit('change') + await flushPromises() + const table = wrapper.findAllComponents({ name: 'ElTable' }).find(row => row.classes().includes('dropship-candidate-table'))! + expect(table.props('data')).toEqual([ + expect.objectContaining({ customerId: 'customer-a', packageCount: 10, volumeCbm: 0.23 }), + expect.objectContaining({ customerId: 'customer-b', packageCount: 2, volumeCbm: 0.1 }), + ]) + for (const checkbox of table.findAllComponents(ElCheckbox)) checkbox.vm.$emit('change', true) + const destination = wrapper.findAllComponents(ElSelect).find(row => row.props('placeholder') === '请选择FBA目的仓')! + destination.vm.$emit('update:modelValue', 'fba-a') + await flushPromises() + await wrapper.find('[data-test="submit-outbound"]').trigger('click') + await flushPromises() + expect(api.bindDestination).toHaveBeenCalledOnce() + expect(api.bindDestination).toHaveBeenCalledWith(expect.objectContaining({ transfers: [ + { customerId: 'customer-a', transferNo: 'WT-TEST' }, + { customerId: 'customer-b', transferNo: 'WT-TEST' }, + ], destinationWarehouseCode: 'ABE5' })) + }) + it('offers enabled customers from later pages when creating a return outbound', async () => { vi.mocked(listPlatformTenants) .mockResolvedValueOnce({ records: [{ id: 'customer-a', tenantName: '柏悦物流demo2', status: 'ACTIVE' }], page: 1, size: 200, total: 201, pages: 2 } as Awaited>) diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 4adc9670..27fe7eaf 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -138,7 +138,19 @@ async function changeOutboundType() { selectedReturnNo.value = ''; selectedRetur async function loadTransferCandidates() { inventoryLoading.value = true try { - transferCandidates.value = await listWarehouseTransferOutboundCandidates({ customerId: selectedCustomerId.value || undefined, warehouseId: selectedWarehouseId.value || undefined }) + const pallets = await listWarehouseTransferOutboundCandidates({ customerId: selectedCustomerId.value || undefined, warehouseId: selectedWarehouseId.value || undefined }) + // The API returns pallet rows, but destination binding applies to the whole transfer. + const transfers = new Map() + for (const pallet of pallets) { + const key = transferSelectionKey(pallet) + const transfer = transfers.get(key) + if (transfer) { + transfer.packageCount += pallet.packageCount + transfer.grossWeightKg = sumTransferMeasurement(transfer.grossWeightKg, pallet.grossWeightKg) + transfer.volumeCbm = sumTransferMeasurement(transfer.volumeCbm, pallet.volumeCbm) + } else transfers.set(key, { ...pallet }) + } + transferCandidates.value = [...transfers.values()] const candidateKeys = new Set(transferCandidates.value.map(transferSelectionKey)) selectedTransferPalletIds.value = selectedTransferPalletIds.value.filter(key => candidateKeys.has(key)) if (transferCandidates.value.length === 1) selectedTransferPalletIds.value = [transferSelectionKey(transferCandidates.value[0])] @@ -149,6 +161,9 @@ async function loadTransferCandidates() { } finally { inventoryLoading.value = false } } function transferSelectionKey(row: WarehouseTransferOutboundCandidate) { return `${row.customerId}:${row.transferNo}` } +function sumTransferMeasurement(left?: number | null, right?: number | null) { + return left == null && right == null ? null : Number(((left ?? 0) + (right ?? 0)).toFixed(6)) +} function toggleTransferPallet(row: WarehouseTransferOutboundCandidate, checked: boolean | string | number) { const key = transferSelectionKey(row); selectedTransferPalletIds.value = checked ? [...new Set([...selectedTransferPalletIds.value, key])] : selectedTransferPalletIds.value.filter(id => id !== key) } function transferDestinationLabel(row: WarehouseTransferOutboundCandidate) { if (row.destinationType === 'PRIVATE' || row.privateDestinationAddress?.trim()) { diff --git a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue index 89476278..1b9e77db 100644 --- a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue +++ b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue @@ -177,9 +177,18 @@ function openPlanDetail(row: Record) { planDetail.value = planRows.value.find(plan => plan.planNo === row.planNo); planDetailOpen.value = !!planDetail.value; } -const planCartonItemRows = computed(() => (planDetail.value?.resultCartons || []).flatMap(carton => +const planCartonItemRows = computed(() => (planDetail.value?.completionCartons || []).flatMap(carton => carton.items.map(item => ({ ...carton, skuCode: item.skuCode, quantity: item.quantity })), )); +const planSourceRows = computed(() => (planDetail.value?.sources || []).flatMap(source => { + const original = source.originalItems?.length ? source.originalItems : source.items; + return original.map(item => { + const take = source.items.find(line => line.skuCode === item.skuCode)?.quantity || 0; + return { cartonMark: source.cartonMark, sourceReturnNo: source.sourceReturnNo, participation: source.participation, + skuCode: item.skuCode, originalQuantity: source.originalItems?.length ? item.quantity : null, take, + remaining: source.originalItems?.length ? item.quantity - take : null }; + }); +})); const plannedItemRows = computed(() => (planDetail.value?.boxes || []).flatMap((box, index) => (box.items || []).map(item => ({ boxNo: index + 1, skuCode: item.skuCode, quantity: item.quantity })), )); @@ -858,12 +867,34 @@ const tableColumns = computed(() => >{{ planDetail.remark || "—" }} + > +

计划来源箱明细

+ + + + + + + + + + + - - +