feat(dispatch): validate reviewed whole-trip departures
This commit is contained in:
@@ -24,8 +24,14 @@ export function normalizeDispatchTask(value) {
|
||||
taskNo: String(value?.taskNo || ''),
|
||||
dispatchNo: String(value?.dispatchNo || ''),
|
||||
vehiclePlate: String(value?.vehiclePlate || ''),
|
||||
tripNo: String(value?.tripNo || ''),
|
||||
tripRecordVersion: value?.tripRecordVersion == null ? null : String(value.tripRecordVersion),
|
||||
tripDispatchCount: count(value?.tripDispatchCount),
|
||||
driverWarning: String(value?.driverWarning || ''),
|
||||
status: String(value?.status || ''),
|
||||
canDepart: value?.canDepart === true,
|
||||
canPick: value?.canPick === true,
|
||||
pickBlockReason: String(value?.pickBlockReason || ''),
|
||||
departBlockReason: String(value?.departBlockReason || ''),
|
||||
items: asList(value?.items).map((item) => ({
|
||||
...item,
|
||||
@@ -97,6 +103,13 @@ export function departDispatch(dispatchNo, request) {
|
||||
return overseasRequest('POST', `${DISPATCH_BASE}/${encoded(dispatchNo)}/depart`, request)
|
||||
}
|
||||
|
||||
export function tripDepartureRequest(task, idempotencyKey) {
|
||||
if (!task?.tripNo || task.tripRecordVersion == null || !/^\d+$/.test(String(task.tripRecordVersion))) {
|
||||
throw new Error('车次信息不完整,请刷新任务后再发车')
|
||||
}
|
||||
return { recordVersion: String(task.tripRecordVersion), idempotencyKey, remark: '海外 PDA 整车核平后发车' }
|
||||
}
|
||||
|
||||
export async function listOutboundTasks(status = '') {
|
||||
const suffix = status ? `?status=${encoded(status)}` : ''
|
||||
return asList(await overseasRequest('GET', `${OUTBOUND_BASE}${suffix}`)).map(normalizeOutboundTask)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<view class="fulfillment-card summary-card">
|
||||
<view class="task-head"><text class="business-code">{{ current.dispatchNo || '—' }}</text><text class="status-badge">{{ statusLabel(current.status) }}</text></view>
|
||||
<text class="task-meta">任务号 {{ current.taskNo || '—' }} · 车牌 {{ current.vehiclePlate || '—' }}</text>
|
||||
<text v-if="!current.canPick && !pickingCompleted" class="gate-warning">{{ current.pickBlockReason || '请先通过调度审核再拣货' }}</text>
|
||||
<view class="metrics"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value success">{{ pickedPackages }}</text><text>已拣箱数</text></view><view><text class="metric-value warning">{{ remainingPackages }}</text><text>待拣箱数</text></view></view>
|
||||
</view>
|
||||
|
||||
@@ -67,12 +68,14 @@
|
||||
</view>
|
||||
|
||||
<view v-if="pickingCompleted" class="fulfillment-card gate-card">
|
||||
<text class="card-title">发车门禁</text>
|
||||
<text class="card-title">整车发车</text>
|
||||
<text v-if="current.tripNo" class="task-meta">车次 {{ current.tripNo }} · 共 {{ current.tripDispatchCount }} 张排车单</text>
|
||||
<text v-if="current.driverWarning && !departed" class="gate-warning">{{ current.driverWarning }}</text>
|
||||
<text v-if="!allPicked" class="gate-warning">服务端仍显示 {{ remainingPackages }} 箱未拣,不允许发车。</text>
|
||||
<text v-else-if="departed" class="gate-success">该排车任务已执行发车,不可重复提交。</text>
|
||||
<text v-else-if="!current.canDepart" class="gate-warning">{{ current.departBlockReason || '当前排车暂不满足发车条件,请刷新后重试。' }}</text>
|
||||
<text v-else class="gate-note">拣货已完成且全部箱数核平,可以发车。</text>
|
||||
<button v-if="!departed" class="depart-button" :disabled="!canDepart || busy" @tap="depart">{{ busyAction === 'depart' ? '发车中…' : '确认发车' }}</button>
|
||||
<text v-else class="gate-note">本车所有排车单已就绪,确认后将一并发车。</text>
|
||||
<button v-if="!departed" class="depart-button" :disabled="!canDepart || busy" @tap="depart">{{ busyAction === 'depart' ? '发车中…' : '确认整车发车' }}</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -85,7 +88,7 @@
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { completeDispatchPicking, departDispatch, getDispatchTask, listDispatchTasks, pickDispatchItem } from '@/api/fulfillment.js'
|
||||
import { completeDispatchPicking, departDispatch, getDispatchTask, listDispatchTasks, pickDispatchItem, tripDepartureRequest } from '@/api/fulfillment.js'
|
||||
import { fulfillment, fulfillmentIdempotencyKey, releaseFulfillmentKey, restoreFulfillmentDraft, updateFulfillmentDraft } from '@/stores/fulfillment.js'
|
||||
import { requireSession, session } from '@/stores/session.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
@@ -109,9 +112,9 @@
|
||||
allPicked() { return this.plannedPackages > 0 && this.remainingPackages === 0 },
|
||||
pickingCompleted() { return finishedStatuses.includes(this.current?.status) },
|
||||
departed() { return this.current?.status === 'DEPARTED' },
|
||||
canPick() { return this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining && !this.pickingCompleted },
|
||||
canPick() { return this.current?.canPick === true && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining && !this.pickingCompleted },
|
||||
canCompleteStage() { return Boolean(this.current) && !this.pickingCompleted },
|
||||
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 },
|
||||
canComplete() { return this.current?.canPick === true && this.plannedPackages > 0 && this.remainingPackages === 0 },
|
||||
canDepart() { return this.pickingCompleted && this.allPicked && this.current?.canDepart === true && !this.departed }
|
||||
},
|
||||
onLoad(options) { this.resumeOnEntry = options?.resume === '1' },
|
||||
@@ -168,8 +171,13 @@
|
||||
},
|
||||
async depart() {
|
||||
if (!this.canDepart) return
|
||||
const key = fulfillmentIdempotencyKey('dispatch', 'depart', { dispatchNo: this.current.dispatchNo })
|
||||
await this.run('执行发车', async () => { const result = await departDispatch(this.current.dispatchNo, { idempotencyKey: key.value, remark: '海外 PDA 拣货核平后发车' }); this.current = { ...this.current, status: result?.status || 'DEPARTED' }; releaseFulfillmentKey('dispatch', key.signature); this.successMessage = '排车已发车'; playSuccessSound() })
|
||||
const key = fulfillmentIdempotencyKey('dispatch', 'depart', { tripNo: this.current.tripNo, recordVersion: this.current.tripRecordVersion })
|
||||
await this.run('执行整车发车', async () => {
|
||||
const result = await departDispatch(this.current.dispatchNo, tripDepartureRequest(this.current, key.value))
|
||||
if (result?.status !== 'IN_TRANSIT' || result.tripNo !== this.current.tripNo || !result.dispatches?.some(item => item.dispatchNo === this.current.dispatchNo)) throw new Error('发车结果未包含当前车次和排车单,请刷新核对')
|
||||
this.current = await getDispatchTask(this.current.dispatchNo)
|
||||
releaseFulfillmentKey('dispatch', key.signature); this.successMessage = `车次 ${result.tripNo} 已整车发车`; playSuccessSound()
|
||||
})
|
||||
},
|
||||
async retry() { if (this.current?.dispatchNo) await this.openTask(this.current.dispatchNo); else if (fulfillment.dispatch.currentNo) await this.openTask(fulfillment.dispatch.currentNo); else await this.loadTasks() },
|
||||
async run(action, work, loading = false) { if (this.busy || this.loading) return; this.error = null; this.successMessage = ''; if (loading) this.loading = true; else { this.busy = true; this.busyAction = action.includes('发车') ? 'depart' : action.includes('完成') ? 'complete' : 'pick' } try { await work() } catch (error) { this.setError(error, `${action}失败`) } finally { this.loading = false; this.busy = false; this.busyAction = '' } },
|
||||
|
||||
Reference in New Issue
Block a user