From 500103e7342e2d7564e1cd6f10001311570a3cdb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 5 Nov 2026 16:30:49 +0800 Subject: [PATCH] fix: return completed loading tasks to the task list --- pages/picking/picking.vue | 22 ++++++- tests/loading-completion-state.test.mjs | 6 +- tests/picking-failure-recovery.test.mjs | 8 ++- tests/picking-request-lifecycle.test.mjs | 78 ++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 7ed03fe..0a93606 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -922,8 +922,13 @@ }) }, persistSession() { + const identity = this.draftIdentity() + const completed = this.remotePick?.taskStatus === 'COMPLETED' + if (completed) this.completedBoxes = { ...this.completedBoxes, [this.currentBoxIndex]: true } this.saveCurrentBoxDraft() - savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, this.draftIdentity()) + // 完成任务仍更新作业记录的内存快照,空身份阻止重新写入其持久化草稿。 + savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, completed ? {} : identity) + if (completed) clearPickingDraft(identity) }, leavePicking() { this.persistSession(); this.activeOverlay = ''; navigateBackOr('/pages/home/home') }, clearTimers() { clearTimeout(this._toastTimer); clearTimeout(this._evidencePreviewOpenTimer) }, @@ -1186,6 +1191,16 @@ this.currentBox.boxNo = task.currentContainerNo this.currentBox.currentContainerNo = task.currentContainerNo const remoteTask = normalizePickingTask(task) + // 刷新确认后台已完成时直接返回任务列表,不再停留在已完成柜子内。 + if (remoteTask.taskStatus === 'COMPLETED') { + this.remotePick = remoteTask + this.pickingVersion = remoteTask.pickingVersion ?? remoteTask.version + if (this.activeOverlay) this.closeOverlay() + this.boxDropdownOpen = false + this.persistSession() + navigateBackOr('/pages/tasks/tasks') + return + } this.assignmentVersion = remoteTask.assignment?.version ?? remoteTask.assignmentVersion ?? (remoteTask.workflowStatus === 'UNASSIGNED' ? 0 : null) if (remoteTask.workflowStatus !== 'AWAITING_REVIEW' && this.assignmentVersion === null) throw new Error('任务领取状态尚未同步,请刷新重试') let claimed = null @@ -1470,6 +1485,11 @@ } this.remoteError = '' this.showToast('装柜已完成', 'success') + // 收起页面内浮层,避免 onBackPress 拦截完成后的程序化返回。 + if (this.activeOverlay) this.closeOverlay() + this.boxDropdownOpen = false + this.persistSession() + navigateBackOr('/pages/tasks/tasks') } catch (error) { if (!this.isEvidenceContextActive(contextId, identity)) return if (accepted) { diff --git a/tests/loading-completion-state.test.mjs b/tests/loading-completion-state.test.mjs index 3a4f8f1..6521f18 100644 --- a/tests/loading-completion-state.test.mjs +++ b/tests/loading-completion-state.test.mjs @@ -63,18 +63,19 @@ test('query failure is not retried as a completion mutation and completed accept readTask: async () => assert.fail('no query after authoritative completion'), isActive: () => true, onTask() {} }), completed) }) -test('page only announces success without persisting a replay request after authoritative completion', async () => { +test('page only returns to the task list after authoritative completion without persisting a replay request', async () => { const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') const method = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] assert.ok(method) for (const status of ['COMPLETED', 'COMPLETION_FAILED', 'COMPLETING', 'LOADING', 'QUERY_ERROR', 'NO_RESPONSE']) { - const messages = [], cleared = [], saved = [] + const messages = [], cleared = [], saved = [], destinations = [] const bindings = { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion, isRemoteReady: () => true, restoreLoadingCompletionAttempt: () => null, stableTaskOperationIdentity: () => 'stable-task', stableOperationId: () => 'original-operation', saveLoadingCompletionAttempt: (_, attempt) => saved.push(attempt), clearLoadingCompletionAttempt: identity => cleared.push(identity), + navigateBackOr: url => destinations.push(url), completeContainerPickingTask: async () => status === 'NO_RESPONSE' ? null : { taskStatus: 'COMPLETING', version: 2 }, normalizePickingTask: task => task, observeLoadingCompletion: async options => { @@ -99,6 +100,7 @@ test('page only announces success without persisting a replay request after auth assert.equal(saved.length, 0) assert.equal(cleared.length, 0) assert.equal(messages.some(message => message.message === '装柜已完成'), status === 'COMPLETED') + assert.deepEqual(destinations, status === 'COMPLETED' ? ['/pages/tasks/tasks'] : []) if (status !== 'COMPLETED') assert.ok(context.remoteError) } }) diff --git a/tests/picking-failure-recovery.test.mjs b/tests/picking-failure-recovery.test.mjs index 0490785..c2ece7f 100644 --- a/tests/picking-failure-recovery.test.mjs +++ b/tests/picking-failure-recovery.test.mjs @@ -7,12 +7,13 @@ const page = await readFile(new URL('../pages/picking/picking.vue', import.meta. const method = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)[0] function completion(overrides = {}, savedAttempt = null) { let attempt = savedAttempt - const calls = [], cleared = [], messages = [] + const calls = [], cleared = [], messages = [], destinations = [] const bindings = { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion, isRemoteReady: () => true, stableTaskOperationIdentity: () => 'task', stableOperationId: (_action, _identity, payload) => `complete-v${payload.expectedTaskVersion}`, restoreLoadingCompletionAttempt: () => attempt, saveLoadingCompletionAttempt: (_identity, value) => { attempt = value }, clearLoadingCompletionAttempt: identity => { cleared.push(identity); attempt = null }, + navigateBackOr: url => destinations.push(url), completeContainerPickingTask: async (_task, payload) => { calls.push(payload); return { taskStatus: 'COMPLETED' } }, normalizePickingTask: task => task, observeLoadingCompletion: async options => options.initialTask, sanitizeUserMessage: value => value, ...overrides @@ -27,15 +28,16 @@ function completion(overrides = {}, savedAttempt = null) { remoteFailureMessage: error => error.message, showToast: message => messages.push(message) } state.complete = behavior.completeLoadingTask.bind(state) - return { state, calls, cleared, messages, attempt: () => attempt } + return { state, calls, cleared, messages, destinations, attempt: () => attempt } } test('completion in flight blocks a second click and releases busy after the task context changes', async () => { let resolve - const { state, calls } = completion({ completeContainerPickingTask: (...args) => { calls.push(args); return new Promise(done => { resolve = done }) } }) + const { state, calls, destinations } = completion({ completeContainerPickingTask: (...args) => { calls.push(args); return new Promise(done => { resolve = done }) } }) const first = state.complete(); await state.complete(); assert.equal(calls.length, 1) state.evidenceContextId++; resolve({ taskStatus: 'COMPLETED' }); await first assert.equal(state.submitting, false); assert.equal(state.remotePick.taskStatus, undefined) + assert.deepEqual(destinations, []) }) test('an old completion response cannot release a newer picking submission', async () => { diff --git a/tests/picking-request-lifecycle.test.mjs b/tests/picking-request-lifecycle.test.mjs index 8dfddec..bb8efc5 100644 --- a/tests/picking-request-lifecycle.test.mjs +++ b/tests/picking-request-lifecycle.test.mjs @@ -2,6 +2,8 @@ import test from 'node:test' import assert from 'node:assert/strict' import vm from 'node:vm' import { readFile } from 'node:fs/promises' +import { navigateBackOr } from '../utils/navigation.js' +import { COMPLETION_PHASE, observeLoadingCompletion, resolveCompletionPhase, shouldReconcileCompletion } from '../utils/loading-completion-state.js' const source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') const script = source.match(/