diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index ea3fb51..5bfd45a 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -170,7 +170,7 @@ {{ remoteError }} 刷新任务 - + {{ submitting ? '正在提交…' : submitButtonText }} @@ -421,7 +421,8 @@ return this.boxes.length - this.completedCount }, pickingCompleted() { - if (this.remotePick) { + if (this.remoteTaskId || this.currentBox.taskId) { + if (!this.remotePick) return false const hasServerPending = this.currentBox.items.some((item) => ['PENDING', 'PICKING'].includes(item.itemStatus)) return isPickingCompleteTask(this.remotePick) && !hasServerPending } @@ -469,10 +470,10 @@ return '' }, submitButtonText() { - return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该柜次已完成' : this.summary.buttonText + return this.pickingCompleted ? '该柜次已完成' : this.summary.buttonText }, submitStatusHint() { - if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本柜次任务已完成' + if (this.pickingCompleted) return '本柜次任务已完成' if (!this.summary.canSubmit) return '请先处理至少一件' if (this.summary.pending > 0) return '提交时需选择原因' return `已处理 ${this.summary.processed}/${this.summary.total}` @@ -503,6 +504,9 @@ onLoad(options = {}) { if (!requireSession()) return if (options.taskId) { + // A local completion marker belongs to the previously opened task. The + // canonical server detail decides whether this task is complete. + this.completedBoxes = {} this.boxes = [{ boxNo: String(options.currentContainerNo || options.cabinetNo || '同步中'), cabinetNo: options.cabinetNo ? String(options.cabinetNo) : null, currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''), taskId: String(options.taskId), loadingPlanId: null, containerBookingId: null, @@ -1305,7 +1309,7 @@ } catch (error) { if (!this.isEvidenceContextActive(contextId, identity)) return if (this.isTaskConflict(error)) { - await this.refreshAfterTaskConflict(error.message || '装柜前置条件已变化,请刷新后重试') + await this.refreshAfterTaskConflict(error.message || '订舱、装柜关联或证据状态已变化,请刷新后重试') return } this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败') @@ -1434,7 +1438,7 @@ }, submitPicking() { if (!this.requirePickingOperation()) return - if (!this.summary.canSubmit || this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return + if (!this.summary.canSubmit || this.pickingCompleted) return if (this.summary.pending > 0) { this.selectedIncompleteReason = '' this.openOverlay('submitConfirm') diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs index 237329e..51aeeae 100644 --- a/tests/pda-loading-api.test.mjs +++ b/tests/pda-loading-api.test.mjs @@ -62,7 +62,7 @@ test('picking page exposes five evidence slots, retry and remote completion stat assert.match(page, /createEvidenceUploadQueue/) assert.match(page, /retryEvidence/) assert.match(page, /uploadEvidenceFile/) - assert.match(page, /submitting \|\| completedBoxes\[currentBoxIndex\]/) + assert.match(page, /submitting \|\| pickingCompleted \|\| !summary\.canSubmit/) assert.match(page, /completePicking\(/) assert.match(page, /sourceType: \['camera'\]/) assert.doesNotMatch(page, /sourceType: \['camera', 'album'\]/) @@ -126,3 +126,9 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(page, /availablePickQuantity/) assert.match(page, /库存\/库位可用量不足/) }) + +test('PDA final completion delegates automatic cabinet side effects to the task complete endpoint', () => { + const complete = source.match(/export function completeContainerPickingTask\(taskId, payload = \{\}\) \{[\s\S]*?\n\}/)?.[0] || '' + assert.match(complete, /\/first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/complete/) + assert.doesNotMatch(complete, /拖车|trucking/i) +}) diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs index fa408d6..92c0de6 100644 --- a/tests/picking-evidence-stage.test.mjs +++ b/tests/picking-evidence-stage.test.mjs @@ -7,10 +7,20 @@ const page = await readFile(new URL('../pages/picking/picking.vue', import.meta. test('picking and photo workspaces are independently selectable', () => { assert.match(page, /v-if="activeStage === 'evidence' && loadingStageReady" class="loading-evidence-section"/) assert.match(page, /isPickingCompleteTask\(task\)[\s\S]*?AWAITING_REVIEW[\s\S]*?COMPLETED/) - assert.match(page, /pickingCompleted\(\)[\s\S]*?if \(this\.remotePick\)[\s\S]*?hasServerPending[\s\S]*?isPickingCompleteTask\(this\.remotePick\) && !hasServerPending/) + assert.match(page, /pickingCompleted\(\)[\s\S]*?this\.remoteTaskId \|\| this\.currentBox\.taskId[\s\S]*?if \(!this\.remotePick\) return false[\s\S]*?hasServerPending[\s\S]*?isPickingCompleteTask\(this\.remotePick\) && !hasServerPending/) assert.match(page, /请先处理并提交全部待拣明细/) }) +test('a server task never inherits a completed marker from the previously opened local cabinet', () => { + assert.match(page, /pickingCompleted\(\) \{[\s\S]*?this\.remoteTaskId \|\| this\.currentBox\.taskId[\s\S]*?if \(!this\.remotePick\) return false[\s\S]*?hasServerPending/) + assert.match(page, /if \(options\.taskId\) \{[\s\S]*?this\.completedBoxes = \{\}[\s\S]*?this\.boxes = \[/) + assert.match(page, /:disabled="submitting \|\| pickingCompleted \|\| !summary\.canSubmit"/) + assert.match(page, /submitButtonText\(\)[\s\S]*?return this\.pickingCompleted \? '该柜次已完成'/) + assert.match(page, /submitStatusHint\(\)[\s\S]*?if \(this\.pickingCompleted\) return '本柜次任务已完成'/) + const submit = page.match(/submitPicking\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.doesNotMatch(submit, /completedBoxes/) +}) + test('loading evidence is not requested before picking completion', () => { assert.match(page, /if \(this\.loadingStageReady\) await this\.loadLoadingEvidence\(\)/) const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' @@ -72,7 +82,8 @@ test('incremental batch submits only current server-pending items and refreshes test('final completion preserves backend gate errors and refreshes after an atomic A0409', () => { const complete = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' - assert.match(complete, /this\.isTaskConflict\(error\)[\s\S]*?await this\.refreshAfterTaskConflict\(error\.message \|\| '装柜前置条件已变化,请刷新后重试'\)/) + assert.match(complete, /this\.isTaskConflict\(error\)[\s\S]*?await this\.refreshAfterTaskConflict\(error\.message \|\| '订舱、装柜关联或证据状态已变化,请刷新后重试'\)/) + assert.doesNotMatch(complete, /拖车|trucking/i) const save = page.match(/async saveEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' assert.match(save, /this\.isTaskConflict\(error\)[\s\S]*?refreshAfterTaskConflict/) const flush = page.match(/async flushPendingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''