优化
This commit is contained in:
@@ -81,6 +81,15 @@
|
||||
</view>
|
||||
</template><view v-if="successMessage" class="fulfillment-success" role="status">{{ successMessage }}</view>
|
||||
</view></scroll-view>
|
||||
<view v-if="scanNotice" class="loading-overlay scan-notice-overlay">
|
||||
<view class="loading-dialog scan-notice-dialog" role="status" aria-live="polite">
|
||||
<text class="loading-dialog-title">托盘扫描成功</text>
|
||||
<text class="scan-notice-pallet">{{ scanNotice.palletNo }}</text>
|
||||
<text class="scan-notice-label">本次可拣箱数</text>
|
||||
<text class="scan-notice-count">{{ scanNotice.packages }}<text class="loading-count-unit"> 箱</text></text>
|
||||
<text class="scan-notice-hint">3 秒后自动关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingPreview" class="loading-overlay">
|
||||
<view class="loading-dialog" role="dialog" aria-modal="true" aria-labelledby="loading-confirm-title">
|
||||
<view class="loading-dialog-heading"><text id="loading-confirm-title" class="loading-dialog-title">确认装车</text></view>
|
||||
@@ -103,7 +112,7 @@ import { toDispatchError } from '@/utils/dispatch-error.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput, DispatchHandoverStatus, DispatchLoadingDocuments },
|
||||
data() { return { user: session.currentUser, tasks: [], current: null, activeTab: 'picking', scanCode: '', loading: false, requestBusy: false, photoBusy: false, error: null, successMessage: '', bootstrapped: false, resumeOnEntry: false, generation: 0, loadingPreview: null, loadingRows: [], sourceChoices: [], choiceCartonCode: '', reviewReason: '' } },
|
||||
data() { return { user: session.currentUser, tasks: [], current: null, activeTab: 'picking', scanCode: '', scanNotice: null, loading: false, requestBusy: false, photoBusy: false, error: null, successMessage: '', bootstrapped: false, resumeOnEntry: false, generation: 0, loadingPreview: null, loadingRows: [], sourceChoices: [], choiceCartonCode: '', reviewReason: '' } },
|
||||
computed: {
|
||||
busy() { return this.requestBusy || this.photoBusy },
|
||||
recentTaskNo() { return fulfillment.dispatch.currentNo || '' }, packages() { return fulfillment.dispatch.packages },
|
||||
@@ -121,9 +130,15 @@ export default {
|
||||
},
|
||||
onLoad(options) { this.resumeOnEntry = options?.resume === '1' },
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (!this.bootstrapped) { this.bootstrapped = true; if (this.resumeOnEntry && fulfillment.dispatch.currentNo) this.openTask(fulfillment.dispatch.currentNo); else this.loadTasks() } else if (this.current?.dispatchNo) this.openTask(this.current.dispatchNo); else this.loadTasks() },
|
||||
onHide() { this.stopScanner(); this.generation++ }, beforeUnmount() { this.stopScanner(); this.generation++ },
|
||||
onHide() { this.stopScanner(); this.clearScanNotice(); this.generation++ }, beforeUnmount() { this.stopScanner(); this.clearScanNotice(); this.generation++ },
|
||||
methods: {
|
||||
selectTab(tab) { if (this.busy || this.loading || !['picking', 'photos'].includes(tab) || (tab === 'photos' && !this.canShowPhotos)) return; this.activeTab = tab },
|
||||
clearScanNotice() { clearTimeout(this._scanNoticeTimer); this._scanNoticeTimer = null; this.scanNotice = null },
|
||||
showScanNotice(item) {
|
||||
this.clearScanNotice()
|
||||
this.scanNotice = { palletNo: item.palletNo, packages: this.selectedRemaining }
|
||||
this._scanNoticeTimer = setTimeout(() => this.clearScanNotice(), 3000)
|
||||
},
|
||||
selectTab(tab) { if (this.busy || this.loading || !['picking', 'photos'].includes(tab) || (tab === 'photos' && !this.canShowPhotos)) return; this.clearScanNotice(); this.activeTab = tab },
|
||||
gate(action) { return this.current?.actions?.[action]?.allowed === true },
|
||||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: value => this.handleScan(value), onError: error => this.setError(error, '扫码广播注册失败') }) }, stopScanner() { this._stopScanner?.(); this._stopScanner = null },
|
||||
updatePackages(event) { const value = Number(event.detail.value); updateFulfillmentDraft('dispatch', { packages: Number.isFinite(value) ? value : 0 }) },
|
||||
@@ -133,6 +148,7 @@ export default {
|
||||
handleScan(raw) {
|
||||
const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim()
|
||||
if (!code || this.busy || this.loading || this.activeTab !== 'picking') return
|
||||
this.clearScanNotice()
|
||||
this.error = null; this.successMessage = ''; this.sourceChoices = []; this.choiceCartonCode = ''
|
||||
updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', sourceKey: '', 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)
|
||||
@@ -151,6 +167,7 @@ export default {
|
||||
this.selectPallet(item)
|
||||
if (fulfillment.dispatch.dispatchItemId !== item.dispatchItemId) return
|
||||
if (cartonCode) updateFulfillmentDraft('dispatch', { cartonCode, packages: 1 })
|
||||
else this.showScanNotice(item)
|
||||
this.scanCode = ''; playSuccessSound()
|
||||
},
|
||||
pickablePackages(item) {
|
||||
@@ -206,8 +223,8 @@ export default {
|
||||
})
|
||||
},
|
||||
async retry() { if (this.current?.dispatchNo) await this.openTask(this.current.dispatchNo); else await this.loadTasks() },
|
||||
async run(work, loading = false) { if (this.busy || this.loading) return; this.error = null; this.successMessage = ''; if (loading) this.loading = true; else this.requestBusy = true; try { await work() } catch (cause) { this.setError(cause, '排车操作失败') } finally { this.loading = false; this.requestBusy = false } },
|
||||
setError(cause, fallback) { this.error = toDispatchError(cause, fallback); playErrorSound() },
|
||||
async run(work, loading = false) { if (this.busy || this.loading) return; this.clearScanNotice(); this.error = null; this.successMessage = ''; if (loading) this.loading = true; else this.requestBusy = true; try { await work() } catch (cause) { this.setError(cause, '排车操作失败') } finally { this.loading = false; this.requestBusy = false } },
|
||||
setError(cause, fallback) { this.clearScanNotice(); this.error = toDispatchError(cause, fallback); playErrorSound() },
|
||||
stageLabel(task) { if (task.status === 'CANCELLED') return '已取消'; if (task.stage === 'BEFORE_LOADING') { if (task.actions?.LOADING_REVIEW?.allowed === true) return '待理货审核'; if ([task.reviews?.dispatchReview, task.reviews?.loadingReview].some(review => review?.status === 'REJECTED')) return '已退回客服处理' }; return ({ BEFORE_LOADING: '装车前', LOADING: '装车中', AFTER_LOADING: '装车后', DELIVERING: '派送中', DELIVERED: '派送完成' })[task.stage] || '状态待核实' },
|
||||
reviewTime(value) { if (!value) return '未填写'; const date = new Date(new Date(value).getTime() + 8 * 60 * 60 * 1000); return Number.isNaN(date.getTime()) ? '时间无效' : date.toISOString().slice(0, 16).replace('T', ' ') },
|
||||
reviewLabel(review) { return ({ NOT_SUBMITTED: '未提交', PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回' })[review?.status] || '待核对' },
|
||||
@@ -273,5 +290,11 @@ export default {
|
||||
.loading-cancel { color: #52657d; background: #f0f3f8; }
|
||||
.loading-confirm { color: #fff; background: #1764ec; }
|
||||
.loading-dialog-actions button[disabled] { opacity: .5; }
|
||||
.scan-notice-overlay { pointer-events: none; }
|
||||
.scan-notice-dialog { text-align: center; }
|
||||
.scan-notice-pallet { display: block; margin-top: 20rpx; color: #243c58; font-size: 30rpx; overflow-wrap: anywhere; }
|
||||
.scan-notice-label { display: block; margin-top: 28rpx; color: #536982; font-size: 28rpx; }
|
||||
.scan-notice-count { display: block; margin-top: 12rpx; color: #1764ec; font-size: 72rpx; font-weight: 700; line-height: 1.3; }
|
||||
.scan-notice-hint { display: block; margin-top: 24rpx; color: #72839b; font-size: 24rpx; }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -43,6 +43,43 @@ function page(dependencies = {}) {
|
||||
}
|
||||
const proof = (command, version = 1) => ({ dispatchNo: command.dispatchNo, action: command.action, idempotencyKey: command.body.idempotencyKey, recordVersion: version, stage: command.action === 'LOADING_COMPLETE' ? 'AFTER_LOADING' : 'LOADING', result: { confirmed: true } })
|
||||
|
||||
test('pallet scan notice reports the current pickable quantity and closes after three seconds without submitting', t => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
storage(); let writes = 0, sounds = 0
|
||||
const state = page({ pickDispatchItem: async () => { writes++ }, playSuccessSound: () => { sounds++ } })
|
||||
state.current = task({ items: [item({ scheduledPackages: 12, pickedPackages: 4, positions: [{ positionId: 'position-1', availablePackages: 6, allocatedPackages: 0, pickedPackages: 0 }] })] })
|
||||
state.handleScan('PALLET-1')
|
||||
assert.deepEqual(state.scanNotice, { palletNo: 'PALLET-1', packages: 6 })
|
||||
assert.equal(sounds, 1)
|
||||
t.mock.timers.tick(2999); assert.ok(state.scanNotice)
|
||||
t.mock.timers.tick(1); assert.equal(state.scanNotice, null)
|
||||
assert.equal(writes, 0); assert.equal(state.packages, 6)
|
||||
})
|
||||
|
||||
test('consecutive pallet scans replace the notice and restart its full display time', t => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
storage(); const state = page()
|
||||
state.current = task({ items: [item(), item({ dispatchItemId: 'item-2', sourceKey: 'source-2', palletNo: 'PALLET-2', scheduledPackages: 1 })] })
|
||||
state.handleScan('PALLET-1'); t.mock.timers.tick(2000)
|
||||
state.handleScan('PALLET-2')
|
||||
assert.deepEqual(state.scanNotice, { palletNo: 'PALLET-2', packages: 1 })
|
||||
t.mock.timers.tick(1000); assert.ok(state.scanNotice)
|
||||
t.mock.timers.tick(2000); assert.equal(state.scanNotice, null)
|
||||
})
|
||||
|
||||
test('scan errors, carton scans and switching tabs cannot retain a stale pallet notice', t => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
storage(); const state = page()
|
||||
state.handleScan('PALLET-1'); state.handleScan('UNKNOWN')
|
||||
assert.equal(state.scanNotice, null); assert.ok(state.error)
|
||||
state.handleScan('PALLET-1'); state.selectTab('photos')
|
||||
assert.equal(state.scanNotice, null)
|
||||
state.selectTab('picking'); state.handleScan('PALLET-1')
|
||||
state.current = task({ items: [item({ returnCartons: [{ returnCartonId: 'rc-1', cartonMark: 'RC-1', status: 'OUTBOUND_PENDING', productQuantity: 5 }] })] })
|
||||
state.handleScan('RC-1'); assert.equal(state.scanNotice, null)
|
||||
t.mock.timers.tick(3000); assert.equal(state.scanNotice, null)
|
||||
})
|
||||
|
||||
test('real picking requires the native gate, physical source and current stage; empty or merely counted tasks cannot complete loading', async () => {
|
||||
storage(); const state = page(); state.selectPallet(state.current.items[0]); assert.equal(state.canPick, true)
|
||||
for (const allowed of [false, 'true', undefined]) { state.current.actions.PICK.allowed = allowed; assert.equal(state.canPick, false) }
|
||||
|
||||
Reference in New Issue
Block a user