优化
This commit is contained in:
@@ -43,7 +43,7 @@ export function normalizeDispatchTask(value) {
|
||||
...value, taskNo: String(value.taskNo || ''), dispatchNo: String(value.dispatchNo),
|
||||
recordVersion: value.recordVersion, pickingReconciled: value.pickingReconciled === true,
|
||||
actions: Object.fromEntries(Object.entries(value.actions || {}).map(([key, gate]) => [key, { ...gate, allowed: gate?.allowed === true }])),
|
||||
items: value.items.map(item => ({ ...item, dispatchItemId: String(item.dispatchItemId || ''), palletNo: String(item.palletNo || ''),
|
||||
items: value.items.map(item => ({ ...item, dispatchItemId: String(item.dispatchItemId || ''), sourceKey: String(item.sourceKey || ''), sourceBusinessType: item.sourceBusinessType == null ? null : String(item.sourceBusinessType), sourceBusinessNo: item.sourceBusinessNo == null ? null : String(item.sourceBusinessNo), palletNo: String(item.palletNo || ''),
|
||||
scheduledPackages: dispatchCount(item.scheduledPackages), pickedPackages: dispatchCount(item.pickedPackages),
|
||||
returnCartons: (item.returnCartons || []).map(carton => ({ ...carton, returnCartonId: String(carton.returnCartonId || ''), cartonMark: String(carton.cartonMark || ''), productQuantity: dispatchCount(carton.productQuantity) })),
|
||||
positions: (item.positions || []).map(position => ({ ...position, positionId: String(position.positionId || ''), balanceId: String(position.balanceId || ''), availablePackages: dispatchCount(position.availablePackages), allocatedPackages: dispatchCount(position.allocatedPackages), pickedPackages: dispatchCount(position.pickedPackages) }))
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
<template v-if="!current && !loading"><view class="list-toolbar"><text class="list-title">排车任务</text><button :disabled="busy" @tap="loadTasks">刷新</button></view><view v-if="!tasks.length && !error" class="fulfillment-state">暂无排车任务,可扫描排车单号读取。</view><button v-for="task in tasks" :key="task.dispatchNo" class="task-card" @tap="openTask(task.dispatchNo)"><text class="business-code">{{ task.dispatchNo }}</text><text v-if="task.dispatchNo === recentTaskNo" class="recent-badge">上次作业</text><text class="status-badge">{{ stageLabel(task) }}</text><text class="task-meta">任务号 {{ task.taskNo || '—' }}</text></button></template>
|
||||
<template v-if="current"><DispatchHandoverStatus v-if="departed" :dispatch-no="current.dispatchNo" :record-version="current.recordVersion" />
|
||||
<view class="fulfillment-card summary-card"><view class="task-head"><text class="business-code">{{ current.dispatchNo }}</text><text class="status-badge">{{ stageLabel(current) }}</text></view><text class="task-meta">任务号 {{ current.taskNo || '—' }}</text><text v-if="!gate('PICK') && current.stage === 'LOADING'" class="gate-warning">{{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }}</text><view class="metrics"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value">{{ pickedPackages }}</text><text>真实已拣</text></view><view><text class="metric-value">{{ remainingPackages }}</text><text>未拣箱数</text></view></view></view>
|
||||
<view v-for="item in current.items" :key="item.dispatchItemId" class="fulfillment-card item-card" :class="{ selected: selectedItem?.dispatchItemId === item.dispatchItemId }" @tap="selectPallet(item)"><view class="task-head"><text class="business-code">{{ item.palletNo || '未关联托盘码' }}</text><text>{{ item.pickedPackages }}/{{ item.scheduledPackages }}箱</text></view><text class="task-meta">{{ item.customerName || '—' }} · {{ routeLabel(item) }}</text><text v-for="carton in item.returnCartons" :key="carton.returnCartonId" class="task-meta">{{ carton.cartonMark }} · {{ carton.status === 'OUTBOUNDED' ? '已拣货' : '待扫描' }}</text><view class="progress"><view :style="{ width: `${itemProgress(item)}%` }"></view></view></view>
|
||||
<view v-for="item in current.items" :key="item.dispatchItemId" class="fulfillment-card item-card" :class="{ selected: selectedItem?.dispatchItemId === item.dispatchItemId }" @tap="selectPallet(item)"><view class="task-head"><text class="business-code">{{ item.palletNo || '未关联托盘码' }}</text><text>{{ item.pickedPackages }}/{{ item.scheduledPackages }}箱</text></view><text class="task-meta">{{ item.sourceBusinessType === 'RELABEL_ORDER' ? '换标单号' : '来源业务号' }} {{ item.sourceBusinessNo || '—' }} · {{ item.customerName || '—' }} · {{ routeLabel(item) }}</text><text v-for="carton in item.returnCartons" :key="carton.returnCartonId" class="task-meta">{{ carton.cartonMark }} · {{ carton.status === 'OUTBOUNDED' ? '已拣货' : '待扫描' }}</text><view class="progress"><view :style="{ width: `${itemProgress(item)}%` }"></view></view></view>
|
||||
<view v-if="!current.items.length" class="fulfillment-state">本车暂无货物,空单不能核平、装车或发车。</view>
|
||||
<view v-if="sourceChoices.length > 1" class="fulfillment-card"><text class="card-title">请选择本次真实拣货来源</text><button v-for="item in sourceChoices" :key="item.dispatchItemId" :disabled="busy || Boolean(pendingCommand)" @tap="chooseSource(item)">{{ item.sourceBusinessNo || '—' }} · {{ item.customerName || '—' }} · 可拣{{ pickablePackages(item) }}箱</button></view>
|
||||
<view v-if="selectedItem && selectedRemaining > 0" class="fulfillment-card"><text class="card-title">本次拣货:{{ selectedItem.palletNo || '来源箱唛' }}</text><text v-if="selectedItem.returnCartons.length">{{ selectedReturnCarton ? `已扫 ${selectedReturnCarton.cartonMark}` : '请扫描来源箱唛,每次一箱' }}</text><view v-else class="count-row"><text>拣货箱数</text><input type="number" :value="packages" :placeholder="`请输入 1-${selectedRemaining} 箱`" :disabled="busy || Boolean(pendingCommand)" @input="updatePackages" /><text>最多{{ selectedRemaining }}箱</text></view><button class="primary-button" :disabled="!canPick || busy || Boolean(pendingCommand)" @tap="submitPick">确认真实拣货</button></view>
|
||||
<view v-if="current.stage === 'LOADING'" class="fulfillment-card gate-card"><text class="card-title">确认装车完成</text><text>{{ current.pickingReconciled ? 'PDA拣货已核平,确认实际装载后进入装车后。' : '先完成全部真实拣货,不能补录数量代替核平。' }}</text><text v-if="!gate('LOADING_COMPLETE')" class="gate-warning">{{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }}</text><button class="primary-button" :disabled="!canComplete || busy || Boolean(pendingCommand)" @tap="prepareLoading">核对实载并确认装车完成</button></view>
|
||||
<view v-if="current.stage === 'AFTER_LOADING' || departed" class="fulfillment-card gate-card"><text class="card-title">实际发车</text><text v-if="departed" class="gate-success">本车已经实际发车,不重复提交。</text><text v-else-if="!gate('DEPART')" class="gate-warning">{{ current.actions.DEPART?.blockReason || '请先由后台完成本车全部客户的派送安排' }}</text><text v-else>装车完成与实际发车是两个动作,请确认现场已经发车。</text><button v-if="!departed" class="depart-button" :disabled="!canDepart || busy || Boolean(pendingCommand)" @tap="depart">确认整车发车</button></view>
|
||||
@@ -33,11 +34,11 @@ import { toDispatchError } from '@/utils/dispatch-error.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput, DispatchHandoverStatus },
|
||||
data() { return { user: session.currentUser, tasks: [], current: null, scanCode: '', loading: false, busy: false, error: null, successMessage: '', bootstrapped: false, resumeOnEntry: false, generation: 0, loadingPreview: null, loadingRows: [] } },
|
||||
data() { return { user: session.currentUser, tasks: [], current: null, scanCode: '', loading: false, busy: false, error: null, successMessage: '', bootstrapped: false, resumeOnEntry: false, generation: 0, loadingPreview: null, loadingRows: [], sourceChoices: [], choiceCartonCode: '' } },
|
||||
computed: {
|
||||
recentTaskNo() { return fulfillment.dispatch.currentNo || '' }, pendingCommand() { return fulfillment.dispatch.pendingCommand }, packages() { return fulfillment.dispatch.packages },
|
||||
selectedItem() { return this.current?.items.find(row => row.dispatchItemId === fulfillment.dispatch.dispatchItemId) || null },
|
||||
selectedReturnCarton() { return this.selectedItem?.returnCartons.find(row => row.cartonMark === fulfillment.dispatch.cartonCode && row.status === 'OUTBOUND_PENDING') || null },
|
||||
selectedItem() { return this.current?.items.find(row => row.dispatchItemId === fulfillment.dispatch.dispatchItemId && row.sourceKey === fulfillment.dispatch.sourceKey) || null },
|
||||
selectedReturnCarton() { const matches = this.selectedItem?.returnCartons.filter(row => row.cartonMark === fulfillment.dispatch.cartonCode && row.status === 'OUTBOUND_PENDING') || []; return matches.length === 1 ? matches[0] : null },
|
||||
selectedPosition() { return this.selectedItem?.positions.find(row => row.availablePackages > 0 && row.allocatedPackages > row.pickedPackages) || null },
|
||||
selectedRemaining() { const remaining = Math.max(0, (this.selectedItem?.scheduledPackages || 0) - (this.selectedItem?.pickedPackages || 0)); return this.selectedItem?.returnCartons.length ? remaining : Math.min(remaining, this.selectedPosition?.availablePackages || 0, Math.max(0, (this.selectedPosition?.allocatedPackages || 0) - (this.selectedPosition?.pickedPackages || 0))) },
|
||||
plannedPackages() { return (this.current?.items || []).reduce((sum, row) => sum + row.scheduledPackages, 0) }, pickedPackages() { return (this.current?.items || []).reduce((sum, row) => sum + row.pickedPackages, 0) }, remainingPackages() { return Math.max(0, this.plannedPackages - this.pickedPackages) },
|
||||
@@ -54,10 +55,41 @@ export default {
|
||||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: value => this.handleScan(value), onError: error => this.setError(error, '扫码广播注册失败') }) }, stopScanner() { this._stopScanner?.(); this._stopScanner = null },
|
||||
updatePackages(event) { if (this.pendingCommand) return; const value = Number(event.detail.value); updateFulfillmentDraft('dispatch', { packages: Number.isFinite(value) ? value : 0 }) },
|
||||
async loadTasks() { const generation = ++this.generation; await this.run(async () => { const result = await listDispatchTasks(); if (generation === this.generation) { this.tasks = result; this.current = null } }, true) },
|
||||
async openTask(no) { if (this.pendingCommand && this.pendingCommand.dispatchNo !== no) return this.setError(new Error('请先核对原排车请求,不能切到另一张单执行'), '操作未完成'); const generation = ++this.generation; await this.run(async () => { const value = await getDispatchTask(no); if (generation !== this.generation) return; if (value.dispatchNo !== no) throw new Error('返回排车与扫描业务号不一致'); const same = fulfillment.dispatch.currentNo === no; this.current = value; updateFulfillmentDraft('dispatch', { currentNo: no, palletCode: same ? fulfillment.dispatch.palletCode : '', dispatchItemId: same ? fulfillment.dispatch.dispatchItemId : '', cartonCode: same ? fulfillment.dispatch.cartonCode : '', packages: same ? fulfillment.dispatch.packages : 1 }); this.scanCode = '' }, true) },
|
||||
closeTask() { if (this.busy) return; this.current = null; this.loadingPreview = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); this.loadTasks() },
|
||||
handleScan(raw) { const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return; if (this.pendingCommand) return this.setError(new Error('请先核对原排车操作结果'), '原请求未完成'); this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code); const cartonItem = this.current.items.find(item => item.returnCartons.some(carton => carton.cartonMark === code)); if (cartonItem) { if (!cartonItem.returnCartons.some(carton => carton.cartonMark === code && carton.status === 'OUTBOUND_PENDING')) return this.setError(new Error('该箱已拣或不可出库'), '扫码未匹配'); this.selectPallet(cartonItem); updateFulfillmentDraft('dispatch', { cartonCode: code, packages: 1 }); this.scanCode = ''; playSuccessSound(); return } const matches = this.current.items.filter(item => item.palletNo === code && item.scheduledPackages > item.pickedPackages); if (matches.length > 1) return this.setError(new Error('此托盘包含多条待拣明细,请选择对应客户货物卡片'), '请选择明细'); if (!matches.length) return this.setError(new Error(this.current.items.some(item => item.palletNo === code) ? '该托盘已完成拣货,请勿重复操作' : '扫描未匹配本车实物托盘或来源箱唛'), '扫码未匹配'); this.selectPallet(matches[0]); this.scanCode = ''; playSuccessSound() },
|
||||
selectPallet(item) { if (this.pendingCommand || this.busy || !item?.dispatchItemId || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: 1 }); this.error = null },
|
||||
async openTask(no) { if (this.pendingCommand && this.pendingCommand.dispatchNo !== no) return this.setError(new Error('请先核对原排车请求,不能切到另一张单执行'), '操作未完成'); const generation = ++this.generation; await this.run(async () => { const value = await getDispatchTask(no); if (generation !== this.generation) return; if (value.dispatchNo !== no) throw new Error('返回排车与扫描业务号不一致'); const same = fulfillment.dispatch.currentNo === no; this.current = value; this.sourceChoices = []; this.choiceCartonCode = ''; updateFulfillmentDraft('dispatch', { currentNo: no, palletCode: same ? fulfillment.dispatch.palletCode : '', dispatchItemId: same ? fulfillment.dispatch.dispatchItemId : '', sourceKey: same ? fulfillment.dispatch.sourceKey : '', cartonCode: same ? fulfillment.dispatch.cartonCode : '', packages: same ? fulfillment.dispatch.packages : 1 }); this.scanCode = '' }, true) },
|
||||
closeTask() { if (this.busy) return; this.current = null; this.loadingPreview = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', dispatchItemId: '', sourceKey: '', cartonCode: '', packages: 1 }); this.loadTasks() },
|
||||
handleScan(raw) {
|
||||
const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim()
|
||||
if (!code || this.busy || this.loading) return
|
||||
if (this.pendingCommand) return this.setError(new Error('请先核对原排车操作结果'), '原请求未完成')
|
||||
this.error = null; this.successMessage = ''; this.sourceChoices = []; this.choiceCartonCode = ''
|
||||
updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', sourceKey: '', cartonCode: '', packages: 1 })
|
||||
if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code)
|
||||
const cartonItems = this.current.items.filter(item => item.returnCartons.some(carton => carton.cartonMark === code))
|
||||
if (cartonItems.some(item => item.returnCartons.filter(carton => carton.cartonMark === code && carton.status === 'OUTBOUND_PENDING').length > 1)) return this.setError(new Error('同一来源返回重复待拣箱唛,请重新读取并核对箱记录'), '扫码未匹配')
|
||||
const matches = cartonItems.length ? cartonItems.filter(item => item.scheduledPackages > item.pickedPackages && item.returnCartons.some(carton => carton.cartonMark === code && carton.status === 'OUTBOUND_PENDING')) : this.current.items.filter(item => item.palletNo === code && item.scheduledPackages > item.pickedPackages)
|
||||
if (!matches.length) return this.setError(new Error(cartonItems.length ? '该箱已拣或不可出库' : this.current.items.some(item => item.palletNo === code) ? '该托盘已完成拣货,请勿重复操作' : '扫描未匹配本车实物托盘或来源箱唛'), '扫码未匹配')
|
||||
this.sourceChoices = matches; this.choiceCartonCode = cartonItems.length ? code : ''
|
||||
if (matches.length > 1) return this.setError(new Error('此扫码命中多条待拣明细,请按来源业务号、客户和可拣箱数选择'), '请选择明细')
|
||||
this.chooseSource(matches[0])
|
||||
},
|
||||
chooseSource(item) {
|
||||
if (this.pendingCommand || this.busy || !this.sourceChoices.some(row => row.dispatchItemId === item.dispatchItemId)) return
|
||||
const cartonCode = this.choiceCartonCode
|
||||
this.selectPallet(item)
|
||||
if (fulfillment.dispatch.dispatchItemId !== item.dispatchItemId) return
|
||||
if (cartonCode) updateFulfillmentDraft('dispatch', { cartonCode, packages: 1 })
|
||||
this.scanCode = ''; playSuccessSound()
|
||||
},
|
||||
pickablePackages(item) {
|
||||
const remaining = Math.max(0, item.scheduledPackages - item.pickedPackages)
|
||||
return Math.min(remaining, item.returnCartons.length ? item.returnCartons.filter(row => row.status === 'OUTBOUND_PENDING').length : item.positions.reduce((sum, row) => sum + Math.min(row.availablePackages, Math.max(0, row.allocatedPackages - row.pickedPackages)), 0))
|
||||
},
|
||||
selectPallet(item) {
|
||||
if (this.pendingCommand || this.busy || !item?.dispatchItemId || item.scheduledPackages <= item.pickedPackages) return
|
||||
if (!item.sourceKey || item.sourceBusinessType === 'RELABEL_ORDER' && !item.sourceBusinessNo) return this.setError(new Error('来源业务身份未完整返回,请重新读取任务'), '请选择明细')
|
||||
updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, sourceKey: item.sourceKey, cartonCode: '', packages: 1 })
|
||||
this.sourceChoices = []; this.choiceCartonCode = ''; this.error = null
|
||||
},
|
||||
async submitPick() { if (!this.canPick || this.pendingCommand) return; try { const body = { recordVersion: requireDispatchVersion(this.current.recordVersion), positionId: this.selectedReturnCarton ? null : this.selectedPosition.positionId, packages: this.selectedReturnCarton ? 1 : Number(this.packages), productQuantity: this.selectedReturnCarton?.productQuantity || 0, cartonMark: this.selectedReturnCarton?.cartonMark || null, remark: null }; beginDispatchCommand(this.current.dispatchNo, 'PICK', { ...body, dispatchItemId: this.selectedItem.dispatchItemId }); await this.recoverCommand() } catch (cause) { this.setError(cause, '拣货未提交') } },
|
||||
async prepareLoading() { if (!this.canComplete || this.pendingCommand) return; await this.run(async () => { const data = await getDispatchLoading(this.current.dispatchNo); if (data.dispatchNo !== this.current.dispatchNo || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足'); requireDispatchVersion(data.recordVersion); this.loadingPreview = data; this.loadingRows = data.pallets.map(row => { const confirmed = data.confirmedLoad?.manifestHash === data.manifestHash ? data.confirmedLoad.pallets.find(item => String(item.warehousePalletId) === String(row.warehousePalletId)) : null; return { warehousePalletId: String(row.warehousePalletId), palletNo: row.palletNo, palletSpaces: row.palletSpaces, deliveryIndex: row.deliveryIndex, weightKg: confirmed?.weightKg ?? (row.evidence.some(item => item.field === 'weightKg' && item.actual === true) ? row.weightKg : null), volumeCbm: confirmed?.volumeCbm ?? (row.evidence.some(item => item.field === 'volumeCbm' && item.actual === true) ? row.volumeCbm : null) } }) }) },
|
||||
setMeasurement(row, field, event) { if (this.busy || this.pendingCommand) return; const value = event.detail.value === '' ? null : Number(event.detail.value); row[field] = value != null && Number.isFinite(value) ? value : null },
|
||||
|
||||
@@ -4,7 +4,7 @@ const STORAGE_KEY = 'overseas-pda-fulfillment-draft-v2'
|
||||
|
||||
const blankState = () => ({
|
||||
scopeKey: '',
|
||||
dispatch: { currentNo: '', palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1, pendingCommand: null },
|
||||
dispatch: { currentNo: '', palletCode: '', dispatchItemId: '', sourceKey: '', cartonCode: '', packages: 1, pendingCommand: null },
|
||||
outbound: { currentTaskNo: '', scanCode: '', packages: 1, pendingKeys: {} }
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user