diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index b893c0f..74cb8b0 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -282,7 +282,7 @@ - 确认提交后,以下待拣商品会被明确标记为“未拣出”,本次演示提交不可撤销。 + 确认提交后,以下待拣商品会标记为“未拣出”,并记录所选原因。 {{ item.location ? `${item.sku} · ${item.location}` : item.sku }} 另有 {{ pendingItems.length - 3 }} 件 @@ -293,7 +293,7 @@ - + @@ -571,12 +571,15 @@ return } if (this._showRefreshPromise) return this._showRefreshPromise - this._showRefreshPromise = (async () => { + const showGeneration = this.taskRequestId + const refreshPromise = (async () => { await this.refreshAvailableBoxes() + if (!this._pageVisible || showGeneration !== this.taskRequestId) return await this.syncRemoteTask() })() - try { await this._showRefreshPromise } - finally { this._showRefreshPromise = null } + this._showRefreshPromise = refreshPromise + try { await refreshPromise } + finally { if (this._showRefreshPromise === refreshPromise) this._showRefreshPromise = null } }, onHide() { this._pageVisible = false @@ -584,10 +587,11 @@ this.evidencePreviewOpening = false this.evidencePreviewReturning = true } + if (!this.cameraPicking && !this.evidencePreviewReturning) this.invalidateTaskSync() this.stopHardwareScanner() }, - onUnload() { this.stopHardwareScanner(); this.persistSession(); this.clearTimers() }, - beforeUnmount() { this.stopHardwareScanner(); this.clearTimers() }, + onUnload() { this._pageVisible = false; this.invalidateTaskSync(); this.evidenceContextId++; this.stopHardwareScanner(); this.persistSession(); this.clearTimers() }, + beforeUnmount() { this._pageVisible = false; this.invalidateTaskSync(); this.stopHardwareScanner(); this.clearTimers() }, onBackPress() { if (this.activeOverlay) { this.closeOverlay(); return true } if (this.boxDropdownOpen) { this.boxDropdownOpen = false; return true } @@ -595,6 +599,11 @@ return false }, methods: { + invalidateTaskSync() { this.taskRequestId++; this.remoteLoading = false; this._showRefreshPromise = null }, + isTaskSyncActive(requestId, taskId) { + return this._pageVisible === true && requestId === this.taskRequestId + && String(this.remoteTaskId || this.currentBox.taskId) === String(taskId) + }, containerReference, isSelectableTask(task) { return Boolean(task?.recordStatus === 'ACTIVE' @@ -1090,7 +1099,7 @@ return '' }, async syncRemoteTask() { - if (!session.token) return + if (!session.token || !this._pageVisible) return const requestedTaskId = this.remoteTaskId || this.currentBox.taskId if (!requestedTaskId) { this.remoteError = '当前没有可进入的拣货任务,请返回任务列表刷新'; return } this.rememberEvidenceDraft() @@ -1114,7 +1123,7 @@ try { const warehouseId = this.currentBox.warehouseId const task = await getContainerPickingTask(selector, warehouseId) - if (requestId !== this.taskRequestId) return + if (!this.isTaskSyncActive(requestId, requestedTaskId)) return if (!task) throw new Error('服务端未返回当前拣货任务') if (this.isMergedContainerRecord(task)) { this.clearMergedTask(task, previousIdentity) @@ -1181,9 +1190,10 @@ if (remoteTask.workflowStatus !== 'AWAITING_REVIEW' && this.assignmentVersion === null) throw new Error('任务领取状态尚未同步,请刷新重试') let claimed = null if (remoteTask.workflowStatus === 'UNASSIGNED') { + if (!this.isTaskSyncActive(requestId, requestedTaskId)) return const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}` claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId) - if (requestId !== this.taskRequestId) return + if (!this.isTaskSyncActive(requestId, requestedTaskId)) return } else if (remoteTask.workflowStatus === 'IN_PROGRESS') { const assigneeId = remoteTask.assignment?.assigneeId if (!assigneeId || String(assigneeId) !== String(session.currentUser?.id || '')) throw new Error('当前任务已由其他作业员领取,请返回任务列表刷新') @@ -1215,9 +1225,10 @@ if (addedItemIds.length) this.showToast(`已同步 ${addedItemIds.length} 条新增待拣明细`, 'warning') } await this.loadLoadingEvidence() + if (!this.isTaskSyncActive(requestId, requestedTaskId)) return if (this.loadingStageReady) this.activeStage = 'evidence' } catch (error) { - if (requestId !== this.taskRequestId) return + if (!this.isTaskSyncActive(requestId, requestedTaskId)) return this.debugSubmit('refresh.error', { requestId, message: error.message || '', code: error.code || '', statusCode: error.statusCode || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '' }) this.remoteError = error.network ? '网络不可用,无法开始拣货,恢复网络后请重试' : this.remoteFailureMessage(error, '任务同步失败') } finally { if (requestId === this.taskRequestId) this.remoteLoading = false } @@ -1675,10 +1686,11 @@ this.performSubmit() }, confirmIncompleteSubmit() { - if (!this.selectedIncompleteReason) return - this.performSubmit(this.selectedIncompleteReason) + if (this.submitting || !this.selectedIncompleteReason) return + return this.performSubmit(this.selectedIncompleteReason) }, async performSubmit(incompleteReason = '') { + if (this.submitting) return const beforeSubmit = this.summary const remoteRequired = Boolean(session.token) this.remoteError = '' diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs index f525ef9..59f0008 100644 --- a/tests/page-contract.test.mjs +++ b/tests/page-contract.test.mjs @@ -292,7 +292,10 @@ test('picking session persists per task identity and clears on logout', async () assert.match(store, /savePickingSession/) assert.match(store, /resetPickingSession/) assert.match(session, /signOut\(\)[\s\S]*?resetPickingSession\(\)/) - assert.match(page, /onUnload\(\) \{ this\.stopHardwareScanner\(\); this\.persistSession\(\)/) + const unload = page.match(/onUnload\(\) \{([^}]+)\}/)?.[1] || '' + assert.match(unload, /this\.stopHardwareScanner\(\)/) + assert.match(unload, /this\.persistSession\(\)/) + assert.match(unload, /this\.invalidateTaskSync\(\)/) }) test('shared visual contracts preserve accessible PDA targets', async () => { diff --git a/tests/picking-refresh-route.test.mjs b/tests/picking-refresh-route.test.mjs index 128999d..6a7d078 100644 --- a/tests/picking-refresh-route.test.mjs +++ b/tests/picking-refresh-route.test.mjs @@ -36,12 +36,14 @@ test('duplicate onShow shares one load chain while background return refreshes a const page = await read('../pages/picking/picking.vue') const show = page.match(/async onShow\(\) \{[\s\S]*?\r?\n\t\t\},/)?.[0] const hide = page.match(/onHide\(\) \{[\s\S]*?\r?\n\t\t\},/)?.[0] - assert.ok(show && hide) - const hooks = new Function('requireSession', 'session', `return ({${show}${hide}})`)(() => true, { currentUser: { id: '7' } }) + const invalidate = page.match(/invalidateTaskSync\(\) \{[^\r\n]+\},/)?.[0] + assert.ok(show && hide && invalidate) + const hooks = new Function('requireSession', 'session', `return ({${show}${hide}${invalidate}})`)(() => true, { currentUser: { id: '7' } }) const calls = [] let release const pending = new Promise((resolve) => { release = resolve }) - const context = { startHardwareScanner() {}, stopHardwareScanner() {}, async refreshAvailableBoxes() { calls.push('pending'); await pending }, async syncRemoteTask() { calls.push('detail+evidence') } } + const context = { taskRequestId: 0, invalidateTaskSync: hooks.invalidateTaskSync, + startHardwareScanner() {}, stopHardwareScanner() {}, async refreshAvailableBoxes() { calls.push('pending'); await pending }, async syncRemoteTask() { calls.push('detail+evidence') } } const first = hooks.onShow.call(context) const duplicate = hooks.onShow.call(context) assert.deepEqual(calls, ['pending']) @@ -51,6 +53,9 @@ test('duplicate onShow shares one load chain while background return refreshes a await hooks.onShow.call(context) assert.equal(calls.length, 2) hooks.onHide.call(context) + assert.equal(context.taskRequestId, 1) + assert.equal(context.remoteLoading, false) + assert.equal(context._showRefreshPromise, null) await hooks.onShow.call(context) assert.deepEqual(calls, ['pending', 'detail+evidence', 'pending', 'detail+evidence']) const onLoad = page.match(/onLoad\(options = \{\}\) \{[\s\S]*?\r?\n\t\t\},/)?.[0] diff --git a/tests/picking-request-lifecycle.test.mjs b/tests/picking-request-lifecycle.test.mjs new file mode 100644 index 0000000..858eea4 --- /dev/null +++ b/tests/picking-request-lifecycle.test.mjs @@ -0,0 +1,118 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import vm from 'node:vm' +import { readFile } from 'node:fs/promises' + +const source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') +const script = source.match(/