fix: support whole-pallet picking by carton scan
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+25
-26
@@ -107,7 +107,7 @@
|
||||
<button class="pick-btn pick-btn-skip" @tap="openSkip(item.id)">跳拣</button>
|
||||
</view>
|
||||
<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>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user