diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 9bc73ab..b09b3ad 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -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 } diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs index e865d1c..bdeabc8 100644 --- a/tests/picking-evidence-stage.test.mjs +++ b/tests/picking-evidence-stage.test.mjs @@ -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', () => {