From ec62a8a397cb77cb27340963a85f970a73743286 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 18:16:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/picking/picking.vue | 60 +++++++++++++++++++++++++++++++++- tests/page-contract.test.mjs | 2 +- tests/pda-loading-api.test.mjs | 8 +++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 778a295..f783876 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -1127,7 +1127,7 @@ const serverPartialQuantities = {} this.currentBox.items.forEach((item) => { if (item.itemStatus === 'PICKED') serverStates[item.id] = ITEM_STATUS.PICKED - else if (item.itemStatus === 'PARTIAL') { + else if (item.itemStatus === 'PICKING' || item.itemStatus === 'PARTIAL') { serverStates[item.id] = ITEM_STATUS.PARTIAL serverPartialQuantities[item.id] = Number(item.pickedPackageCount ?? item.finalPickedPackageCount ?? 0) } else if (item.itemStatus === 'SKIPPED') serverStates[item.id] = ITEM_STATUS.SKIPPED @@ -1529,6 +1529,64 @@ this.showToast(`已部分拣货:${value}/${item.qty} ${item.unit}`, 'success') this.persistSession() }, + async submitImmediateItemAction(item, action, payload = {}) { + if (this.submitting) { this.showToast('上一笔拣货正在提交,请稍候', 'warning'); return false } + if (!this.remoteTaskId || !this.currentBox.warehouseId || this.pickingVersion === null) { + this.showToast('任务状态尚未同步,请刷新重试', 'warning') + return false + } + const itemId = item.taskItemId ?? item.remoteItemId ?? item.id + const identity = stableTaskOperationIdentity(this.draftIdentity()) + const operationId = stableOperationId(`item-${action.toLowerCase()}`, identity, { + itemId, itemVersion: item.version, taskVersion: this.pickingVersion, ...payload + }) + const common = { warehouseId: this.currentBox.warehouseId, version: Number(item.version || 0), operationId } + this.submitting = true + this.remoteError = '' + try { + let task + if (action === 'PICK') task = await pickContainerItem(this.remoteTaskId, itemId, { ...common, pickedPackageCount: payload.pickedPackageCount }) + else if (action === 'PARTIAL') task = await partialContainerItem(this.remoteTaskId, itemId, { ...common, finalPickedPackageCount: payload.finalPickedPackageCount }) + else if (action === 'SKIP') task = await skipContainerItem(this.remoteTaskId, itemId, { ...common, reason: payload.reason }) + else task = await pickContainerPallet(this.remoteTaskId, { warehouseId: this.currentBox.warehouseId, palletNo: payload.palletNo, expectedTaskVersion: Number(this.pickingVersion), operationId }) + this.applyImmediateTaskResult(task) + return true + } catch (error) { + if (this.isTaskConflict(error)) await this.refreshAfterTaskConflict(sanitizeUserMessage(error.message, '装柜明细已变化,请刷新后重试')) + else { + this.remoteError = error.network ? '网络失败,本次拣货未提交' : this.remoteFailureMessage(error, '拣货提交失败') + this.showToast('拣货未提交,请重试', 'error') + } + return false + } finally { + this.submitting = false + } + }, + applyImmediateTaskResult(result) { + if (!result) return + const task = normalizePickingTask({ ...this.remotePick, ...result }) + this.remotePick = task + this.pickingVersion = task.pickingVersion ?? task.version ?? this.pickingVersion + const remoteItems = Array.isArray(result.items) ? result.items : [] + const byId = new Map(remoteItems.map((item) => [String(item.id ?? item.itemId), item])) + this.currentBox.items = this.currentBox.items.map((item) => { + const remote = byId.get(String(item.taskItemId ?? item.remoteItemId ?? item.id)) + return remote ? { ...item, ...remote, id: item.id, taskItemId: remote.id ?? remote.itemId ?? item.taskItemId } : item + }) + const nextStates = { ...this.itemStates } + const nextPartial = { ...this.partialQuantities } + this.currentBox.items.forEach((item) => { + const count = Number(item.pickedPackageCount || 0) + if (item.itemStatus === 'PICKED') nextStates[item.id] = ITEM_STATUS.PICKED + else if (item.itemStatus === 'PICKING' || item.itemStatus === 'PARTIAL') { + nextStates[item.id] = ITEM_STATUS.PARTIAL + nextPartial[item.id] = count + } else if (item.itemStatus === 'SKIPPED') nextStates[item.id] = ITEM_STATUS.SKIPPED + }) + this.itemStates = nextStates + this.partialQuantities = nextPartial + this.evidenceIdentity = this.captureEvidenceIdentity() + }, availablePickQuantity(item) { if (!item) return null if (item.locationAvailable === false || item.locationStatus === 'DISABLED') return 0 diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs index 755cbc3..e92929e 100644 --- a/tests/page-contract.test.mjs +++ b/tests/page-contract.test.mjs @@ -268,7 +268,7 @@ test('picking scan results use the shared success and error audio feedback', asy assert.match(audio, /export function playErrorSound\(\)/) assert.match(picking, /import \{ playErrorSound, playSuccessSound \} from '@\/utils\/audio-feedback\.js'/) assert.match(picking, /result\.type === 'unknown'[\s\S]*?playErrorSound\(\)/) - assert.match(picking, /playSuccessSound\(\)[\s\S]*?this\.pickScannedBox\(result\.item, value\)/) + assert.match(picking, /await this\.pickScannedBox\(result\.item, value\)[\s\S]*?if \(submitted\) playSuccessSound\(\)/) }) test('reusable scan target input separates field selection from page-level broadcast handling', async () => { diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs index 7b1c99a..1a6b01d 100644 --- a/tests/pda-loading-api.test.mjs +++ b/tests/pda-loading-api.test.mjs @@ -109,6 +109,8 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.doesNotMatch(source, /query\.push\(`cabinetNo=/) assert.doesNotMatch(source, /query\.push\(`loadingPlanId=/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/) + assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/items\/\$\{encodeURIComponent\(itemId\)\}\/\$\{path\}/) + assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/pallets\/pick/) assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/inventory-snapshot\?warehouseId=/) assert.match(source, /submitContainerPickingBatch/) @@ -116,6 +118,12 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(source, /Math\.imul\(hash, 16777619\)/) assert.match(source, /slice\(0, 128\)/) assert.match(page, /claimContainerPickingTask\(/) + assert.match(page, /submitImmediateItemAction\(item, 'PICK'/) + assert.match(page, /submitImmediateItemAction\(item, 'PARTIAL'/) + assert.match(page, /submitImmediateItemAction\(item, 'SKIP'/) + assert.match(page, /submitImmediateItemAction\(item, 'PICK_PALLET'/) + assert.match(page, /item\.itemStatus === 'PICKING' \|\| item\.itemStatus === 'PARTIAL'/) + assert.doesNotMatch(page, /@tap="undoItem\(item\.id\)"/) assert.match(page, /submitContainerPickingBatch\(/) assert.match(source, /palletNo: action\.type === 'PICK_PALLET' \? action\.palletNo : null/) assert.match(page, /type: 'PICK_PALLET'[\s\S]*?palletNo: item\.palletNo/)