From 95078f750e4d0035d274b7ba8e9af409794f9256 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Fri, 9 Oct 2026 15:54:21 +0800
Subject: [PATCH] fix(pda): prevent stale and ambiguous dispatch scan
selections
---
src/pages/dispatch/dispatch.vue | 16 ++++++----
src/pages/return-receipt/return-receipt.vue | 2 +-
tests/fulfillment-workflows.test.mjs | 33 ++++++++++++++++++++-
3 files changed, 43 insertions(+), 8 deletions(-)
diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue
index 033ed8a..723da3c 100644
--- a/src/pages/dispatch/dispatch.vue
+++ b/src/pages/dispatch/dispatch.vue
@@ -5,14 +5,14 @@
排车拣货与发车
- 客户与当前海外仓由登录令牌和服务端确定;扫描排车单号或任务中的托盘码开始作业
+ 扫描排车单号打开任务,再扫描任务中的托盘码进行拣货
当前任务排车拣货详情
- 网络失败不会清空草稿,再次提交会沿用同一幂等键。
+ 网络中断会保留当前作业,恢复后可重试。
正在加载排车任务…
@@ -21,7 +21,7 @@
错误码:{{ error.code }}
requestId:{{ error.requestId }}
traceId:{{ error.traceId }}
- 网络连接失败,未完成作业与幂等键已保留。
+ 网络连接失败,当前作业已保留,请恢复网络后重试。
@@ -154,17 +154,21 @@
const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return
console.info('[Overseas PDA dispatch] scan', { code, task: this.current?.dispatchNo || '' })
this.scanCode = code
+ this.error = null; this.successMessage = ''
+ updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 })
if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code)
const cartonItem = this.current.items.find((item) => item.returnCartons?.some((carton) => carton.cartonMark === code))
if (cartonItem) {
if (cartonItem.returnCartons.find((carton) => carton.cartonMark === code)?.status !== 'OUTBOUND_PENDING') return this.setError(new Error('该箱已拣货或不可出库'), '扫码未匹配')
this.selectPallet(cartonItem); updateFulfillmentDraft('dispatch', { cartonCode: code, packages: 1 }); this.scanCode = ''; playSuccessSound(); return
}
- const item = this.current.items.find((value) => [value.palletNo, value.fbaWarehouseCode].includes(code))
- if (!item) return this.setError(new Error('扫码未匹配当前排车任务的托盘或稳定业务码'), '扫码未匹配')
+ const matches = this.current.items.filter((value) => value.palletNo === code && value.scheduledPackages > value.pickedPackages)
+ if (matches.length > 1) return this.setError(new Error('此托盘包含多条待拣明细,请选择对应货物卡片'), '请选择拣货明细')
+ const item = matches[0]
+ if (!item) return this.setError(new Error(this.current.items.some((value) => value.palletNo === code) ? '该托盘已完成拣货,请勿重复操作' : '扫码未匹配当前任务的托盘码或来源箱唛'), '扫码未匹配')
this.selectPallet(item); this.scanCode = ''; playSuccessSound()
},
- selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) },
+ selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) },
async submitPick() {
if (!this.canPick) return
const pick = this.selectedReturnCarton ? { positionId: null, cartonMark: this.selectedReturnCarton.cartonMark, packages: 1, productQuantity: this.selectedReturnCarton.productQuantity } : { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 }
diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue
index 2fd2f72..4470d52 100644
--- a/src/pages/return-receipt/return-receipt.vue
+++ b/src/pages/return-receipt/return-receipt.vue
@@ -24,7 +24,7 @@
包裹码/快递单号
托盘码({{ unitType === 'BOX' ? '选填' : '必填' }}){{ palletCheck.empty ? '空托盘,可绑定当前退货单和 SKU' : `当前单托盘,已有 ${palletCheck.skuCodes.join('、')},共 ${palletCheck.quantity} 件` }}
-
+
快递单号(选填)
diff --git a/tests/fulfillment-workflows.test.mjs b/tests/fulfillment-workflows.test.mjs
index 00d63c0..fef2092 100644
--- a/tests/fulfillment-workflows.test.mjs
+++ b/tests/fulfillment-workflows.test.mjs
@@ -219,7 +219,7 @@ test('pages expose loading empty error offline and permission diagnostics with g
assert.match(page, /正在加载/)
assert.match(page, /暂无/)
assert.match(page, /requestId[\s\S]*traceId/)
- assert.match(page, /error\.network[\s\S]*幂等键/)
+ assert.match(page, /error\.network[\s\S]*保留/)
assert.match(page, /fulfillmentIdempotencyKey/)
assert.match(page, /releaseFulfillmentKey/)
assert.match(page, /if \(!this\.can/)
@@ -297,3 +297,34 @@ test('dispatch review gate preserves the server decision without treating a stri
assert.match(source, /canPick\(\) \{ return this.current\?\.canPick === true/)
assert.match(source, /canComplete\(\) \{ return this.current\?\.canPick === true/)
})
+
+async function dispatchScanHarness(items) {
+ const source = (await read('../src/pages/dispatch/dispatch.vue')).match(/