From 0be4088fa9ce6ce6d8a47cf2761cefc059832b34 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 8 Oct 2026 15:34:37 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overseas-warehouse/api/returnInventory.ts | 13 ++++++ .../overseas-warehouse/api/returnOrders.ts | 4 +- .../views/ReturnInventoryView.vue | 44 ++++++++++++++++--- .../views/ReturnOrdersView.vue | 18 ++++++-- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/modules/overseas-warehouse/api/returnInventory.ts b/src/modules/overseas-warehouse/api/returnInventory.ts index 440900d..f0d9415 100644 --- a/src/modules/overseas-warehouse/api/returnInventory.ts +++ b/src/modules/overseas-warehouse/api/returnInventory.ts @@ -54,6 +54,11 @@ export interface ReturnPackingPreview { skus: Array<{ skuCode: string; productNameZh?: string; availableQuantity: number }> } +export interface ReturnPackingPlan { + planNo: string; customerId: string; overseasWarehouseId: string; returnNos: string[]; mode: string; status: string + boxes: Array<{ boxNo?: number; items: Array<{ skuCode: string; quantity: number }>; length: number; width: number; height: number; weight: number }>; remark?: string +} + export function getReturnPackingPreview(returnNo?: string) { return apiRequest({ method: 'GET', url: '/pda/v1/last-leg/return-inventory/packing/orders', params: { returnNo: returnNo || undefined } }) } @@ -66,3 +71,11 @@ export function mergeReturnInventory(payload: { method: 'POST', url: '/pda/v1/last-leg/return-inventory/cartons/merge', data: payload, }) } + +export function listReturnPackingPlans(params: Record = {}) { + return apiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/return-packing-plans', params }) +} + +export function createReturnPackingPlan(payload: { returnNos: string[]; mode: string; boxes: ReturnPackingPlan['boxes']; remark?: string; idempotencyKey: string }) { + return apiRequest({ method: 'POST', url: '/merchant/v1/last-leg/return-packing-plans', data: payload }) +} diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts index c8b7233..e6cda49 100644 --- a/src/modules/overseas-warehouse/api/returnOrders.ts +++ b/src/modules/overseas-warehouse/api/returnOrders.ts @@ -14,10 +14,12 @@ export interface ReturnReceipt { receivedQuantity: number; operatorName?: string; signedAt: string; unitType: 'PALLET' | 'BOX' | 'PARCEL'; unitCode?: string; boxCount: number items: Array<{ skuCode: string; productNameZh?: string; quantity: number; boxCount: number }>; cartonMark?: string; printableHtml?: string labels?: Array<{ cartonMark: string; printableHtml: string }> + labelRepairRequired?: boolean } export interface ReturnPallet { palletCode: string; unitType: 'PALLET' | 'BOX'; unitCode: string; skuTypes: number; totalQuantity: number; totalBoxCount: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number; boxCount: number }> } export interface ReturnPhoto { fileId: number; fileName: string; contentType: string; sizeBytes: number } -export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[] } +export interface ReturnPackingDetail { sourceCartonMark: string; targetCartonMark: string; returnNo: string; skuCode: string; quantity: number; createdAt: string } +export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[] } export interface ReturnOrderCreate { overseasWarehouseId: string; warehouseContactName: string; warehouseContactPhone: string; customerReferenceNo?: string; remark?: string; needRelabel: boolean; idempotencyKey: string items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }> diff --git a/src/modules/overseas-warehouse/views/ReturnInventoryView.vue b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue index f82c6e1..d506bce 100644 --- a/src/modules/overseas-warehouse/views/ReturnInventoryView.vue +++ b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue @@ -1,12 +1,13 @@ @@ -28,7 +30,7 @@ const primaryActions = computed(() => [ {{ action.title }}{{ action.description }} - 开始下单 + {{ action.actionLabel ?? '开始下单' }}

当前账号暂无常用业务权限,请通过已授权菜单办理业务,或联系企业管理员。

@@ -46,7 +48,7 @@ const primaryActions = computed(() => [ .business-intro { padding-top: 0; } .business-intro h1 { margin: 0; color: #172033; font-size: 27px; font-weight: 750; letter-spacing: -.5px; line-height: 1.3; } .business-intro p { margin: 5px 0 0; color: #7a8495; font-size: 13px; line-height: 1.55; } -.quick-actions { display: grid; grid-template-columns: repeat(6, minmax(188px, 1fr)); gap: 10px; overflow-x: auto; padding: 0 0 6px; scroll-snap-type: x proximity; scrollbar-color: #bdc9dc transparent; scrollbar-width: thin; } +.quick-actions { display: grid; grid-template-columns: repeat(8, minmax(150px, 1fr)); gap: 10px; overflow-x: auto; padding: 0 0 6px; scroll-snap-type: x proximity; scrollbar-color: #bdc9dc transparent; scrollbar-width: thin; } .quick-actions::-webkit-scrollbar { height: 7px; } .quick-actions::-webkit-scrollbar-thumb { border-radius: 999px; background: #bdc9dc; } .quick-actions::-webkit-scrollbar-track { background: transparent; } @@ -60,6 +62,8 @@ const primaryActions = computed(() => [ .quick-action--cyan { --action-a: #0891b2; --action-b: #0e7490; --action-soft: rgba(8, 145, 178, .18); } .quick-action--orange { --action-a: #f59e0b; --action-b: #dc6b12; --action-soft: rgba(245, 158, 11, .2); } .quick-action--rose { --action-a: #e11d48; --action-b: #be185d; --action-soft: rgba(225, 29, 72, .17); } +.quick-action--teal { --action-a: #0f766e; --action-b: #0e7490; --action-soft: rgba(15, 118, 110, .18); } +.quick-action--brown { --action-a: #b45309; --action-b: #c2410c; --action-soft: rgba(180, 83, 9, .18); } .quick-action:hover { border-color: color-mix(in srgb, var(--action-a) 35%, transparent); box-shadow: 0 24px 54px rgba(15, 23, 42, .14), 0 0 0 1px color-mix(in srgb, var(--action-a) 16%, transparent); transform: translateY(-9px); } .quick-action:hover::before { transform: scale(1.25); } .quick-action:hover::after { right: 12%; left: 12%; opacity: 1; transform: scaleX(1); } @@ -93,8 +97,8 @@ const primaryActions = computed(() => [ @keyframes action-enter { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: translateY(0); } } @keyframes featured-aura { 0%, 100% { opacity: .86; transform: translate3d(0, 0, 0) scale(1); } 50% { opacity: 1; transform: translate3d(-10px, 9px, 0) scale(1.08); } } @keyframes featured-shine { 0%, 55% { left: -55%; opacity: 0; } 65% { opacity: 1; } 88%, 100% { left: 125%; opacity: 0; } } -@container dashboard-content (max-width: 1180px) { .quick-actions { grid-template-columns: repeat(6, minmax(178px, 1fr)); } } +@container dashboard-content (max-width: 1180px) { .quick-actions { grid-template-columns: repeat(8, minmax(178px, 1fr)); } } @media (max-width: 720px) { .client-dashboard { padding: 0; gap: 16px; } .business-intro h1 { font-size: 25px; } .quick-actions { grid-template-columns: repeat(2, minmax(150px, 1fr)); } } -@media (max-width: 420px) { .quick-actions { grid-template-columns: repeat(6, minmax(168px, 1fr)); } } +@media (max-width: 420px) { .quick-actions { grid-template-columns: repeat(8, minmax(168px, 1fr)); } } @media (prefers-reduced-motion: reduce) { .client-dashboard, .quick-action, .quick-action--featured::before, .quick-action--featured .action-enter::before { animation: none; } .quick-action, .quick-action::before, .quick-action::after, .action-icon, .action-enter svg { transition: none; } } -- 2.54.0 From 2aa20b7172a6005f137814738c8a9183efca038d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 8 Oct 2026 20:18:39 +0800 Subject: [PATCH 4/4] test(client): verify return labels preserve server print templates --- .../views/ReturnOrdersView.test.mjs | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 14b21f5..651ddc1 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -74,13 +74,68 @@ test('退货签收详情使用独立路由和紧凑连续页面', () => { assert.doesNotMatch(view, /class="return-detail-dialog"/) }) -test('退货箱唛打印复用统一的紧凑单页模板并按箱分页', () => { - assert.match(view, /@page\{size:100mm 100mm;margin:8mm\}/) - assert.match(view, /\.label\{width:84mm;height:84mm;padding:8mm/) - assert.match(view, /html\.slice\(contentStart, bodyEnd\)/) +// ReturnInventoryService supplies the 100 mm template; the client must preserve +// its page margin (8 mm) and content padding (4 mm), including barcode markup. +const labelStyle = '' +const labelHtml = mark => `${labelStyle}

${mark}

` + +function printHarness() { + const source = view.slice(view.indexOf('async function printReturnLabel('), view.indexOf('\nfunction openDetail(')) + .replace('receipt: ReturnReceipt', 'receipt') + const calls = { opened: 0, printed: 0, html: '', warnings: [], errors: [], fetched: [] } + const popup = { + document: { open() {}, write(html) { calls.html = html }, close() {} }, + focus() {}, print() { calls.printed += 1 }, close() {}, + } + const print = new Function('window', 'ElMessage', 'getReturnInventoryLabel', `${source}; return printReturnLabel`)( + { open() { calls.opened += 1; return popup } }, + { warning(message) { calls.warnings.push(message) }, error(message) { calls.errors.push(message) } }, + async mark => { calls.fetched.push(mark); return { printableHtml: labelHtml(mark) } }, + ) + return { print, calls } +} + +test('退货箱唛打印保留服务端整份模板,不另行覆盖纸张或条码', async () => { + const { print, calls } = printHarness() + const printableHtml = labelHtml('RETURN-TEST-1') + await print({ cartonMark: 'RETURN-TEST-1', printableHtml }) + assert.equal(calls.html, printableHtml) + assert.equal(calls.printed, 1) + assert.deepEqual(calls.fetched, []) + assert.deepEqual(calls.errors, []) +}) + +test('退货多箱打印保留统一模板尺寸和每箱条码,并按箱分页', async () => { + const { print, calls } = printHarness() + const labels = ['RETURN-TEST-1', 'RETURN-TEST-2'].map(cartonMark => ({ cartonMark, printableHtml: labelHtml(cartonMark) })) + await print({ cartonMark: labels[0].cartonMark, labels }) + assert.equal(calls.html.split(labelStyle).length - 1, 1) + assert.equal((calls.html.match(//g) || []).length, 1) + assert.equal((calls.html.match(/
/g) || []).length, 2) + for (const label of labels) assert.ok(calls.html.includes(``)) + assert.match(calls.html, /\.return-label\{break-after:page\}/) + assert.match(calls.html, /\.return-label:last-child\{break-after:auto\}/) + assert.match(calls.html, /\.return-label \.label\{page-break-after:auto\}/) + assert.equal(calls.printed, 1) + assert.deepEqual(calls.fetched, []) + assert.deepEqual(calls.errors, []) assert.match(view, /打印全部箱唛(\{\{ receipt\.labels\?\.length \|\| receipt\.boxCount \|\| 1 \}\} 箱)/) }) +test('缺少内嵌模板时读取鉴权箱唛模板,待修复箱记录禁止打印', async () => { + const { print, calls } = printHarness() + await print({ cartonMark: 'RETURN-TEST-1' }) + assert.deepEqual(calls.fetched, ['RETURN-TEST-1']) + assert.ok(calls.html.includes(labelStyle)) + assert.equal(calls.printed, 1) + await print({ cartonMark: 'RETURN-TEST-2', labelRepairRequired: true }) + assert.equal(calls.opened, 1) + assert.equal(calls.printed, 1) + assert.deepEqual(calls.fetched, ['RETURN-TEST-1']) + assert.equal(calls.warnings.length, 1) + assert.deepEqual(calls.errors, []) +}) + test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => { assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/) assert.match(view, /if \(editingReturnNo\.value\) await updateReturnOrder\(editingReturnNo\.value, payload\)/) -- 2.54.0