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>
|
<text class="evidence-count" :class="{ complete: evidenceEntries(slot.type).length }">{{ evidenceEntries(slot.type).length ? `已拍 ${evidenceEntries(slot.type).length} 张` : '待拍摄' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</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>
|
||||||
<view v-if="evidenceEntries(slot.type).length" class="evidence-photo-grid">
|
<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">
|
<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)" />
|
<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>
|
<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>
|
<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>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
||||||
@@ -149,11 +149,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="seal-row">
|
<view class="seal-row">
|
||||||
<input v-model="sealNo" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
<input v-model="sealNo" :disabled="evidenceReadOnly" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
||||||
<button class="evidence-btn seal-btn" :disabled="sealSaving || !sealReadyToSave" @tap="saveSealNo">{{ sealSaving ? '上传中…' : '保存并上传' }}</button>
|
<button class="evidence-btn seal-btn" :disabled="evidenceReadOnly || sealSaving || !sealReadyToSave" @tap="saveSealNo">{{ sealSaving ? '上传中…' : '保存并上传' }}</button>
|
||||||
</view>
|
</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>
|
<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">
|
<view v-if="pickingVersion === null" class="remote-error-row">
|
||||||
<text class="version-hint" role="status">状态尚未同步,请刷新重试</text>
|
<text class="version-hint" role="status">状态尚未同步,请刷新重试</text>
|
||||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||||
@@ -348,7 +349,7 @@
|
|||||||
import { isOnline } from '@/utils/network.js'
|
import { isOnline } from '@/utils/network.js'
|
||||||
import { stableOperationId } from '@/utils/idempotency.js'
|
import { stableOperationId } from '@/utils/idempotency.js'
|
||||||
import { stableTaskOperationIdentity } from '@/utils/task-identity.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 { containerReference, formatApiError, sanitizeUserMessage } from '@/utils/user-facing.js'
|
||||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||||
import PopupHeader from '@/components/PopupHeader.vue'
|
import PopupHeader from '@/components/PopupHeader.vue'
|
||||||
@@ -444,6 +445,17 @@
|
|||||||
&& Boolean(this.remoteTaskId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
|
&& Boolean(this.remoteTaskId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
|
||||||
&& this.pickingVersion !== null
|
&& 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() {
|
loadingStageReady() {
|
||||||
return this.pickingCompleted
|
return this.pickingCompleted
|
||||||
&& this.evidenceIdentity
|
&& this.evidenceIdentity
|
||||||
@@ -791,6 +803,10 @@
|
|||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
requireEvidenceStage() {
|
requireEvidenceStage() {
|
||||||
|
if (this.evidenceReadOnly) {
|
||||||
|
this.showToast(this.loadingCompletionState.label, 'warning')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!this.pickingCompleted) {
|
if (!this.pickingCompleted) {
|
||||||
this.showToast('请先提交拣货完成', 'warning')
|
this.showToast('请先提交拣货完成', 'warning')
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
import {
|
import {
|
||||||
COMPLETION_PHASE,
|
COMPLETION_PHASE,
|
||||||
completionPresentation,
|
completionPresentation,
|
||||||
@@ -8,6 +9,31 @@ import {
|
|||||||
shouldReconcileCompletion
|
shouldReconcileCompletion
|
||||||
} from '../utils/loading-completion-state.js'
|
} 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', () => {
|
test('accepted completion is processing and blocks every repeated mutation', () => {
|
||||||
for (const facts of [
|
for (const facts of [
|
||||||
{ taskStatus: 'COMPLETING' },
|
{ 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')
|
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', () => {
|
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, /sealSaving \? '上传中…' : '保存并上传'/)
|
||||||
assert.match(page, /async saveSealNo\(\) \{[\s\S]*?if \(this\.sealSaving\) return/)
|
assert.match(page, /async saveSealNo\(\) \{[\s\S]*?if \(this\.sealSaving\) return/)
|
||||||
assert.match(page, /find\(\(item\) => item\.localPath && !item\.id\)/)
|
assert.match(page, /find\(\(item\) => item\.localPath && !item\.id\)/)
|
||||||
|
|||||||
Reference in New Issue
Block a user