diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue
index e68e4fd..b893c0f 100644
--- a/pages/picking/picking.vue
+++ b/pages/picking/picking.vue
@@ -129,14 +129,14 @@
{{ evidenceEntries(slot.type).length ? `已拍 ${evidenceEntries(slot.type).length} 张` : '待拍摄' }}
-
+
图片处理中
{{ photo.fileName || '已记录' }}
-
+
@@ -149,11 +149,12 @@
-
-
+
+
{{ remoteError }}
-
+ {{ loadingCompletionState.label }}
+
状态尚未同步,请刷新重试
@@ -348,7 +349,7 @@
import { isOnline } from '@/utils/network.js'
import { stableOperationId } from '@/utils/idempotency.js'
import { stableTaskOperationIdentity } from '@/utils/task-identity.js'
- import { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion } from '@/utils/loading-completion-state.js'
+ import { COMPLETION_PHASE, completionPresentation, resolveCompletionPhase, shouldReconcileCompletion } from '@/utils/loading-completion-state.js'
import { containerReference, formatApiError, sanitizeUserMessage } from '@/utils/user-facing.js'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import PopupHeader from '@/components/PopupHeader.vue'
@@ -444,6 +445,17 @@
&& Boolean(this.remoteTaskId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
&& this.pickingVersion !== null
},
+ loadingCompletionState() {
+ const phase = resolveCompletionPhase({
+ taskStatus: this.remotePick?.workflowStatus || this.remotePick?.taskStatus,
+ commandStatus: this.remotePick?.completionCommand?.status || this.remotePick?.commandStatus,
+ submitting: this.submitting
+ })
+ return { ...completionPresentation(phase), phase }
+ },
+ evidenceReadOnly() {
+ return this.loadingCompletionState.blocked && !this.loadingCompletionState.retryable
+ },
loadingStageReady() {
return this.pickingCompleted
&& this.evidenceIdentity
@@ -791,6 +803,10 @@
return false
},
requireEvidenceStage() {
+ if (this.evidenceReadOnly) {
+ this.showToast(this.loadingCompletionState.label, 'warning')
+ return false
+ }
if (!this.pickingCompleted) {
this.showToast('请先提交拣货完成', 'warning')
return false
diff --git a/tests/loading-completion-state.test.mjs b/tests/loading-completion-state.test.mjs
index 096764e..dd91eda 100644
--- a/tests/loading-completion-state.test.mjs
+++ b/tests/loading-completion-state.test.mjs
@@ -1,5 +1,6 @@
import test from 'node:test'
import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
import {
COMPLETION_PHASE,
completionPresentation,
@@ -8,6 +9,31 @@ import {
shouldReconcileCompletion
} from '../utils/loading-completion-state.js'
+test('reopened processing and completed tasks keep evidence and completion read-only', async () => {
+ const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
+ const methods = ['loadingCompletionState', 'evidenceReadOnly', 'requireEvidenceStage'].map(name => {
+ const method = page.match(new RegExp(`${name}\\(\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0]
+ assert.ok(method, name)
+ return method
+ })
+ const behavior = new Function('resolveCompletionPhase', 'completionPresentation', `return ({${methods.join('\n')}})`)(resolveCompletionPhase, completionPresentation)
+ for (const workflowStatus of ['PROCESSING', 'COMPLETED']) {
+ const messages = []
+ const context = { remotePick: { workflowStatus, completionCommand: { status: workflowStatus } }, submitting: false, pickingCompleted: true, loadingStageReady: true, showToast(message) { messages.push(message) } }
+ context.loadingCompletionState = behavior.loadingCompletionState.call(context)
+ context.evidenceReadOnly = behavior.evidenceReadOnly.call(context)
+ assert.equal(context.evidenceReadOnly, true)
+ assert.equal(behavior.requireEvidenceStage.call(context), false)
+ assert.deepEqual(messages, [workflowStatus === 'COMPLETED' ? '装柜已完成' : '完成处理中'])
+ }
+ const retry = { remotePick: { workflowStatus: 'COMPLETION_FAILED' }, submitting: false }
+ retry.loadingCompletionState = behavior.loadingCompletionState.call(retry)
+ assert.equal(behavior.evidenceReadOnly.call(retry), false)
+ assert.match(page, /v-if="evidenceReadOnly"[^>]*role="status"/)
+ assert.match(page, /v-else class="loading-complete-btn"/)
+ assert.match(page, /class="evidence-btn evidence-add-btn" :disabled="evidenceReadOnly"/)
+})
+
test('accepted completion is processing and blocks every repeated mutation', () => {
for (const facts of [
{ taskStatus: 'COMPLETING' },
diff --git a/tests/seal-save-idempotency.test.mjs b/tests/seal-save-idempotency.test.mjs
index 1dba9ae..b09d39c 100644
--- a/tests/seal-save-idempotency.test.mjs
+++ b/tests/seal-save-idempotency.test.mjs
@@ -5,7 +5,7 @@ import { readFile } from 'node:fs/promises'
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
test('saving a seal is single-flight and submits the pending photo through the atomic evidence command', () => {
- assert.match(page, /:disabled="sealSaving \|\| !sealReadyToSave"/)
+ assert.match(page, /:disabled="evidenceReadOnly \|\| sealSaving \|\| !sealReadyToSave"/)
assert.match(page, /sealSaving \? '上传中…' : '保存并上传'/)
assert.match(page, /async saveSealNo\(\) \{[\s\S]*?if \(this\.sealSaving\) return/)
assert.match(page, /find\(\(item\) => item\.localPath && !item\.id\)/)