fix(pda): stage sealed pallet scans for atomic batch picking
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
<view v-if="activeStage === 'picking'" class="picking-stage">
|
||||
<view class="scan-section">
|
||||
<view class="scan-row">
|
||||
<ScanTargetInput v-model="scanValue" name="boxCode" label="箱码扫码框" placeholder="扫描或输入箱唛标签码" :active="activeScanField === 'boxCode'" @activate="setScanTarget" @submit="handleManualSubmit" />
|
||||
<ScanTargetInput v-model="scanValue" name="boxCode" :label="hasPalletItems ? '箱码或托盘码扫码框' : '箱码扫码框'" :placeholder="hasPalletItems ? '扫描 DP 托盘码或箱唛标签码' : '扫描或输入箱唛标签码'" :active="activeScanField === 'boxCode'" @activate="setScanTarget" @submit="handleManualSubmit" />
|
||||
</view>
|
||||
<view v-if="lastScanResult" class="last-scan" :class="lastScanResult.type" role="status">
|
||||
<view class="last-scan-icon">{{ lastScanResult.type === 'success' ? '✓' : '!' }}</view>
|
||||
@@ -78,7 +78,7 @@
|
||||
:class="itemState(item)"
|
||||
>
|
||||
<view class="pick-item-row1">
|
||||
<text class="pick-item-sku">拣货明细</text>
|
||||
<text class="pick-item-sku">{{ isPalletItem(item) ? (item.palletNo || '托盘码待同步') + ' · 整托不可拆' : '拣货明细' }}</text>
|
||||
<view class="pick-item-meta">
|
||||
<text v-if="itemState(item) === ITEM_STATUS.PICKED" class="pick-status-tag tag-picked">已拣 ✓</text>
|
||||
<text v-else-if="itemState(item) === ITEM_STATUS.PARTIAL" class="pick-status-tag tag-partial">
|
||||
@@ -98,12 +98,12 @@
|
||||
<text v-if="item.location" class="pick-item-location">{{ item.location }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="itemState(item) === ITEM_STATUS.PENDING" class="pick-item-actions">
|
||||
<view v-if="itemState(item) === ITEM_STATUS.PENDING && !isPalletItem(item)" class="pick-item-actions">
|
||||
<button class="pick-btn pick-btn-confirm" @tap="pickItem(item.id)">确认拣出</button>
|
||||
<button class="pick-btn pick-btn-partial" @tap="openPartial(item.id)">部分拣货</button>
|
||||
<button class="pick-btn pick-btn-skip" @tap="openSkip(item.id)">跳拣</button>
|
||||
</view>
|
||||
<view v-else-if="pickingOperationReady" class="pick-item-actions processed-actions">
|
||||
<view v-else-if="isPalletItem(item) && itemState(item) === ITEM_STATUS.PENDING" class="pick-item-actions"><text>请扫描上方 DP 托盘码整托拣出</text></view><view v-else-if="isPalletItem(item) && pickingOperationReady && ['PENDING', 'PICKING'].includes(item.itemStatus)" class="pick-item-actions"><button class="pick-btn pick-btn-undo" @tap="undoPallet(item)">撤销整托草稿</button></view><view v-else-if="pickingOperationReady && !isPalletItem(item)" class="pick-item-actions processed-actions">
|
||||
<button class="pick-btn pick-btn-undo" @tap="undoItem(item.id)">撤销</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -333,7 +333,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates } from '@/utils/picking-state.js'
|
||||
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates, isPalletItem } from '@/utils/picking-state.js'
|
||||
import { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js'
|
||||
@@ -395,6 +395,7 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPalletItems() { return this.currentBox.items.some(isPalletItem) },
|
||||
currentBox() {
|
||||
return this.boxes[this.currentBoxIndex] || { boxNo: '', orderNo: '', inboundCode: '', items: [] }
|
||||
},
|
||||
@@ -561,6 +562,24 @@
|
||||
return false
|
||||
},
|
||||
methods: {
|
||||
isPalletItem,
|
||||
undoPallet(item) {
|
||||
if (!this.requirePickingOperation()) return
|
||||
const members = this.currentBox.items.filter((entry) => isPalletItem(entry) && entry.palletNo === item.palletNo)
|
||||
if (members.some((entry) => !['PENDING', 'PICKING'].includes(entry.itemStatus))) return
|
||||
for (const member of members) { this.itemStates[member.id] = ITEM_STATUS.PENDING; delete this.partialQuantities[member.id] }
|
||||
delete this.scannedBoxCodes[item.palletNo]
|
||||
this.persistSession()
|
||||
},
|
||||
pickScannedPallet(item) {
|
||||
if (this.submitting || !this.requirePickingOperation() || !item.palletNo) return
|
||||
const members = this.currentBox.items.filter((entry) => isPalletItem(entry) && entry.palletNo === item.palletNo)
|
||||
if (members.some((entry) => !['PENDING', 'PICKING'].includes(entry.itemStatus))) { this.showToast('托盘状态已变化,请刷新任务', 'warning'); return }
|
||||
for (const member of members) { this.itemStates[member.id] = ITEM_STATUS.PICKED; this.partialQuantities[member.id] = Number(member.qty) }
|
||||
this.scannedBoxCodes = { ...this.scannedBoxCodes, [item.palletNo]: item.id }
|
||||
this.recordScanResult('success', '整托已记入草稿', item.palletNo, `整托 ${members.reduce((sum, entry) => sum + Number(entry.qty), 0)} 箱,提交后保存`)
|
||||
this.showToast('整托已拣,待统一提交', 'success'); this.persistSession()
|
||||
},
|
||||
containerReference,
|
||||
isSelectableTask(task) {
|
||||
return Boolean(task?.recordStatus === 'ACTIVE'
|
||||
@@ -813,6 +832,10 @@
|
||||
Object.entries(restored.itemStates || {}).forEach(([id, status]) => {
|
||||
if (!itemIds.has(String(id)) || serverStates[id] !== ITEM_STATUS.PENDING || status === ITEM_STATUS.PENDING) return
|
||||
const item = this.currentBox.items.find((entry) => String(entry.id) === String(id))
|
||||
if (isPalletItem(item)) {
|
||||
const members = this.currentBox.items.filter((entry) => isPalletItem(entry) && entry.palletNo === item.palletNo)
|
||||
if (!item.palletNo || !restored.scannedBoxCodes?.[item.palletNo] || status !== ITEM_STATUS.PICKED || members.some((entry) => restored.itemStates?.[entry.id] !== ITEM_STATUS.PICKED || Number(restored.partialQuantities?.[entry.id]) !== Number(entry.qty) || !['PENDING', 'PICKING'].includes(entry.itemStatus))) return
|
||||
}
|
||||
const partial = Number(restored.partialQuantities?.[id] || 0)
|
||||
if (status === ITEM_STATUS.PARTIAL && (!partial || partial >= Number(item?.qty || 0))) return
|
||||
serverStates[id] = status
|
||||
@@ -820,7 +843,7 @@
|
||||
})
|
||||
this.itemStates = serverStates
|
||||
this.partialQuantities = serverPartialQuantities
|
||||
this.scannedBoxCodes = Object.fromEntries(Object.entries(restored.scannedBoxCodes || {}).filter(([, id]) => itemIds.has(String(id))))
|
||||
this.scannedBoxCodes = Object.fromEntries(Object.entries(restored.scannedBoxCodes || {}).filter(([, id]) => itemIds.has(String(id)) && (!isPalletItem(this.currentBox.items.find((item) => String(item.id) === String(id))) || serverStates[id] === ITEM_STATUS.PICKED)))
|
||||
this.boxDrafts = restored.boxDrafts || {}
|
||||
this.completedBoxes = restored.completedBoxes || {}
|
||||
if (this.remotePick && !isPickingCompleteTask(this.remotePick)) {
|
||||
@@ -983,6 +1006,7 @@
|
||||
return
|
||||
}
|
||||
playSuccessSound()
|
||||
if (isPalletItem(result.item)) return this.pickScannedPallet(result.item)
|
||||
this.pickScannedBox(result.item, value)
|
||||
},
|
||||
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]] : []) },
|
||||
@@ -1422,6 +1446,7 @@
|
||||
pickItem(id, source = 'manual') {
|
||||
if (!this.requirePickingOperation()) return
|
||||
const item = this.currentBox.items.find((entry) => entry.id === id)
|
||||
if (isPalletItem(item)) { this.showToast('请扫描托盘码整体拣出', 'warning'); return }
|
||||
const available = this.availablePickQuantity(item)
|
||||
if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return }
|
||||
this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED }
|
||||
@@ -1448,6 +1473,7 @@
|
||||
},
|
||||
openSkip(id) {
|
||||
if (!this.requirePickingOperation()) return
|
||||
if (isPalletItem(this.currentBox.items.find((item) => item.id === id))) return
|
||||
this.skipTargetId = id
|
||||
this.selectedSkipReason = ''
|
||||
this.openOverlay('skip')
|
||||
@@ -1468,6 +1494,7 @@
|
||||
},
|
||||
openPartial(id) {
|
||||
if (!this.requirePickingOperation()) return
|
||||
if (isPalletItem(this.currentBox.items.find((item) => item.id === id))) return
|
||||
this.partialTargetId = id
|
||||
this.partialInput = ''
|
||||
this.openOverlay('partial')
|
||||
@@ -1486,6 +1513,7 @@
|
||||
if (!this.requirePickingOperation()) return
|
||||
const item = this.partialTargetItem
|
||||
if (!item) return
|
||||
if (isPalletItem(item)) return
|
||||
if (!this.partialInputValid) return
|
||||
const value = parseInt(this.partialInput, 10)
|
||||
const available = this.availablePickQuantity(item)
|
||||
@@ -1562,6 +1590,7 @@
|
||||
const actions = this.currentBox.items.map((item) => {
|
||||
if (item.itemStatus && !['PENDING', 'PICKING'].includes(item.itemStatus)) return null
|
||||
const status = this.itemState(item)
|
||||
if (isPalletItem(item)) { if (status !== ITEM_STATUS.PICKED) throw new Error('已封托货物须先扫描 DP 托盘码整托拣出'); return { type: 'PICK_PALLET', itemId: item.taskItemId ?? item.id, palletNo: item.palletNo, version: Number(item.version || 0) } }
|
||||
const itemId = item.taskItemId ?? item.remoteItemId ?? item.id
|
||||
if (status === ITEM_STATUS.PICKED) return { type: 'PICK', itemId, pickedPackageCount: Number(item.qty || 0), locationIds: item.locationIds || [], version: Number(item.version || 0) }
|
||||
if (status === ITEM_STATUS.PARTIAL) return { type: 'PARTIAL', itemId, finalPickedPackageCount: Number(this.partialQuantities[item.id] || 0), locationIds: item.locationIds || [], version: Number(item.version || 0) }
|
||||
|
||||
Reference in New Issue
Block a user