fix(pda): make completed loading evidence read-only
This commit is contained in:
@@ -129,14 +129,14 @@
|
||||
<text class="evidence-count" :class="{ complete: evidenceEntries(slot.type).length }">{{ evidenceEntries(slot.type).length ? `已拍 ${evidenceEntries(slot.type).length} 张` : '待拍摄' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="evidence-btn evidence-add-btn" @tap="chooseEvidence(slot)">{{ evidenceUploadLabel(slot.type) }}</button>
|
||||
<button class="evidence-btn evidence-add-btn" :disabled="evidenceReadOnly" @tap="chooseEvidence(slot)">{{ evidenceUploadLabel(slot.type) }}</button>
|
||||
</view>
|
||||
<view v-if="evidenceEntries(slot.type).length" class="evidence-photo-grid">
|
||||
<view v-for="photo in evidenceEntries(slot.type)" :key="photo.id || photo.fileId || photo.localPath" class="evidence-photo-item">
|
||||
<image v-if="evidencePreviewSource(photo)" class="evidence-thumbnail" :src="evidencePreviewSource(photo)" mode="aspectFill" @tap="previewEvidencePhoto(photo, slot.type)" />
|
||||
<view v-else class="evidence-thumbnail evidence-thumbnail-placeholder" @tap="previewEvidencePhoto(photo, slot.type)">图片处理中</view>
|
||||
<text class="evidence-file" @tap="previewEvidencePhoto(photo, slot.type)">{{ photo.fileName || '已记录' }}</text>
|
||||
<button v-if="photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-delete-btn" aria-label="删除照片" @tap="deleteEvidencePhoto(photo)">×</button>
|
||||
<button v-if="!evidenceReadOnly && photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-delete-btn" aria-label="删除照片" @tap="deleteEvidencePhoto(photo)">×</button>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
||||
@@ -149,11 +149,12 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="seal-row">
|
||||
<input v-model="sealNo" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
||||
<button class="evidence-btn seal-btn" :disabled="sealSaving || !sealReadyToSave" @tap="saveSealNo">{{ sealSaving ? '上传中…' : '保存并上传' }}</button>
|
||||
<input v-model="sealNo" :disabled="evidenceReadOnly" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
||||
<button class="evidence-btn seal-btn" :disabled="evidenceReadOnly || sealSaving || !sealReadyToSave" @tap="saveSealNo">{{ sealSaving ? '上传中…' : '保存并上传' }}</button>
|
||||
</view>
|
||||
<view v-if="remoteError" class="remote-error-row"><text class="remote-error" role="alert">{{ remoteError }}</text><button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button></view>
|
||||
<button class="loading-complete-btn" :disabled="submitting || !loadingStageReady || pickingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<text v-if="evidenceReadOnly" class="version-hint" role="status">{{ loadingCompletionState.label }}</text>
|
||||
<button v-else class="loading-complete-btn" :disabled="submitting || !loadingStageReady || pickingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<view v-if="pickingVersion === null" class="remote-error-row">
|
||||
<text class="version-hint" role="status">状态尚未同步,请刷新重试</text>
|
||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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\)/)
|
||||
|
||||
Reference in New Issue
Block a user