From 648b0c858da21ee87582736446b50cb784517b09 Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Fri, 9 Oct 2026 11:49:03 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/return-packing/return-packing.vue | 6 +--
src/pages/return-receipt/return-receipt.vue | 43 +++++++++++----
tests/return-receipt-page.test.mjs | 58 +++++++++++++++++++++
3 files changed, 93 insertions(+), 14 deletions(-)
diff --git a/src/pages/return-packing/return-packing.vue b/src/pages/return-packing/return-packing.vue
index 9c03496..5009c5f 100644
--- a/src/pages/return-packing/return-packing.vue
+++ b/src/pages/return-packing/return-packing.vue
@@ -4,14 +4,14 @@
- 扫描 SKU 或箱号
- 待作业合箱计划审核通过后推送
+ 仓库现场直接合箱扫描 SKU 或箱号开始;系统会自动锁定客户和当前仓,跨客户货物不能合箱。
+ 平台合箱计划审核通过后推送按计划锁定的来源箱和数量作业,后续合箱规则不变。
选择客户该 SKU 在多个客户有可用退货库存,请先选择客户
没有匹配的已完成退货单或可用库存
- {{ activePlan ? `合箱计划 ${activePlan.planNo}` : '客户散件库存' }}{{ activePlan ? '数量已由计划锁定,请分配到目标箱。' : '可用数量来自该客户同仓全部散件库存,后台自动追溯来源退货单。' }}
+ {{ activePlan ? `合箱计划 ${activePlan.planNo}` : '客户散件库存' }}{{ activePlan ? '数量已由计划锁定,请分配到目标箱。' : '已锁定一个客户;可用数量来自该客户同仓全部散件库存,后台自动追溯来源退货单。' }}
新箱 SKU 数量{{ item.skuCode }}{{ item.productNameZh || '商品名称未填写' }}可用 {{ item.availableQuantity }}装入数量
diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue
index 3f41748..2fd2f72 100644
--- a/src/pages/return-receipt/return-receipt.vue
+++ b/src/pages/return-receipt/return-receipt.vue
@@ -16,29 +16,27 @@
待完成按箱签收{{ receiptBatch.returnNo }}已确认 {{ receiptBatch.entries.filter(entry => entry.result).length }} / {{ receiptBatch.entries.length }} 箱。请继续本批签收,已确认的箱不会重新入库;本批内容暂不可修改。
- {{ current.returnNo }}{{ startedBy === 'SKU' ? '通过 SKU 候选进入' : '通过退货单号进入' }}{{ statusLabel(current.status) }}
+ 退货单{{ current.returnNo }}{{ statusLabel(current.status) }}
本次实物单元
- 按现场收到的实物选择维度,托盘不是必填项。
包裹码/快递单号
- 托盘入库:扫描已有托盘或生成新托盘,沿用原托盘测量流程。箱子可直接散装入库,也可扫描托盘直接打托。
- 托盘码({{ unitType === 'BOX' ? '选填' : '必填' }}){{ palletCheck.empty ? '空托盘,可绑定当前退货单和 SKU' : `当前单托盘,已有 ${palletCheck.skuCodes.join('、')},共 ${palletCheck.quantity} 件` }}
- 体积{{ palletVolume }}cm³
+ 托盘码({{ unitType === 'BOX' ? '选填' : '必填' }}){{ palletCheck.empty ? '空托盘,可绑定当前退货单和 SKU' : `当前单托盘,已有 ${palletCheck.skuCodes.join('、')},共 ${palletCheck.quantity} 件` }}
+
快递单号(选填)
- {{ unitType === 'PARCEL' ? '本次实收数量' : (unitType === 'PALLET' ? '本托实收数量' : '逐箱实收数量') }}本托箱数合计{{ palletBoxCount }}本次箱数第 {{ index + 1 }} 箱{{ item.skuCode }} · 数量{{ item.skuCode }}{{ item.productNameZh || '商品名称未填写' }}剩余 {{ remaining(item) }}数量此 SKU 箱数
+ {{ unitType === 'PARCEL' ? '本次实收数量' : (unitType === 'PALLET' ? '本托实收数量' : '逐箱实收数量') }}本托箱数合计{{ palletBoxCount }}本次箱数第 {{ index + 1 }} 箱{{ item.skuCode }} · 数量{{ item.skuCode }}{{ item.productNameZh || '商品名称未填写' }}剩余 {{ remaining(item) }}数量SKU 箱数
本托合计{{ receiptQuantity }}
退货打托照片签收过程中可随时拍照{{ photoFileIds.length }} / 20 张{{ photoNames[index] || '退货打托照片' }}
本单已完成打托{{ pallet.palletCode }}{{ pallet.skuCode }} · {{ pallet.quantity }} 件签收批次:{{ pallet.receiptBatchNo }}
- 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态
+ 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态
{{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
@@ -64,9 +62,9 @@
startedBy: { get() { return warehouseWork.returnReceipt.startedBy }, set(value) { updateWarehouseWork('returnReceipt', { startedBy: value }) } },
trackingNo: { get() { return warehouseWork.returnReceipt.trackingNo }, set(value) { updateWarehouseWork('returnReceipt', { trackingNo: value }) } },
palletCode: { get() { return warehouseWork.returnReceipt.palletCode }, set(value) { updateWarehouseWork('returnReceipt', { palletCode: value }) } },
- selectedItems() { return this.current?.expectedItems?.filter(item => !this.selectedSku || item.skuCode === this.selectedSku) || [] },
+ selectedItems() { return this.current?.expectedItems || [] },
palletBoxCount() { return this.selectedItems.reduce((sum, item) => sum + this.boxCount(item.skuCode), 0) },
- activeSkuCode() { return this.selectedItems.length === 1 ? this.selectedItems[0].skuCode : '' },
+ activeSkuCode() { return this.selectedItems.find(item => item.skuCode === this.selectedSku)?.skuCode || this.selectedItems[0]?.skuCode || '' },
receiptQuantity() { return this.unitType === 'BOX' ? this.boxRows.reduce((sum, box) => sum + Object.values(box.items).reduce((n, quantity) => n + (Number(quantity) || 0), 0), 0) : Object.values(warehouseWork.returnReceipt.quantities || {}).reduce((total, value) => total + (Number(value) || 0), 0) },
canConfirm() { if (!this.current || !this.selectedItems.length || this.receiptQuantity <= 0) return false; if (this.unitType === 'BOX') return this.boxRows.length > 0 && this.boxRows.every(box => Object.values(box.items).some(quantity => Number(quantity) > 0)) && this.selectedItems.every(item => this.boxRows.reduce((sum, box) => sum + (Number(box.items[item.skuCode]) || 0), 0) <= this.remaining(item)); return (this.unitType === 'PARCEL' ? this.unitCode.trim() : this.palletCode.trim() && this.palletCheck && Number(this.palletLength) > 0 && Number(this.palletWidth) > 0 && Number(this.palletHeight) > 0 && Number(this.palletWeight) > 0 && (this.unitType !== 'PALLET' || this.palletBoxCount > 0)) && this.selectedItems.every((item) => this.quantity(item.skuCode) <= this.remaining(item) && (this.unitType !== 'PALLET' || this.quantity(item.skuCode) === 0 || this.boxCount(item.skuCode) > 0)); },
unitLabel() { return this.unitType === 'PALLET' ? '托盘码' : this.unitType === 'BOX' ? '箱号/箱唛' : '包裹码/快递单号' },
@@ -135,7 +133,7 @@
},
finishReceipt(results) {
const last = results[results.length - 1]
- this.result = { ...last, labels: results.filter(item => item.unitType === 'BOX' && item.printableHtml).map(item => ({ cartonMark: item.cartonMark, printableHtml: item.printableHtml })) }
+ this.result = { ...last, continuationReturnNo: !last.fullyMatched ? this.current?.returnNo || '' : '', labels: results.filter(item => item.unitType === 'BOX' && item.printableHtml).map(item => ({ cartonMark: item.cartonMark, printableHtml: item.printableHtml })) }
resetWarehouseDomain('returnReceipt')
this.current = null
this.candidates = null
@@ -148,7 +146,27 @@
removePhoto(index) { const [id] = this.photoFileIds.splice(index, 1); this.photoNames.splice(index, 1); if (id) delete this.photoUrls[id] },
async reloadCurrent() { if (!this.current?.returnNo || this.busy) return; await this.openOrder(this.current.returnNo, this.startedBy, false) },
clearCurrent() { if (this.busy || this.receiptBatch) return; resetWarehouseDomain('returnReceipt'); this.current = null; this.candidates = null; this.error = null; this.activeTarget = 'lookup' },
- nextReceipt() { this.result = null; this.clearCurrent() },
+ continueScanning() {
+ if (this.busy || this.receiptBatch) return
+ this.clearCurrent()
+ this.result = null
+ this.entryMode = 'SKU'; this.lookupCode = ''; this.selectedSku = ''
+ this.palletCheck = null; this.newPalletHtml = ''
+ this.palletLength = ''; this.palletWidth = ''; this.palletHeight = ''; this.palletWeight = ''
+ this.unitCode = ''; this.boxRows = [{ items: {} }]
+ this.photoFileIds = []; this.photoNames = []; this.photoUrls = {}
+ },
+ async nextReceipt() {
+ if (this.busy) return
+ const returnNo = this.result?.continuationReturnNo
+ if (!returnNo) { this.result = null; this.clearCurrent(); return }
+ await this.run(async () => {
+ await this.openOrderValue(returnNo, 'SKU')
+ this.palletLength = ''; this.palletWidth = ''; this.palletHeight = ''; this.palletWeight = ''
+ this.unitCode = ''; this.boxRows = [{ items: {} }]; this.newPalletHtml = ''
+ this.result = null
+ }, '读取剩余待签收货物失败')
+ },
async run(work, fallback) { if (this.busy) return; this.busy = true; this.error = null; try { await work() } catch (error) { this.setError(error, fallback) } finally { this.busy = false } },
setError(error, fallback) { this.error = { message: error?.message || fallback, code: error?.code, requestId: error?.requestId, traceId: error?.traceId }; playErrorSound() },
statusLabel(value) { return ({ PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '签收中', COMPLETED: '已完成' })[value] || value || '状态未知' }
@@ -158,6 +176,9 @@
diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue
index 2fd2f72..73f81d5 100644
--- a/src/pages/return-receipt/return-receipt.vue
+++ b/src/pages/return-receipt/return-receipt.vue
@@ -36,7 +36,7 @@
本单已完成打托{{ pallet.palletCode }}{{ pallet.skuCode }} · {{ pallet.quantity }} 件签收批次:{{ pallet.receiptBatchNo }}
- 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态
+ 退货签收成功第 {{ index + 1 }} 箱 · {{ label.cartonMark }}{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}托盘:{{ result.palletCode }}签收单:{{ result.receiptNo }}计费批次:{{ result.receiptBatchNo }}{{ result.receivedQuantity }}/{{ result.expectedQuantity }}整单累计{{ result.fullyMatched ? '已完成' : '签收中' }}退货单状态
{{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
@@ -74,6 +74,7 @@
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; this.startScanner(); if (!this.receiptBatch && warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) },
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
methods: {
+ goCount() { const mark = this.result?.labels?.[0]?.cartonMark; if (mark) uni.navigateTo({ url: '/pages/return-counting/return-counting?cartonMark=' + encodeURIComponent(mark) }) },
selectUnitType(type) { if (this.busy) return; this.unitType = type; this.unitCode = ''; this.boxRows = [{ items: {} }]; this.palletCheck = null; this.activeTarget = type === 'PALLET' ? 'pallet' : 'unit' },
resizeBoxRows(raw) { const count = Math.floor(Number(raw) || 0); if (count < 1 || count > 100) return; this.boxRows = Array.from({ length: count }, (_, index) => this.boxRows[index] || { items: {} }) },
setBoxQuantity(index, skuCode, raw) { const value = Math.max(0, Math.floor(Number(raw) || 0)); this.boxRows[index].items = { ...this.boxRows[index].items, [skuCode]: value } },
diff --git a/tests/return-counting-api.test.mjs b/tests/return-counting-api.test.mjs
new file mode 100644
index 0000000..a3946c0
--- /dev/null
+++ b/tests/return-counting-api.test.mjs
@@ -0,0 +1,43 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { listReturnCountTasks, scanReturnCountCarton, scanReturnCountTasks, getCurrentReturnCountTask, beginReturnCountTask, confirmReturnCount } from '../src/api/return-counting.js'
+import { session } from '../src/stores/session.js'
+
+test('counting preserves box contents and retry key without accepting client warehouse scope', async () => {
+ session.apiBase = '/api'; session.token = 'counting-test'
+ const calls = []
+ globalThis.uni = { request(options) { calls.push(options); options.success({ statusCode: 200, data: { code: '200', data: { status: 'PENDING' } }, header: {} }) } }
+ await listReturnCountTasks({ page: 2, keyword: ' RC#2-1 ' })
+ await scanReturnCountCarton('RC#2-1')
+ const request = { idempotencyKey: 'count-retry', boxes: [{ cartonMark: 'RC#2-1', items: [{ skuCode: 'SKU-1', quantity: 3 }] }, { cartonMark: 'RC#2-2', items: [{ skuCode: 'SKU-1', quantity: 7 }] }] }
+ await confirmReturnCount('RR/1', request)
+ assert.deepEqual(calls.map(row => [row.method, row.url]), [
+ ['GET', '/api/pda/v1/last-leg/return-inventory/counting/tasks?page=2&size=10&keyword=RC%232-1'],
+ ['GET', '/api/pda/v1/last-leg/return-inventory/counting/cartons/RC%232-1'],
+ ['POST', '/api/pda/v1/last-leg/return-inventory/counting/receipts/RR%2F1']
+ ])
+ assert.deepEqual(calls[2].data, request)
+ assert.ok(calls.every(row => row.header.Authorization === 'Bearer counting-test'))
+})
+
+test('unified scan supports box and pallet codes and preserves the no-count empty result', async () => {
+ session.apiBase = '/api'; session.token = 'counting-test'
+ const urls = []
+ globalThis.uni = { request(options) { urls.push(options.url); options.success({ statusCode: 200, data: { code: '200', data: [] }, header: {} }) } }
+ assert.deepEqual(await scanReturnCountTasks(' RC#2-1 '), [])
+ assert.deepEqual(await scanReturnCountTasks('PLT/1'), [])
+ assert.deepEqual(urls, ['/api/pda/v1/last-leg/return-inventory/counting/scan/RC%232-1', '/api/pda/v1/last-leg/return-inventory/counting/scan/PLT%2F1'])
+})
+
+test('current task preserves null and begin uses the operator-scoped claim route', async () => {
+ session.apiBase = '/api'; session.token = 'counting-test'
+ const calls = []
+ globalThis.uni = { request(options) { calls.push(options); options.success({ statusCode: 200, data: { code: '200', data: options.method === 'GET' ? null : { receiptNo: 'RR/1', status: 'COUNTING' } }, header: {} }) } }
+ assert.equal(await getCurrentReturnCountTask(), null)
+ assert.equal((await beginReturnCountTask('RR/1')).status, 'COUNTING')
+ assert.deepEqual(calls.map(row => [row.method, row.url]), [
+ ['GET', '/api/pda/v1/last-leg/return-inventory/counting/current'],
+ ['POST', '/api/pda/v1/last-leg/return-inventory/counting/receipts/RR%2F1/begin']
+ ])
+ assert.equal(calls[1].data, undefined)
+})
diff --git a/tests/return-counting-page.test.mjs b/tests/return-counting-page.test.mjs
new file mode 100644
index 0000000..f00a954
--- /dev/null
+++ b/tests/return-counting-page.test.mjs
@@ -0,0 +1,157 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { runInNewContext } from 'node:vm'
+
+const source = readFileSync(new URL('../src/pages/return-counting/return-counting.vue', import.meta.url), 'utf8')
+const script = source.match(/
-
+.destination-summary{display:flex;align-items:center;flex-wrap:wrap;gap:5px;margin-top:6px}.destination-summary .meta-line{margin:0}.destination-tag{display:inline-block;padding:3px 7px;border-radius:5px;background:#edf4ff;color:#285d9f;font-size:10px;line-height:14px}
+.carton-label-list{margin-top:10px;gap:6px}.carton-label-item{display:flex;align-items:center;flex-wrap:nowrap;gap:6px;padding:7px 0;min-width:0}.carton-label{display:flex;flex:1;flex-direction:column;gap:2px;box-sizing:border-box;min-width:0;overflow:hidden;padding:5px 7px;border:1px solid #dce5ef;border-radius:6px;background:#f6f8fb}.fba-label{border-color:#cddff9;background:#eff5ff}.label-caption{color:#718095;font-size:9px;line-height:13px}.label-code{display:block;color:#253e5b;font-size:10px;font-weight:600;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.label-arrow{flex-shrink:0;color:#8090a3;font-size:12px}.carton-label-item>.destination-tag{flex-shrink:0}.sku-detail-card{padding:8px 10px}.sku-detail-card .business-code{font-size:12px;font-weight:600;line-height:18px;overflow-wrap:anywhere}.sku-detail-card .meta-line{font-size:9px;line-height:14px}.sku-detail-card .link-button{height:26px;font-size:9px;line-height:26px}
+.summary-metrics{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:4px;margin-left:auto;font-size:9px;line-height:18px}.summary-metric{display:inline-block;padding:1px 4px;border-radius:4px;background:#dcfce7;color:#16a34a;font-weight:700;white-space:nowrap}
+.candidate-heading{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px}.candidate-customer{color:#52647c;font-size:10px;line-height:16px;overflow-wrap:anywhere}
diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue
index 282835e..5ec3f47 100644
--- a/src/pages/return-receipt/return-receipt.vue
+++ b/src/pages/return-receipt/return-receipt.vue
@@ -21,7 +21,7 @@
本次实物单元
- 包裹码/快递单号(选填)
+ 快递单号(选填)
托盘码({{ unitType === 'BOX' ? '选填' : '必填' }}){{ palletCheck.empty ? '空托盘,可绑定当前退货单和 SKU' : `当前单托盘,已有 ${palletCheck.skuCodes.join('、')},共 ${palletCheck.quantity} 件` }}
@@ -103,7 +103,7 @@
await this.run(async () => {
const rows = this.unitType === 'BOX' ? this.boxRows : [null]
const entries = rows.map((box, index) => {
- const payload = { returnNo: this.current.returnNo, trackingNo: this.trackingNo.trim() || undefined, startedBy: this.startedBy, unitType: this.unitType, unitCode: this.unitType === 'PARCEL' ? this.unitCode.trim() : undefined, palletCode: this.unitType === 'PALLET' || this.unitType === 'BOX' ? this.palletCode.trim() || undefined : undefined, length: this.unitType === 'PALLET' ? Number(this.palletLength) : undefined, width: this.unitType === 'PALLET' ? Number(this.palletWidth) : undefined, height: this.unitType === 'PALLET' ? Number(this.palletHeight) : undefined, weight: this.unitType === 'PALLET' ? Number(this.palletWeight) : undefined, totalBoxCount: this.unitType === 'PALLET' ? totalBoxCount : undefined, items: (this.unitType === 'BOX' ? this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: Number(box.items[item.skuCode]) || 0, boxCount: 1 })) : this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: this.quantity(item.skuCode), boxCount: this.unitType === 'PALLET' ? this.boxCount(item.skuCode) : undefined }))).filter((item) => item.receivedQuantity > 0) }
+ const payload = { returnNo: this.current.returnNo, trackingNo: (this.unitType === 'PARCEL' ? this.unitCode.trim() : this.trackingNo.trim()) || undefined, startedBy: this.startedBy, unitType: this.unitType, unitCode: undefined, palletCode: this.unitType === 'PALLET' || this.unitType === 'BOX' ? this.palletCode.trim() || undefined : undefined, length: this.unitType === 'PALLET' ? Number(this.palletLength) : undefined, width: this.unitType === 'PALLET' ? Number(this.palletWidth) : undefined, height: this.unitType === 'PALLET' ? Number(this.palletHeight) : undefined, weight: this.unitType === 'PALLET' ? Number(this.palletWeight) : undefined, totalBoxCount: this.unitType === 'PALLET' ? totalBoxCount : undefined, items: (this.unitType === 'BOX' ? this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: Number(box.items[item.skuCode]) || 0, boxCount: 1 })) : this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: this.quantity(item.skuCode), boxCount: this.unitType === 'PALLET' ? this.boxCount(item.skuCode) : undefined }))).filter((item) => item.receivedQuantity > 0) }
const key = warehouseIdempotencyKey('returnReceipt', `confirm-${index}`, payload)
return { request: { ...payload, idempotencyKey: key.value }, result: null }
})
diff --git a/src/utils/relabel-error.js b/src/utils/relabel-error.js
new file mode 100644
index 0000000..302c7f3
--- /dev/null
+++ b/src/utils/relabel-error.js
@@ -0,0 +1,8 @@
+export function isRelabelFundsBlocked(error) {
+ return /Relabel funds must be frozen before processing|换标资金尚未冻结/.test(String(error?.message || ''))
+}
+
+export function relabelErrorMessage(error, fallback = '换标作业失败') {
+ if (isRelabelFundsBlocked(error)) return '换标资金尚未冻结,暂时不能认领。请后台检查客户余额或授信及资金冻结结果,处理后再认领。'
+ return error?.message || fallback
+}
diff --git a/tests/relabel-destination.test.mjs b/tests/relabel-destination.test.mjs
index 2884823..0a73723 100644
--- a/tests/relabel-destination.test.mjs
+++ b/tests/relabel-destination.test.mjs
@@ -6,7 +6,8 @@ const page = readFileSync(new URL('../src/pages/relabel/relabel.vue', import.met
test('PDA 箱唛换标明确展示每箱的新标签和 FBA 目的仓', () => {
assert.match(page, /item\.cartonMark[^\n]+item\.fbaCartonMark/)
- assert.match(page, /FBA 目的仓:\{\{ item\.destinationFbaWarehouseCode/)
+ assert.match(page, /class="destination-summary"[^\n]+FBA 目的仓:[^\n]+v-for="destination in fbaDestinations"/)
+ assert.match(page, /v-if="fbaDestinations.length > 1"[^\n]+item\.destinationFbaWarehouseCode/)
assert.match(page, /confirmedItem\?\.destinationFbaWarehouseCode \|\| item\.destinationFbaWarehouseCode/)
assert.match(page, /请贴 FBA 箱唛[^`]+FBA 目的仓:\$\{this\.lastCarton\.destinationFbaWarehouseCode/s)
})
diff --git a/tests/relabel-error.test.mjs b/tests/relabel-error.test.mjs
new file mode 100644
index 0000000..2abf2af
--- /dev/null
+++ b/tests/relabel-error.test.mjs
@@ -0,0 +1,12 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { isRelabelFundsBlocked, relabelErrorMessage } from '../src/utils/relabel-error.js'
+
+test('wallet block receives actionable Chinese guidance without version-conflict recovery', () => {
+ const error = { code: 'A0409', message: 'Relabel funds must be frozen before processing' }
+ assert.equal(isRelabelFundsBlocked(error), true)
+ assert.match(relabelErrorMessage(error), /后台检查客户余额或授信/)
+ assert.doesNotMatch(relabelErrorMessage(error), /刷新任务版本/)
+ assert.equal(isRelabelFundsBlocked({ code: 'A0409', message: 'Task version changed' }), false)
+ assert.equal(relabelErrorMessage({ message: 'Task version changed' }), 'Task version changed')
+})
diff --git a/tests/relabel-pallet-work.test.mjs b/tests/relabel-pallet-work.test.mjs
index ca1f068..192b7d3 100644
--- a/tests/relabel-pallet-work.test.mjs
+++ b/tests/relabel-pallet-work.test.mjs
@@ -14,6 +14,7 @@ function context() {
] } }
for (const [key, method] of Object.entries(component.methods)) value[key] = method.bind(value)
for (const [key, method] of Object.entries(component.computed)) Object.defineProperty(value, key, { get: method.bind(value) })
+ value.confirmBindingChoice = async () => true
value.selectSource({ positionId: '1', palletId: '20', palletCode: 'SOURCE', movablePackages: 2, cartonMarks: ['A', 'B', 'C'] })
return value
}
@@ -58,7 +59,7 @@ test('pallet comparison tolerates mixed API versions and canonical code formatti
test('quantity disassembly has no extra carton scanner', () => {
assert.doesNotMatch(source, /relabel-move-carton/)
assert.match(source, /actualSource:/)
- assert.match(source, /仍归属/)
+ assert.match(source, /this\.message = `托盘 \$\{this\.target\.palletCode\} 已绑定`/)
})
test('source pallet list is concise and hides rows with no movable packages', () => {
@@ -70,7 +71,8 @@ test('source pallet list is concise and hides rows with no movable packages', ()
assert.deepEqual(value.operableSourceRows.map(row => row.palletCode), ['PALLET-1'])
assert.match(source, />托盘)
assert.match(source, />刷新)
- assert.match(source, /托盘号:\{\{ row\.palletCode \}\} · 入仓码:\{\{ row\.inboundCode \}\}/)
+ assert.match(source, /class="source-caption">托盘号<\/text>\{\{ row\.palletCode \}\}/)
+ assert.doesNotMatch(template, /入仓码|row\.inboundCode/)
assert.match(source, /可拆\{\{ isReturnBulk \? '件数' : '箱数' \}\}:\{\{ row\.movablePackages \}\}/)
assert.doesNotMatch(template, /刷新来源托盘/)
assert.doesNotMatch(template, /按原入仓单拆托/)
@@ -104,10 +106,55 @@ test('target confirmation submits binding directly, with no second submit button
value.bind = async () => { binds++ }
await value.confirmTargetAndBind()
assert.equal(binds, 1)
- assert.match(source, /@tap="confirmTargetAndBind"/)
+ assert.match(source, /@submit="confirmTargetAndBind"/)
assert.doesNotMatch(source, /@tap="bind"/)
})
+test('source scan asks before accepting a listed pallet and rejects unknown pallets', async () => {
+ const value = context()
+ value.sourceRows = [{ positionId: '1', palletCode: 'SOURCE', movablePackages: 2 }]
+ value.sourceCode = 'SOURCE'
+ let prompts = 0
+ value.resolve = async () => { value.source = { palletCode: 'SOURCE' } }
+ value.confirmBindingChoice = async () => { prompts++; return false }
+ await value.confirmSourceScan()
+ assert.equal(value.source, null)
+ value.confirmBindingChoice = async () => { prompts++; return true }
+ await value.confirmSourceScan()
+ assert.equal(value.source.palletCode, 'SOURCE')
+ value.sourceCode = 'OTHER'
+ value.showBindingMessage = () => {}
+ value.resolve = async () => assert.fail('unknown pallet must not resolve')
+ await value.confirmSourceScan()
+ assert.equal(value.source, null)
+ assert.equal(prompts, 2)
+})
+
+test('declining target confirmation does not move stock', async () => {
+ const value = context()
+ value.quantity = '1'; value.source = { palletId: '20', palletCode: 'SOURCE' }; value.targetCode = 'TARGET'
+ value.resolve = async () => { value.target = { palletId: '30', palletCode: 'TARGET' } }
+ value.confirmBindingChoice = async () => false
+ value.bind = async () => assert.fail('cancelled target must not bind')
+ await value.confirmTargetAndBind()
+ assert.equal(value.target, null)
+})
+
+test('simplified split automatically fills a batch and counts saved bindings without repeats', async () => {
+ const value = context()
+ value.task.items = ['A', 'B', 'C'].map(cartonMark => ({ cartonMark, operationType: 'CARTON_LABEL', expectedPackages: 1, relabelledPackages: 1, destinationFbaWarehouseCode: 'ONT8' }))
+ value.task.history = [{ action: 'PALLET_BIND', detail: JSON.stringify({ cartonMarks: ['A'], targetPallets: ['OLD-TARGET'] }) }]
+ assert.equal(value.requiredSplitPackages, 3)
+ assert.equal(value.splitPackages, 1)
+ value.quantity = '1'; value.marks = []; value.source = { palletId: '20', palletCode: 'SOURCE' }; value.targetCode = 'TARGET'
+ value.resolve = async () => { value.target = { palletId: '30', palletCode: 'TARGET' } }
+ value.bind = async () => { assert.deepEqual(value.marks, ['B']); assert.equal(value.readyToBind, true) }
+ await value.confirmTargetAndBind()
+ assert.doesNotMatch(template, /按箱数填入本批换标箱|选择本批新目的仓|本批确认|@tap="toggleMark/)
+ value.task.history.push({ action: 'REWORK_ARCHIVE', detail: '{}' })
+ assert.equal(value.splitPackages, 0)
+})
+
test('missing source scan shows modal and never submits a binding', async () => {
const value = context(); value.quantity = '1'; value.fillBatch(); value.targetCode = 'TARGET'
let message = ''
diff --git a/tests/return-receipt-page.test.mjs b/tests/return-receipt-page.test.mjs
index 224b108..3bf41e6 100644
--- a/tests/return-receipt-page.test.mjs
+++ b/tests/return-receipt-page.test.mjs
@@ -70,7 +70,17 @@ test('parcel signing allows an empty code but still rejects quantities above the
await page.confirmReceipt()
assert.equal(calls.length, 1)
assert.equal(calls[0].trackingNo, undefined)
- assert.equal(calls[0].unitCode, '')
+ assert.equal(calls[0].unitCode, undefined)
+ assert.equal(calls[0].palletCode, undefined)
+})
+
+test('parcel waybill is submitted as trackingNo and leaves unitCode for the server', async () => {
+ const calls = []
+ const page = setup(async payload => { calls.push(payload); return { unitType: 'PARCEL' } })
+ page.unitType = 'PARCEL'; page.unitCode = ' sf561626 '; page.trackingNo = 'stale-from-pallet'
+ await page.confirmReceipt()
+ assert.equal(calls[0].trackingNo, 'sf561626')
+ assert.equal(calls[0].unitCode, undefined)
assert.equal(calls[0].palletCode, undefined)
})
From 4f56d97f5e4142ff87e1b0d30c95be8363740b89 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 11 Oct 2026 21:48:44 +0800
Subject: [PATCH 8/8] fix(pda): keep pallet label actions readable on handheld
screens
---
src/pages/warehouse-transfer/warehouse-transfer.vue | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/warehouse-transfer/warehouse-transfer.vue b/src/pages/warehouse-transfer/warehouse-transfer.vue
index 259c77b..188d5aa 100644
--- a/src/pages/warehouse-transfer/warehouse-transfer.vue
+++ b/src/pages/warehouse-transfer/warehouse-transfer.vue
@@ -305,6 +305,7 @@ export default {
.take-photo-button[disabled]{color:#9aa7b5;background:#e7ecf2;opacity:1}
.print-card{box-sizing:border-box;margin-bottom:11px;padding:14px;border:1px solid #b9d3f4;border-radius:13px;background:#f8fbff}.print-row button{width:auto;margin:0;padding:0 10px;color:#135ee8;border:1px solid #9fc0ef;border-radius:8px;background:#fff}.print-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-top:1px solid #dce8f5;color:#728196;font-size:9px}.print-code{display:block;color:#17314f;font-size:13px;font-weight:850}
.generate-button{width:100%;margin-top:12px;color:#fff;background:#135ee8}.generated-card{display:flex;align-items:center;justify-content:space-between;margin-top:12px;padding:10px;border-radius:9px;background:#edf5ff}.generated-card button,.bind-panel>button{width:auto;margin:0;color:#135ee8;background:#fff}.bind-panel{margin-top:10px}.bound-state{margin-top:10px;padding:9px;border-radius:8px;color:#08705f;font-weight:800;background:#dff5ed}
+.generated-card,.print-row{gap:10px}.generated-card>view,.print-row>view{flex:1;min-width:0;overflow-wrap:anywhere}.generated-card button,.print-row button{flex-shrink:0;white-space:nowrap;padding:0 10px}
.operation-detail-heading{display:flex;align-items:flex-start;gap:10px;margin:0 2px 12px;padding:2px 0}.back-to-list{width:auto;height:30px;margin:0;padding:0 9px;border:1px solid #c8d8ec;border-radius:8px;color:#135ee8;font-size:10px;line-height:30px;background:#f8fbff}.operation-detail-heading>view{min-width:0}.operation-detail-title{display:block;color:#243951;font-size:15px;font-weight:850}.operation-detail-subtitle{display:block;margin-top:3px;color:#8794a4;font-size:9px;line-height:14px}
.collapse-button{width:30px;height:28px;margin:0;padding:0;color:#135ee8;font-size:16px;line-height:28px;background:#edf5ff}
.receipt-comparison{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-bottom:9px}.receipt-comparison view{padding:9px;border-radius:9px;text-align:center;background:#fff}.receipt-comparison strong,.receipt-comparison text{display:block}.receipt-comparison strong{color:#17314f;font-size:15px}.receipt-comparison text{margin-top:2px;color:#8390a1;font-size:8px}.receipt-comparison .difference{border:1px solid #e3a044;background:#fff4df}.receipt-comparison .difference strong,.receipt-comparison .difference text{color:#a35b0c;font-weight:800}