fix: support whole-pallet picking by carton scan

This commit is contained in:
2026-09-11 18:15:25 +08:00
parent 28c574ae36
commit e41f708570
2 changed files with 35 additions and 26 deletions
+10
View File
@@ -97,6 +97,16 @@ export function pickContainerItem(taskId, itemId, payload = {}) {
return itemAction(taskId, itemId, 'pick', payload, payload.operationId) 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 = {}) { export function partialContainerItem(taskId, itemId, payload = {}) {
return itemAction(taskId, itemId, 'partial', payload, payload.operationId) return itemAction(taskId, itemId, 'partial', payload, payload.operationId)
} }
+25 -26
View File
@@ -107,7 +107,7 @@
<button class="pick-btn pick-btn-skip" @tap="openSkip(item.id)">跳拣</button> <button class="pick-btn pick-btn-skip" @tap="openSkip(item.id)">跳拣</button>
</view> </view>
<view v-else-if="pickingOperationReady" class="pick-item-actions processed-actions"> <view v-else-if="pickingOperationReady" class="pick-item-actions processed-actions">
<button class="pick-btn pick-btn-undo" @tap="undoItem(item.id)">撤销</button> <text class="version-hint">已同步服务端</text>
</view> </view>
</view> </view>
</view> </view>
@@ -343,7 +343,7 @@
import { navigateBackOr } from '@/utils/navigation.js' import { navigateBackOr } from '@/utils/navigation.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.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 { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
import { isOnline } from '@/utils/network.js' import { isOnline } from '@/utils/network.js'
import { stableOperationId } from '@/utils/idempotency.js' import { stableOperationId } from '@/utils/idempotency.js'
@@ -973,7 +973,7 @@
const pallet = isSealedPalletItem(item) && item.palletNo ? `${item.palletNo} · ` : '' const pallet = isSealedPalletItem(item) && item.palletNo ? `${item.palletNo} · ` : ''
return `${pallet}${item.desc}${reason}` return `${pallet}${item.desc}${reason}`
}, },
processScannedValue(scanValue, source = 'broadcast') { async processScannedValue(scanValue, source = 'broadcast') {
if (!this.requirePickingOperation()) return if (!this.requirePickingOperation()) return
const value = scanValue.trim() const value = scanValue.trim()
if (!value) { if (!value) {
@@ -1001,9 +1001,10 @@
this.showToast(`未找到匹配的待拣条码:${value}`, 'error') this.showToast(`未找到匹配的待拣条码:${value}`, 'error')
return return
} }
playSuccessSound() const submitted = result.type === 'pallet'
if (result.type === 'pallet') this.pickScannedPallet(result.item, value) ? await this.pickScannedPallet(result.item, value)
else this.pickScannedBox(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]] : []) }, 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 || '' }, evidencePreviewSource(photo) { return photo?.localPath || photo?.fileUrl || photo?.downloadUrl || photo?.url || '' },
@@ -1427,8 +1428,11 @@
if (this.isEvidenceContextActive(contextId, identity)) this.submitting = false if (this.isEvidenceContextActive(contextId, identity)) this.submitting = false
} }
}, },
pickScannedBox(item, boxCode) { async pickScannedBox(item, boxCode) {
const pickedCount = Number(this.partialQuantities[item.id] || 0) + 1 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) const plannedCount = Number(item.qty || 0)
this.scannedBoxCodes = { ...this.scannedBoxCodes, [boxCode]: item.id } this.scannedBoxCodes = { ...this.scannedBoxCodes, [boxCode]: item.id }
this.partialQuantities = { ...this.partialQuantities, [item.id]: pickedCount } this.partialQuantities = { ...this.partialQuantities, [item.id]: pickedCount }
@@ -1438,8 +1442,11 @@
`当前明细 · 已拣 ${pickedCount}/${plannedCount} 箱`) `当前明细 · 已拣 ${pickedCount}/${plannedCount} 箱`)
this.showToast(`已拣箱 ${pickedCount}/${plannedCount}`, 'success') this.showToast(`已拣箱 ${pickedCount}/${plannedCount}`, 'success')
this.persistSession() 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) const plannedCount = Number(item.qty || 0)
this.scannedBoxCodes = { ...this.scannedBoxCodes, [item.palletNo]: item.id, [palletNo]: item.id } this.scannedBoxCodes = { ...this.scannedBoxCodes, [item.palletNo]: item.id, [palletNo]: item.id }
this.partialQuantities = { ...this.partialQuantities, [item.id]: plannedCount } this.partialQuantities = { ...this.partialQuantities, [item.id]: plannedCount }
@@ -1447,13 +1454,16 @@
this.recordScanResult('success', '整托扫描成功', palletNo, `已定位托盘 ${item.palletNo} · 共 ${plannedCount} 箱`) this.recordScanResult('success', '整托扫描成功', palletNo, `已定位托盘 ${item.palletNo} · 共 ${plannedCount} 箱`)
this.showToast(`托盘 ${item.palletNo} 已确认装柜`, 'success') this.showToast(`托盘 ${item.palletNo} 已确认装柜`, 'success')
this.persistSession() this.persistSession()
return true
}, },
pickItem(id, source = 'manual') { async pickItem(id, source = 'manual') {
if (!this.requirePickingOperation()) return if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id) const item = this.currentBox.items.find((entry) => entry.id === id)
if (isSealedPalletItem(item)) { this.showToast(`已封托货物必须扫描托盘码 ${item.palletNo}`, 'warning'); return } if (isSealedPalletItem(item)) { this.showToast(`已封托货物必须扫描托盘码 ${item.palletNo}`, 'warning'); return }
const available = this.availablePickQuantity(item) const available = this.availablePickQuantity(item)
if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return } 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.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED }
this.partialQuantities = { ...this.partialQuantities, [id]: Number(item?.qty || 0) } this.partialQuantities = { ...this.partialQuantities, [id]: Number(item?.qty || 0) }
this.showToast(item ? `已确认拣出 ${item.qty} 箱` : '已确认拣出', 'success') this.showToast(item ? `已确认拣出 ${item.qty} 箱` : '已确认拣出', 'success')
@@ -1463,19 +1473,6 @@
const entry = { id: `${Date.now()}-${this.scanHistory.length}-${boxCode}`, type, title, boxCode, detail } const entry = { id: `${Date.now()}-${this.scanHistory.length}-${boxCode}`, type, title, boxCode, detail }
this.scanHistory = [entry, ...this.scanHistory].slice(0, 3) 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) { openSkip(id) {
if (!this.requirePickingOperation()) return if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id) const item = this.currentBox.items.find((entry) => entry.id === id)
@@ -1484,16 +1481,17 @@
this.selectedSkipReason = '' this.selectedSkipReason = ''
this.openOverlay('skip') this.openOverlay('skip')
}, },
confirmSkip() { async confirmSkip() {
if (!this.requirePickingOperation()) return if (!this.requirePickingOperation()) return
if (!this.selectedSkipReason) { if (!this.selectedSkipReason) {
this.showToast('请先选择跳拣原因', 'warning') this.showToast('请先选择跳拣原因', 'warning')
return return
} }
const item = this.currentBox.items.find((entry) => entry.id === this.skipTargetId) 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 if (item) item.skipReason = this.selectedSkipReason
this.itemStates = { ...this.itemStates, [this.skipTargetId]: ITEM_STATUS.SKIPPED } this.itemStates = { ...this.itemStates, [this.skipTargetId]: ITEM_STATUS.SKIPPED }
const reason = this.selectedSkipReason
this.closeOverlay(false) this.closeOverlay(false)
this.showToast(`已标记跳拣:${reason}`, 'warning') this.showToast(`已标记跳拣:${reason}`, 'warning')
this.persistSession() this.persistSession()
@@ -1516,7 +1514,7 @@
this.partialFocused = true this.partialFocused = true
}) })
}, },
confirmPartial() { async confirmPartial() {
if (!this.requirePickingOperation()) return if (!this.requirePickingOperation()) return
const item = this.partialTargetItem const item = this.partialTargetItem
if (!item) return if (!item) return
@@ -1524,6 +1522,7 @@
const value = parseInt(this.partialInput, 10) const value = parseInt(this.partialInput, 10)
const available = this.availablePickQuantity(item) const available = this.availablePickQuantity(item)
if (available !== null && value > available) { this.showToast(`超过库存/库位可用量:可拣 ${available}`, 'error'); return } 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.itemStates = { ...this.itemStates, [item.id]: ITEM_STATUS.PARTIAL }
this.partialQuantities = { ...this.partialQuantities, [item.id]: value } this.partialQuantities = { ...this.partialQuantities, [item.id]: value }
this.closeOverlay(false) this.closeOverlay(false)