fix(pda): 修复收货旧扫码状态和装车完成跳转
This commit was merged in pull request #14.
This commit is contained in:
@@ -53,13 +53,14 @@ export default {
|
||||
const image = await readLoadingPhoto(this.number, photo); this.ensureScope(); this.images[photo.fileId] = image
|
||||
}))
|
||||
},
|
||||
async refreshFacts() {
|
||||
async refreshFacts({ previewCompleted = true } = {}) {
|
||||
this.needsRefresh = true
|
||||
const value = await getLoadingDocuments(this.number); this.ensureScope()
|
||||
this.snapshot = value; this.needsRefresh = false; await this.previews()
|
||||
this.snapshot = value; this.needsRefresh = false
|
||||
if (!value.complete || previewCompleted) await this.previews()
|
||||
},
|
||||
async load() { await this.run(() => this.refreshFacts()) },
|
||||
async refresh() { await this.run(async () => { await this.refreshFacts(); return true }, true) },
|
||||
async refresh() { await this.run(async () => { await this.refreshFacts({ previewCompleted: false }); return true }, true) },
|
||||
async choose(slot, replaceFileId = null) {
|
||||
if (this.busy || !this.editable) return
|
||||
const reviewRound = this.snapshot.reviewRound
|
||||
@@ -85,12 +86,14 @@ export default {
|
||||
} catch (cause) {
|
||||
// 保存可能已成功;只重读最新事实,失败时必须先刷新再继续。
|
||||
try {
|
||||
await this.refreshFacts()
|
||||
await this.refreshFacts({ previewCompleted: false })
|
||||
reconciled = this.snapshot.recordVersion !== latest.recordVersion || this.snapshot.dispatchRecordVersion !== latest.dispatchRecordVersion || this.snapshot.stage !== latest.stage
|
||||
} catch { /* 保留原错误与刷新入口。 */ }
|
||||
throw cause
|
||||
}
|
||||
saved++; await this.previews()
|
||||
saved++
|
||||
// 资料齐全后返回列表,跳转只依赖保存回执,避免被预览下载阻塞。
|
||||
if (!this.snapshot.complete) await this.previews()
|
||||
if (!this.editable) break
|
||||
}
|
||||
uni.showToast({ title: this.snapshot.stage === 'DELIVERING' && this.snapshot.complete ? '资料齐全,已进入派送中' : `已保存${saved}张照片`, icon: 'none' })
|
||||
|
||||
@@ -265,7 +265,46 @@
|
||||
async previewPhoto(index) { const photo = this.photoSlots[index]; if (!photo) return; try { const path = await restoreReceivingPhoto(photo, downloadCurrentUnloadPhoto); if (path) uni.previewImage({ urls: [path], current: path }) } catch (error) { this.error = error } },
|
||||
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', async () => this.validateReceivingWarehouse(await signCurrentReceiving({ photos, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} },
|
||||
clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = ''; receiving.activeScanTarget = 'carton'; this.scheduleScanDrain() },
|
||||
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark || !this._pageActive) return; const scanEpoch = this._scanEpoch || 0; if (this.busy) return this.handleHardwareScan(cartonMark); try { const outcome = await this.run('处理货物码', async () => { const value = await resolveCurrentCarton(cartonMark); if (value.operationMode !== 'DROPSHIP_PALLET') return { value }; if (!this._pageActive || scanEpoch !== (this._scanEpoch || 0)) throw new Error('页面已离开,请返回后重新扫码确认收货'); const payload = { sourcePalletNo: value.sourcePalletNo || cartonMark }; const key = idempotencyKey('whole-pallet-receipt', { containerNo: this.current?.containerNo, ...payload }); const result = await receiveCurrentWholePallet({ ...payload, idempotencyKey: key.value }); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); releaseIdempotencyKey(key.signature); return { value, result, current } }); if (outcome.result) { receiving.lastReceipt = outcome.result; await this.applyCurrentReceiving(outcome.current); this.clearReceiptContext(); playSuccessSound(); return } receiving.resolvedCarton = outcome.value; receiving.resolvedPallet = null; this.cartonInput = outcome.value.cartonMark; this.palletInput = ''; this.packageCount = ''; playSuccessSound() } catch (error) { console.error('[Overseas PDA cargo] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
|
||||
async resolveCarton(raw) {
|
||||
const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim()
|
||||
if (!cartonMark || !this._pageActive) return
|
||||
if (this.busy) return this.handleHardwareScan(cartonMark)
|
||||
const current = this.current, scanEpoch = this._scanEpoch || 0
|
||||
const operatorId = session.currentUser?.id, warehouseId = session.currentUser?.warehouseId, tenantId = session.tenantId
|
||||
const sameContext = () => this.current === current && session.currentUser?.id === operatorId
|
||||
&& session.currentUser?.warehouseId === warehouseId && session.tenantId === tenantId
|
||||
const isCurrent = () => sameContext() && this._pageActive && scanEpoch === (this._scanEpoch || 0)
|
||||
&& this.cartonInput.trim() === cartonMark
|
||||
// 新箱码必须重新解析和验托,失败时也不能沿用上一箱的可提交结果。
|
||||
receiving.resolvedCarton = null; receiving.resolvedPallet = null
|
||||
this.cartonInput = cartonMark; this.palletInput = ''; this.packageCount = ''
|
||||
let receiptSent = false
|
||||
const canApply = () => receiptSent ? sameContext() : isCurrent()
|
||||
try {
|
||||
const outcome = await this.run('处理货物码', async () => {
|
||||
const value = await resolveCurrentCarton(cartonMark)
|
||||
if (!isCurrent()) return null
|
||||
if (value.operationMode !== 'DROPSHIP_PALLET') return { value }
|
||||
const payload = { sourcePalletNo: value.sourcePalletNo || cartonMark }
|
||||
const key = idempotencyKey('whole-pallet-receipt', { containerNo: current?.containerNo, ...payload })
|
||||
// 已发送的整托实收须保留同一柜、同一身份的成功回执,页面短暂隐藏后仍可恢复。
|
||||
receiptSent = true
|
||||
const result = await receiveCurrentWholePallet({ ...payload, idempotencyKey: key.value })
|
||||
if (!sameContext()) return null
|
||||
const updated = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话')
|
||||
releaseIdempotencyKey(key.signature)
|
||||
return { value, result, current: updated }
|
||||
}, canApply)
|
||||
if (!outcome || !canApply()) return
|
||||
if (outcome.result) {
|
||||
receiving.lastReceipt = outcome.result; await this.applyCurrentReceiving(outcome.current)
|
||||
this.clearReceiptContext(); playSuccessSound(); return
|
||||
}
|
||||
receiving.resolvedCarton = outcome.value; this.cartonInput = outcome.value.cartonMark; playSuccessSound()
|
||||
} catch (error) {
|
||||
console.error('[Overseas PDA cargo] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId })
|
||||
}
|
||||
},
|
||||
async resolvePallet(raw) {
|
||||
const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim()
|
||||
if (!palletCode || !this.activeInbound || this.busy) return
|
||||
|
||||
@@ -45,6 +45,49 @@ test('saving the final loading photo reports the confirmed complete snapshot aft
|
||||
assert.equal(uploads, 1)
|
||||
})
|
||||
|
||||
test('a completed photo save notifies navigation without waiting for image downloads', async () => {
|
||||
const completed = data({ recordVersion: '1', complete: true, stage: 'DELIVERING', editable: true,
|
||||
photos: loadingPhotoSlots.map((slot, index) => ({ ...photo, slot, fileId: String(1001 + index) })) })
|
||||
let finishDownload, downloads = 0
|
||||
const pending = new Promise(resolve => { finishDownload = resolve })
|
||||
const { state, events } = component({ bindLoadingPhoto: async () => completed,
|
||||
readLoadingPhoto: async () => { downloads++; await pending; return 'image' } })
|
||||
await state.load()
|
||||
const choosing = state.choose(photo.slot)
|
||||
try {
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
assert.equal(downloads, 0)
|
||||
assert.deepEqual(events.at(-1), ['saved', completed])
|
||||
assert.equal(state.busy, false)
|
||||
} finally { finishDownload(); await choosing }
|
||||
})
|
||||
|
||||
test('refreshing or reconciling completed photos reports completion without preview downloads', async t => {
|
||||
for (const action of ['refresh', 'reconcile']) await t.test(action, async () => {
|
||||
let latest = data(), binds = 0, downloads = 0
|
||||
const completed = data({ recordVersion: '1', complete: true, stage: 'DELIVERING',
|
||||
photos: loadingPhotoSlots.map((slot, index) => ({ ...photo, slot, fileId: String(1001 + index) })) })
|
||||
const { state, events } = component({ getLoadingDocuments: async () => latest,
|
||||
readLoadingPhoto: async () => { downloads++; return 'image' },
|
||||
bindLoadingPhoto: async () => { binds++; latest = completed; throw new Error('保存回执超时') } })
|
||||
await state.load()
|
||||
if (action === 'refresh') { latest = completed; await state.refresh() }
|
||||
else await state.choose(photo.slot)
|
||||
assert.equal(downloads, 0); assert.equal(state.busy, false)
|
||||
assert.deepEqual(events.at(-1), ['saved', completed])
|
||||
assert.equal(binds, action === 'reconcile' ? 1 : 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('opening completed photo history still loads its previews', async () => {
|
||||
let downloads = 0
|
||||
const { state, events } = component({ getLoadingDocuments: async () => data({ complete: true, photos: [photo] }),
|
||||
readLoadingPhoto: async () => { downloads++; return 'history-image' } })
|
||||
await state.load()
|
||||
assert.equal(downloads, 1); assert.equal(state.images[photo.fileId], 'history-image')
|
||||
assert.equal(events.some(event => event[0] === 'saved'), false)
|
||||
})
|
||||
|
||||
test('refresh reads all photos saved by PC and refreshes the task stage without another write', async () => {
|
||||
let latest = data({ photos: [photo] }), writes = 0, fail = false
|
||||
const { state, events } = component({ getLoadingDocuments: async () => { if (fail) throw new Error('刷新失败'); return latest },
|
||||
|
||||
@@ -50,6 +50,74 @@ test('receiving broadcast targets use larger tap areas and show an inactive sele
|
||||
assert.match(input, /active \? '当前扫码' : '点按选中'/)
|
||||
})
|
||||
|
||||
test('resolving a new carton invalidates the previous receipt even when the new code fails', async t => {
|
||||
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
|
||||
store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#1', destinationType: 'OVERSEAS' }
|
||||
store.receiving.resolvedPallet = { palletCode: 'PAL-A', compatible: true, version: 1 }
|
||||
let writes = 0
|
||||
const { vm } = page({ resolveCurrentCarton: async () => { throw new Error('箱码不属于当前柜') },
|
||||
receiveCurrentCarton: async () => { writes++ } })
|
||||
t.after(() => vm.suspendReceiving())
|
||||
vm.cartonInput = 'IN-A#1'; vm.palletInput = 'PAL-A'
|
||||
assert.equal(vm.canSubmitReceipt, true)
|
||||
await vm.resolveCarton('WRONG-CARTON')
|
||||
assert.equal(vm.resolvedCarton, null); assert.equal(vm.resolvedPallet, null)
|
||||
assert.equal(vm.palletInput, ''); assert.equal(vm.canSubmitReceipt, false)
|
||||
await vm.submitReceipt()
|
||||
assert.equal(writes, 0)
|
||||
})
|
||||
|
||||
test('late carton resolution cannot overwrite edited input or another receiving context', async t => {
|
||||
for (const [name, change] of [
|
||||
['input', ({ vm }) => { vm.cartonInput = 'IN-B#1' }],
|
||||
['container', () => store.setCurrentReceiving(sessionValue({ containerNo: 'CAB-B' }))],
|
||||
['operator', ({ deps }) => { deps.session.currentUser.id = 'another-operator' }],
|
||||
['warehouse', ({ deps }) => { deps.session.currentUser.warehouseId = '901' }],
|
||||
['hide and return', ({ vm, definition }) => { definition.onHide.call(vm); definition.onShow.call(vm) }],
|
||||
]) await t.test(name, async t => {
|
||||
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
|
||||
const pending = defer(), toasts = []; let successes = 0
|
||||
const context = page({ resolveCurrentCarton: () => pending.promise, playSuccessSound: () => successes++ })
|
||||
const { vm } = context
|
||||
vm.bootstrap = async () => {}; vm.showToast = (message, type) => toasts.push({ message, type })
|
||||
t.after(() => vm.suspendReceiving())
|
||||
const resolving = vm.resolveCarton('IN-A#1')
|
||||
change(context)
|
||||
pending.resolve({ cartonMark: 'IN-A#1', inboundCode: 'IN-A', destinationType: 'FBA' })
|
||||
await resolving
|
||||
assert.equal(vm.resolvedCarton, null); assert.equal(vm.busy, false)
|
||||
assert.equal(successes, 0); assert.deepEqual(toasts, [])
|
||||
if (name === 'input') assert.equal(vm.cartonInput, 'IN-B#1')
|
||||
})
|
||||
})
|
||||
|
||||
test('obsolete carton errors cannot replace current input or feedback', async t => {
|
||||
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
|
||||
const pending = defer(), toasts = []
|
||||
const { vm } = page({ resolveCurrentCarton: () => pending.promise })
|
||||
t.after(() => vm.suspendReceiving())
|
||||
vm.showToast = (message, type) => toasts.push({ message, type })
|
||||
const resolving = vm.resolveCarton('IN-A#1')
|
||||
vm.cartonInput = 'IN-B#1'
|
||||
pending.reject(new Error('旧箱码解析失败'))
|
||||
await resolving
|
||||
assert.equal(vm.cartonInput, 'IN-B#1'); assert.equal(vm.error, null)
|
||||
assert.equal(vm.busy, false); assert.deepEqual(toasts, [])
|
||||
})
|
||||
|
||||
test('dropship resolution cannot submit after its operator changes', async t => {
|
||||
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
|
||||
const pending = defer(); let writes = 0
|
||||
const { vm, deps } = page({ resolveCurrentCarton: () => pending.promise,
|
||||
receiveCurrentWholePallet: async () => { writes++ } })
|
||||
t.after(() => vm.suspendReceiving())
|
||||
const resolving = vm.resolveCarton('DP-A')
|
||||
deps.session.currentUser.id = 'new-operator'
|
||||
pending.resolve({ operationMode: 'DROPSHIP_PALLET', sourcePalletNo: 'DP-A' })
|
||||
await resolving
|
||||
assert.equal(writes, 0); assert.equal(vm.resolvedCarton, null)
|
||||
})
|
||||
|
||||
test('camera resume retains all unsigned uploads by role and clears them across session or identity changes', async () => {
|
||||
store.resetReceivingSession()
|
||||
const unsigned = sessionValue({ status: 'WAIT_SIGN', photos: [], domesticLoadingPhotos: [] })
|
||||
|
||||
Reference in New Issue
Block a user