From e41f7085700d6873e57444b348a3816d7310d5a7 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Fri, 11 Sep 2026 18:15:25 +0800
Subject: [PATCH] fix: support whole-pallet picking by carton scan
---
api/pda-loading.js | 10 ++++++++
pages/picking/picking.vue | 51 +++++++++++++++++++--------------------
2 files changed, 35 insertions(+), 26 deletions(-)
diff --git a/api/pda-loading.js b/api/pda-loading.js
index e674bb4..b8b7157 100644
--- a/api/pda-loading.js
+++ b/api/pda-loading.js
@@ -97,6 +97,16 @@ export function pickContainerItem(taskId, itemId, payload = {}) {
return itemAction(taskId, itemId, 'pick', payload, payload.operationId)
}
+export function pickContainerPallet(taskId, payload = {}) {
+ if (!taskId || !payload.palletNo) throw new Error('缺少拣货任务或托盘码,无法提交')
+ return request({
+ url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/pallets/pick`,
+ method: 'POST',
+ header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `pick-pallet-${taskId}-${payload.palletNo}`) },
+ data: payload
+ })
+}
+
export function partialContainerItem(taskId, itemId, payload = {}) {
return itemAction(taskId, itemId, 'partial', payload, payload.operationId)
}
diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue
index 808cd93..778a295 100644
--- a/pages/picking/picking.vue
+++ b/pages/picking/picking.vue
@@ -107,7 +107,7 @@
-
+ 已同步服务端
@@ -343,7 +343,7 @@
import { navigateBackOr } from '@/utils/navigation.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
- import { claimContainerPickingTask, completeContainerPickingTask, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, isRemoteReady, listLoadingOrders, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, voidEvidence } from '@/api/pda-loading.js'
+ import { claimContainerPickingTask, completeContainerPickingTask, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, isRemoteReady, listLoadingOrders, normalizePickingTask, partialContainerItem, pickContainerItem, pickContainerPallet, skipContainerItem, submitContainerPickingBatch, uploadEvidenceFile, voidEvidence } from '@/api/pda-loading.js'
import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
import { isOnline } from '@/utils/network.js'
import { stableOperationId } from '@/utils/idempotency.js'
@@ -973,7 +973,7 @@
const pallet = isSealedPalletItem(item) && item.palletNo ? `${item.palletNo} · ` : ''
return `${pallet}${item.desc}${reason}`
},
- processScannedValue(scanValue, source = 'broadcast') {
+ async processScannedValue(scanValue, source = 'broadcast') {
if (!this.requirePickingOperation()) return
const value = scanValue.trim()
if (!value) {
@@ -1001,9 +1001,10 @@
this.showToast(`未找到匹配的待拣条码:${value}`, 'error')
return
}
- playSuccessSound()
- if (result.type === 'pallet') this.pickScannedPallet(result.item, value)
- else this.pickScannedBox(result.item, value)
+ const submitted = result.type === 'pallet'
+ ? await this.pickScannedPallet(result.item, value)
+ : await this.pickScannedBox(result.item, value)
+ if (submitted) playSuccessSound()
},
evidenceEntries(type) { return Array.isArray(this.evidenceFiles[type]) ? this.evidenceFiles[type].filter((item) => item.status !== 'VOID' && item.status !== 'VOIDED') : (this.evidenceFiles[type] ? [this.evidenceFiles[type]] : []) },
evidencePreviewSource(photo) { return photo?.localPath || photo?.fileUrl || photo?.downloadUrl || photo?.url || '' },
@@ -1427,8 +1428,11 @@
if (this.isEvidenceContextActive(contextId, identity)) this.submitting = false
}
},
- pickScannedBox(item, boxCode) {
- const pickedCount = Number(this.partialQuantities[item.id] || 0) + 1
+ async pickScannedBox(item, boxCode) {
+ const submitted = await this.submitImmediateItemAction(item, 'PICK', { pickedPackageCount: 1, boxCode })
+ if (!submitted) return false
+ const latest = this.currentBox.items.find((entry) => String(entry.id) === String(item.id)) || item
+ const pickedCount = Number(latest.pickedPackageCount || 0)
const plannedCount = Number(item.qty || 0)
this.scannedBoxCodes = { ...this.scannedBoxCodes, [boxCode]: item.id }
this.partialQuantities = { ...this.partialQuantities, [item.id]: pickedCount }
@@ -1438,8 +1442,11 @@
`当前明细 · 已拣 ${pickedCount}/${plannedCount} 箱`)
this.showToast(`已拣箱 ${pickedCount}/${plannedCount}`, 'success')
this.persistSession()
+ return true
},
- pickScannedPallet(item, palletNo) {
+ async pickScannedPallet(item, palletNo) {
+ const submitted = await this.submitImmediateItemAction(item, 'PICK_PALLET', { palletNo: item.palletNo })
+ if (!submitted) return false
const plannedCount = Number(item.qty || 0)
this.scannedBoxCodes = { ...this.scannedBoxCodes, [item.palletNo]: item.id, [palletNo]: item.id }
this.partialQuantities = { ...this.partialQuantities, [item.id]: plannedCount }
@@ -1447,13 +1454,16 @@
this.recordScanResult('success', '整托扫描成功', palletNo, `已定位托盘 ${item.palletNo} · 共 ${plannedCount} 箱`)
this.showToast(`托盘 ${item.palletNo} 已确认装柜`, 'success')
this.persistSession()
+ return true
},
- pickItem(id, source = 'manual') {
+ async pickItem(id, source = 'manual') {
if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id)
if (isSealedPalletItem(item)) { this.showToast(`已封托货物必须扫描托盘码 ${item.palletNo}`, 'warning'); return }
const available = this.availablePickQuantity(item)
if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return }
+ const remaining = Number(item?.qty || 0) - Number(item?.pickedPackageCount || 0)
+ if (remaining < 1 || !await this.submitImmediateItemAction(item, 'PICK', { pickedPackageCount: remaining, source })) return
this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED }
this.partialQuantities = { ...this.partialQuantities, [id]: Number(item?.qty || 0) }
this.showToast(item ? `已确认拣出 ${item.qty} 箱` : '已确认拣出', 'success')
@@ -1463,19 +1473,6 @@
const entry = { id: `${Date.now()}-${this.scanHistory.length}-${boxCode}`, type, title, boxCode, detail }
this.scanHistory = [entry, ...this.scanHistory].slice(0, 3)
},
- undoItem(id) {
- if (!this.requirePickingOperation()) return
- const item = this.currentBox.items.find((entry) => entry.id === id)
- if (item) item.skipReason = ''
- this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PENDING }
- const nextPartialQuantities = { ...this.partialQuantities }
- delete nextPartialQuantities[id]
- this.partialQuantities = nextPartialQuantities
- this.scannedBoxCodes = Object.fromEntries(Object.entries(this.scannedBoxCodes)
- .filter(([, itemId]) => String(itemId) !== String(id)))
- this.showToast('已撤销操作', 'success')
- this.persistSession()
- },
openSkip(id) {
if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id)
@@ -1484,16 +1481,17 @@
this.selectedSkipReason = ''
this.openOverlay('skip')
},
- confirmSkip() {
+ async confirmSkip() {
if (!this.requirePickingOperation()) return
if (!this.selectedSkipReason) {
this.showToast('请先选择跳拣原因', 'warning')
return
}
const item = this.currentBox.items.find((entry) => entry.id === this.skipTargetId)
+ const reason = this.selectedSkipReason
+ if (!item || !await this.submitImmediateItemAction(item, 'SKIP', { reason })) return
if (item) item.skipReason = this.selectedSkipReason
this.itemStates = { ...this.itemStates, [this.skipTargetId]: ITEM_STATUS.SKIPPED }
- const reason = this.selectedSkipReason
this.closeOverlay(false)
this.showToast(`已标记跳拣:${reason}`, 'warning')
this.persistSession()
@@ -1516,7 +1514,7 @@
this.partialFocused = true
})
},
- confirmPartial() {
+ async confirmPartial() {
if (!this.requirePickingOperation()) return
const item = this.partialTargetItem
if (!item) return
@@ -1524,6 +1522,7 @@
const value = parseInt(this.partialInput, 10)
const available = this.availablePickQuantity(item)
if (available !== null && value > available) { this.showToast(`超过库存/库位可用量:可拣 ${available}`, 'error'); return }
+ if (!await this.submitImmediateItemAction(item, 'PARTIAL', { finalPickedPackageCount: value })) return
this.itemStates = { ...this.itemStates, [item.id]: ITEM_STATUS.PARTIAL }
this.partialQuantities = { ...this.partialQuantities, [item.id]: value }
this.closeOverlay(false)