fix(pda): stage sealed pallet scans for atomic batch picking

This commit is contained in:
2026-09-09 12:29:46 +08:00
parent 0745e9600c
commit b19cecb54f
10 changed files with 136 additions and 11 deletions
@@ -21,8 +21,8 @@
<view v-if="hasDifference" class="difference-box"><text>实收与应收不一致,请确认接受差异并填写处理结果。</text><label><checkbox :checked="acceptVariance" @tap="acceptVariance = !acceptVariance" />接受本次数量差异</label><textarea v-model="discrepancyRemark" maxlength="500" placeholder="填写差异原因及处理结果" /></view>
<button class="primary-button" :disabled="busy || !online || !canConfirmCarton" @tap="confirmCarton">确认本箱实收</button>
</view>
<view class="panel"><view class="heading"><text class="panel-title">待绑托箱</text><text class="badge">{{ task.unboundCartons || 0 }}</text></view><view v-if="!unboundCartons.length" class="empty">暂无待绑托箱</view><view v-for="carton in unboundCartons" :key="carton.cartonId" class="carton-row"><view><text>{{ carton.boxCode }}</text><text>{{ carton.skuCode }} · 实收 {{ carton.actualSkuQuantity }}</text></view></view></view>
<view class="panel"><view class="heading"><text class="panel-title">托盘</text><text class="minor">{{ pallets.length }} 托</text></view><view v-if="!pallets.length" class="empty">扫描 DP 托盘码开始组托</view>
<view class="panel"><button class="primary-button" :disabled="busy || !online || task.taskStatus !== 'CLAIMED' || !unboundCartons.length" @tap="createPallet">新建托盘并绑定</button><view class="heading"><text class="panel-title">待绑托箱</text><text class="badge">{{ task.unboundCartons || 0 }}</text></view><view v-if="!unboundCartons.length" class="empty">暂无待绑托箱</view><view v-for="carton in unboundCartons" :key="carton.cartonId" class="carton-row"><view><text>{{ carton.boxCode }}</text><text>{{ carton.skuCode }} · 实收 {{ carton.actualSkuQuantity }}</text></view></view></view>
<view class="panel"><view class="heading"><text class="panel-title">托盘</text><text class="minor">{{ pallets.length }} 托</text></view><view v-if="!pallets.length" class="empty">清点箱码后新建托盘,或扫描已有 DP 托盘码</view>
<view v-for="pallet in pallets" :key="pallet.palletId" class="pallet-card" :class="{ active: pallet.palletNo === activePalletNo }">
<view class="heading"><view><text class="pallet-code">{{ pallet.palletNo }}</text><text class="minor">{{ pallet.cartonCount || 0 }} 箱 · {{ pallet.skuCount || 0 }} SKU</text></view><text class="badge" :class="{ sealed: pallet.status === 'SEALED' }">{{ pallet.status === 'SEALED' ? '已封托' : '组托中' }}</text></view>
<view v-if="pallet.weightKg != null || pallet.volumeCbm != null" class="measure-row"><text>重量 {{ pallet.weightKg ?? '—' }} kg</text><text>体积 {{ pallet.volumeCbm ?? '—' }} m³</text></view>
@@ -38,7 +38,7 @@
<script>
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { bindPendingDropshipCartons, countDropshipCarton, getDropshipPalletizingTask, moveDropshipCarton, resolveDropshipCarton, sealDropshipPallet } from '@/api/pda-dropship.js'
import { createAndBindDropshipPallet, bindPendingDropshipCartons, countDropshipCarton, getDropshipPalletizingTask, moveDropshipCarton, resolveDropshipCarton, sealDropshipPallet } from '@/api/pda-dropship.js'
import { session, requireSession } from '@/stores/session.js'
import { clearDropshipPalletizingDraft, restoreDropshipPalletizingDraft, saveDropshipPalletizingDraft } from '@/stores/dropship-palletizing.js'
import { stableOperationId } from '@/utils/idempotency.js'
@@ -61,6 +61,19 @@
onLoad(options) { if (!requireSession()) return; this.user = session.currentUser; this.taskId = String(options.taskId || ''); this.warehouseId = String(options.warehouseId || ''); const draft = restoreDropshipPalletizingDraft(this.draftIdentity); if (draft?.activePalletNo) this.activePalletNo = draft.activePalletNo; this.scanState = createDropshipScanState({ activePalletNo: this.activePalletNo }); this._stopNetwork = watchNetwork((value) => { this.online = value; this.scanState.online = value }) },
onShow() { if (!this.taskId || !this.warehouseId) { this.error = '任务参数缺失,请返回任务列表刷新'; return }; this.startScanner(); this.loadTask() }, onHide() { this.stopScanner() }, onUnload() { this.stopScanner(); if (this._stopNetwork) this._stopNetwork() },
methods: {
async createPallet() {
if (this.busy || !this.online || this.task?.taskStatus !== 'CLAIMED' || !this.unboundCartons.length) return
const cartonIds = this.unboundCartons.map((carton) => String(carton.cartonId)).sort()
const payload = { warehouseId: this.warehouseId, operationId: stableOperationId('dropship-create-bind', `${this.user?.id}-${this.taskId}`, { warehouseId: this.warehouseId, taskVersion: this.task.version, cartonIds }) }
this.busy = true; this.error = ''
try {
this.task = await createAndBindDropshipPallet(this.taskId, payload)
const pallet = this.pallets.find((item) => (item.cartons || []).some((carton) => cartonIds.includes(String(carton.cartonId))))
if (pallet) { this.activePalletNo = pallet.palletNo; this.scanState.activePalletNo = pallet.palletNo; this.persistDraft() }
this.notice = `已新建托盘 ${pallet?.palletNo || ''} 并绑定待绑托箱`; playSuccessSound()
} catch (error) { this.error = formatApiError(error, '新建托盘失败,请重试'); playErrorSound() }
finally { this.busy = false }
},
statusLabel(status) { return status === 'COMPLETED' ? '已完成' : status === 'CLAIMED' ? '清点组托中' : '待领取' }, palletCartons(pallet) { return pallet.cartons || [] },
startScanner() { if (!this._stopScanner) this._stopScanner = startScanBroadcast({ onScan: (value) => this.processScan(value), onError: () => playErrorSound() }) }, stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
async loadTask() { if (!this.online) { this.error = '当前网络不可用,请联网后刷新'; return }; this.loading = true; this.error = ''; try { this.task = await getDropshipPalletizingTask(this.taskId, this.warehouseId); if (this.task?.taskStatus === 'COMPLETED') { this.activePalletNo = ''; this.scanState = createDropshipScanState(); clearDropshipPalletizingDraft(this.draftIdentity) } } catch (error) { this.error = formatApiError(error, '清点组托任务加载失败') } finally { this.loading = false } },