This commit is contained in:
2026-08-28 16:53:58 +08:00
parent 863cd97689
commit 921847d70c
2 changed files with 39 additions and 41 deletions
+15 -22
View File
@@ -771,6 +771,14 @@
}
return true
},
requireEvidenceMutation() {
if (!this.requireEvidenceCapture()) return false
if (!isRemoteReady(this.evidenceIdentity.inboundId)) {
this.showToast('当前会话未连接服务端,证据保留本地', 'warning')
return false
}
return true
},
requirePickingOperation() {
if (this.pickingOperationReady) return true
this.showToast('任务尚未领取完成,请稍候或刷新重试', 'warning')
@@ -1172,10 +1180,8 @@
this.debugSubmit('refresh.incremental', { previousTaskVersion, taskVersion: this.pickingVersion, addedItemIds, pickingCompleted: this.pickingCompleted })
if (addedItemIds.length) this.showToast(`已同步 ${addedItemIds.length} 条新增待拣明细`, 'warning')
}
if (this.loadingStageReady) {
this.activeStage = 'evidence'
await this.loadLoadingEvidence()
}
await this.loadLoadingEvidence()
if (this.loadingStageReady) this.activeStage = 'evidence'
} catch (error) {
if (requestId !== this.taskRequestId) return
this.debugSubmit('refresh.error', { requestId, message: error.message || '', code: error.code || '', statusCode: error.statusCode || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '' })
@@ -1183,7 +1189,7 @@
} finally { if (requestId === this.taskRequestId) this.remoteLoading = false }
},
async loadLoadingEvidence() {
if (!this.loadingStageReady || !this.evidenceIdentity || !isRemoteReady(this.evidenceIdentity.inboundId)) return
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) return
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
try {
@@ -1286,18 +1292,13 @@
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
return
}
if (!this.loadingStageReady) {
this.rememberEvidenceDraft(identity)
this.showToast('照片已上传,拣货完成后自动保存', 'success')
return
}
await this.saveEvidence(type, null, path)
},
async flushPendingEvidence() {
if (!this.loadingStageReady) return
if (!this.evidenceIdentity?.inboundId) return
const pending = this.evidenceSlots.flatMap((slot) => this.evidenceEntries(slot.type).filter((entry) => !entry.id && entry.localPath).map((entry) => ({ type: slot.type, path: entry.localPath, fileId: entry.fileId })))
for (const item of pending) {
if (!this.loadingStageReady) return
if (!this.evidenceIdentity?.inboundId) return
if (item.fileId && item.type !== 'SEAL') await this.saveEvidence(item.type, null, item.path)
else if (item.fileId) continue
else await this.queueEvidenceUpload(item.type, item.path)
@@ -1322,18 +1323,11 @@
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
return
}
if (!this.loadingStageReady) {
this.rememberEvidenceDraft(identity)
this.showToast('照片已上传,拣货完成后自动保存', 'success')
return
}
await this.saveEvidence(type)
},
async saveEvidence(type, operationId = null, localPath = null) {
if (!this.requireEvidenceStage()) return
if (!this.requireEvidenceMutation()) return
if (type === 'SEAL' && !String(this.sealNo || '').trim()) { this.showToast('请输入封签号后点击保存封签', 'warning'); return }
if (!this.evidenceIdentity?.inboundId) { this.showToast('状态尚未同步,请刷新重试', 'warning'); return }
if (!isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
const inboundId = identity.inboundId
@@ -1365,7 +1359,6 @@
if (this.sealSaving) return
if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return }
this.rememberEvidenceDraft()
if (!this.loadingStageReady) { this.showToast('封签号已暂存,装柜状态同步后自动保存', 'success'); return }
const current = [...this.evidenceEntries('SEAL')].reverse().find((item) => item.fileId && !item.id)
if (!current) { this.showToast('当前封签已保存,无需重复提交', 'success'); return }
const operationId = stableOperationId('seal', stableTaskOperationIdentity(this.draftIdentity()), { fileId: String(current.fileId), sealNo: String(this.sealNo).trim() })
@@ -1377,7 +1370,7 @@
}
},
async voidEvidencePhoto(photo) {
if (!this.requireEvidenceStage()) return
if (!this.requireEvidenceMutation()) return
if (!photo?.id || !this.evidenceIdentity?.inboundId) return
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
+24 -19
View File
@@ -32,12 +32,12 @@ test('picking completion leads to current cabinet evidence instead of an all-com
assert.doesNotMatch(page, /查看全部完成|allCompletePopup|nextBox\(\)/)
})
test('loading evidence is not requested before picking completion', () => {
assert.match(page, /if \(this\.loadingStageReady\) \{[\s\S]*?await this\.loadLoadingEvidence\(\)/)
test('loading evidence is requested as soon as the claimed task has a stable evidence identity', () => {
assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)[\s\S]*?await this\.loadLoadingEvidence\(\)/)
const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(loader, /if \(!this\.loadingStageReady/)
assert.match(loader, /if \(!this\.evidenceIdentity\?\.inboundId/)
assert.doesNotMatch(loader, /if \(!this\.loadingStageReady/)
assert.match(loader, /await getEvidence\(/)
assert.match(page, /this\.completedBoxes = \{[\s\S]*?this\.loadLoadingEvidence\(\)/)
})
test('operators receive actionable refresh guidance without internal version wording', () => {
@@ -50,11 +50,11 @@ test('operators receive actionable refresh guidance without internal version wor
assert.doesNotMatch(complete, /completeLoading\(inboundId/)
})
test('AWAITING_REVIEW alone cannot request or mutate server evidence without a server loading version', () => {
test('AWAITING_REVIEW without a server loading version blocks final completion but not evidence maintenance', () => {
assert.match(page, /loadingStageReady\(\)[\s\S]*?this\.pickingCompleted[\s\S]*?this\.loadingVersion !== null[\s\S]*?Number\.isFinite/)
assert.match(page, /拣货已完成,装柜状态尚未同步,请刷新重试/)
assert.match(page, /if \(!this\.loadingStageReady \|\| !this\.evidenceIdentity/)
assert.doesNotMatch(page, /if \(this\.pickingCompleted\) await this\.loadLoadingEvidence\(\)/)
assert.match(page, /async completeLoadingTask\([^)]*\) \{[\s\S]*?requireEvidenceStage/)
assert.match(page, /requireEvidenceMutation\(\)[\s\S]*?requireEvidenceCapture/)
})
test('dynamic pending items do not block switching between picking and evidence', () => {
@@ -95,7 +95,7 @@ test('incremental batch submits only current server-pending items and refreshes
test('completed detail overrides stale local state and an empty batch refreshes into evidence instead of submitting', () => {
assert.match(page, /if \(item\.itemStatus === 'PICKED'\) serverStates\[item\.id\] = ITEM_STATUS\.PICKED/)
assert.match(page, /if \(!this\.pickingCompleted\) this\.restoreRemoteDraft\(\)/)
assert.match(page, /if \(this\.loadingStageReady\) \{[\s\S]*?this\.activeStage = 'evidence'[\s\S]*?await this\.loadLoadingEvidence\(\)/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
const submit = page.match(/async submitPicking\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(submit, /if \(this\.pickingCompleted\)[\s\S]*?await this\.syncRemoteTask\(\)[\s\S]*?this\.activeStage = 'evidence'[\s\S]*?return/)
const perform = page.match(/async performSubmit\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
@@ -116,7 +116,7 @@ test('final completion preserves backend gate errors and refreshes after an atom
assert.match(flush, /else await this\.queueEvidenceUpload\(item\.type, item\.path\)/)
})
test('seal photo upload waits for an explicit seal-number save', () => {
test('seal photo upload waits for an explicit seal-number save without waiting for picking completion', () => {
const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
const retry = page.match(/async retryEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
const save = page.match(/async saveEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
@@ -124,6 +124,8 @@ test('seal photo upload waits for an explicit seal-number save', () => {
assert.match(retry, /if \(type === 'SEAL'\)[\s\S]*?rememberEvidenceDraft\(identity\)[\s\S]*?return[\s\S]*?saveEvidence\(type\)/)
assert.match(save, /type === 'SEAL' && !String\(this\.sealNo \|\| ''\)\.trim\(\)[\s\S]*?请输入封签号后点击保存封签/)
assert.match(page, /async saveSealNo\(\)[\s\S]*?await this\.saveEvidence\('SEAL'/)
const saver = page.match(/async saveSealNo\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.doesNotMatch(saver, /if \(!this\.loadingStageReady\)/)
assert.match(page, /hasUnboundUploadedEvidence\(slot\.type\)[\s\S]*?照片已上传,请输入封签号并保存/)
})
@@ -151,22 +153,25 @@ test('merged or non-active cabinet details are rejected before claim and clear s
for (const reset of ['remoteLoading = false', 'submitting = false', 'remoteTaskId = null', 'loadingVersion = null', 'scanHistory = []', 'currentBox.containerBookingId = null', 'currentBox.currentContainerNo = null', 'resetEvidenceState()']) assert.match(clear, new RegExp(reset.replace(/[.()[\]]/g, '\\$&')))
})
test('photos upload before picking completion while evidence binding remains gated', () => {
test('photos upload, bind, reload and void before picking completion', () => {
assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/)
const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.ok(queue.indexOf('await queue.flush()') < queue.indexOf('if (!this.loadingStageReady)'))
assert.ok(queue.indexOf('if (!this.loadingStageReady)') < queue.indexOf('await this.saveEvidence(type, null, path)'))
assert.match(queue, /await queue\.flush\(\)[\s\S]*?await this\.saveEvidence\(type, null, path\)/)
assert.doesNotMatch(queue, /if \(!this\.loadingStageReady\)/)
assert.match(page, /chooseEvidence\(slot\)[\s\S]*?this\.queueEvidenceUpload\(slot\.type, path\)/)
const retry = page.match(/async retryEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(retry, /if \(!this\.requireEvidenceCapture\(\)\) return/)
assert.ok(retry.indexOf('await queue.flush()') < retry.indexOf('if (!this.loadingStageReady)'))
for (const method of ['saveEvidence', 'voidEvidencePhoto', 'completeLoadingTask']) {
const body = page.match(new RegExp(`(?:async )?${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
assert.match(body, /if \(!this\.requireEvidenceStage\(\)\)(?: return| \{)/, `${method} should guard the evidence stage`)
}
assert.match(retry, /await queue\.flush\(\)[\s\S]*?await this\.saveEvidence\(type\)/)
assert.doesNotMatch(retry, /if \(!this\.loadingStageReady\)/)
assert.match(page, /async loadLoadingEvidence\(\) \{[\s\S]*?!this\.evidenceIdentity\?\.inboundId[\s\S]*?getEvidence/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
assert.match(page, /async saveEvidence\([^)]*\) \{[\s\S]*?if \(!this\.requireEvidenceMutation\(\)\) return/)
assert.match(page, /async voidEvidencePhoto\([^)]*\) \{[\s\S]*?if \(!this\.requireEvidenceMutation\(\)\) return/)
assert.match(page, /async completeLoadingTask\([^)]*\) \{[\s\S]*?if \(!this\.requireEvidenceStage\(\)\) return/)
assert.doesNotMatch(page, /照片已上传,拣货完成后自动保存/)
})
test('refresh preserves local photos for the same evidence identity and flushes them after loading readiness', () => {
test('refresh preserves local photos and flushes them as soon as evidence identity is ready', () => {
assert.match(page, /this\.rememberEvidenceDraft\(\)/)
assert.match(page, /this\.restoreEvidenceDraft\(\)/)
assert.match(page, /async flushPendingEvidence\(\)[\s\S]*?queueEvidenceUpload/)
@@ -249,7 +254,7 @@ test('completed server tasks restore evidence stage without being claimed again'
assert.match(page, /remoteTask\.workflowStatus !== 'AWAITING_REVIEW' && this\.assignmentVersion === null/)
assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\) \{[\s\S]*?claimContainerPickingTask/)
assert.match(page, /else if \(!isPickingCompleteTask\(remoteTask\)\)/)
assert.match(page, /if \(this\.loadingStageReady\) \{[\s\S]*?this\.activeStage = 'evidence'[\s\S]*?await this\.loadLoadingEvidence\(\)/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
})
test('picking mutations stay closed until the canonical task has been claimed', () => {