diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js index 95ca579..e7e0925 100644 --- a/src/api/receiving-adapter.js +++ b/src/api/receiving-adapter.js @@ -118,6 +118,7 @@ export function toReceivingError(error, fallback = '海外仓作业失败') { const receivingMessages = { 'Carton mark must contain a non-empty inbound code prefix': '请扫描箱码或国内DP码', 'Pallet code is not part of the current FBA destination group': '托盘不属于当前目的仓,请重扫', + 'Pallet code is not reserved for this inbound code': '该托盘未预留给当前入仓码,请扫描本票已预留托盘', 'Reserved pallet binding is incomplete': '预留托盘资料不完整,请联系管理员检查入仓计划', 'Reserved pallet customer binding is invalid': '预留托盘与当前客户不匹配,请联系管理员检查入仓计划', 'Scanned pallet does not match the reserved inbound plan resource': '扫描托盘与入仓计划预留资源不一致,请扫描已预留的托盘', diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 847bda6..4246b3e 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -191,7 +191,17 @@ photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.errorMessage || (photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'); return '' }, startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null }, handleHardwareScan(value) { if (!this._pageActive || this.loading) return; const scan = { value: String(value || '').trim(), target: this.activeTarget }; if (!scan.value) return; if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.busy || this.activeTarget === 'quantity') { if (this.scanQueue.length >= 100) return this.showToast('扫码队列已满,请先处理当前托盘', 'warning'); this.scanQueue.push(scan); return this.showToast(`已排队 ${this.scanQueue.length} 个扫码`, 'warning') } return this.dispatchHardwareScan(scan) }, - dispatchHardwareScan(scan) { if (!this._pageActive || this.loading) return; const palletWorkflowActive = Boolean(this.activeInbound || this.resolvedPallet); const target = palletWorkflowActive ? 'pallet' : scan.target; console.info('[Overseas PDA scan] dispatch', { value: scan.value, target, queuedTarget: scan.target }); if (target === 'pallet') { this.palletInput = scan.value; return this.resolvePallet(scan.value) } this.cartonInput = scan.value; return this.resolveCarton(scan.value) }, + dispatchHardwareScan(scan) { + if (!this._pageActive || this.loading) return + // 排队扫码按当前步骤处理,避免沿用上一请求开始时的扫描目标。 + if (this.activeInbound && this.activeTarget === 'pallet') { + this.palletInput = scan.value + return this.resolvePallet(scan.value) + } + if (this.canSubmitReceipt) return this.showToast('请先确认当前箱收货,再扫描下一箱', 'warning') + this.cartonInput = scan.value + return this.resolveCarton(scan.value) + }, drainScanQueue() { if (!this._pageActive || this.loading || this.busy || this.activeTarget === 'quantity' || !this.scanQueue.length || this.phase !== 'receive') return; const next = this.scanQueue.shift(); this.dispatchHardwareScan(next) }, suspendReceiving() { this._pageActive = false; this._scanEpoch = (this._scanEpoch || 0) + 1; this._resumeReceiving = true; clearTimeout(this._drainTimer); clearTimeout(this._toastTimer); this.scanQueue = []; this.stopScanner() }, scheduleScanDrain() { clearTimeout(this._drainTimer); if (!this._pageActive) return; const epoch = this._scanEpoch || 0; this._drainTimer = setTimeout(() => { if (this._pageActive && epoch === (this._scanEpoch || 0)) this.drainScanQueue() }, 0) }, diff --git a/tests/receiving-lifecycle.test.mjs b/tests/receiving-lifecycle.test.mjs index c17812c..2833698 100644 --- a/tests/receiving-lifecycle.test.mjs +++ b/tests/receiving-lifecycle.test.mjs @@ -64,6 +64,30 @@ test('editing and rejecting a pallet cannot submit using the previous successful assert.equal(writes, 0) }) +test('queued pallet scan follows the current step while a pending carton requires confirmation', t => { + store.resetReceivingSession(); store.setCurrentReceiving(sessionValue()) + store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#1', destinationType: 'OVERSEAS' } + const { vm } = page() + t.after(() => vm.suspendReceiving()) + const calls = [], toasts = [] + vm.resolvePallet = value => calls.push(['pallet', value]) + vm.resolveCarton = value => calls.push(['carton', value]) + vm.showToast = message => toasts.push(message) + vm.activeTarget = 'pallet' + vm.dispatchHardwareScan({ value: 'PAL-A', target: 'carton' }) + assert.deepEqual(calls, [['pallet', 'PAL-A']]) + + store.receiving.resolvedPallet = { palletCode: 'PAL-A', compatible: true } + vm.palletInput = 'PAL-A'; vm.activeTarget = 'carton' + vm.dispatchHardwareScan({ value: 'IN-A#2', target: 'carton' }) + assert.deepEqual(calls, [['pallet', 'PAL-A']]) + assert.deepEqual(toasts, ['请先确认当前箱收货,再扫描下一箱']) + assert.equal(vm.activeInbound.cartonMark, 'IN-A#1') + vm.clearReceiptContext() + vm.dispatchHardwareScan({ value: 'IN-A#2', target: 'pallet' }) + assert.deepEqual(calls, [['pallet', 'PAL-A'], ['carton', 'IN-A#2']]) +}) + test('an incompatible pallet never reports success and a corrected scan can be validated normally', async t => { store.resetReceivingSession(); store.setCurrentReceiving(sessionValue()) store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#22-1', destinationType: 'FBA' }