优化提交
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
<view class="fulfillment-heading"><text class="fulfillment-title">出库任务</text><text class="fulfillment-subtitle">一件代发扫描托盘码可整托出库;扫描排车业务号会转入“排车拣货”</text></view>
|
||||
<view v-if="current" class="detail-toolbar"><view><text class="detail-kicker">当前任务</text><text class="detail-title">出库作业详情</text></view><button :disabled="busy" @tap="closeTask">‹ 返回列表</button></view>
|
||||
<view class="fulfillment-card">
|
||||
<ScanTargetInput name="outbound" :model-value="scanCode" label="任务号、出库单号、排车单号、托盘码或箱码" placeholder="扫描任务号 / 出库单号 / 排车单号 / 托盘码 / 箱码" :active="true" @update:modelValue="updateScanCode" @submit="handleScan" />
|
||||
<button class="secondary-button" :disabled="!scanCode || loading || busy" @tap="handleScan(scanCode)">{{ current ? '匹配出库货物' : '打开出库任务' }}</button>
|
||||
<ScanTargetInput name="outbound" :model-value="scanCode" :label="discardCurrent ? '废弃订单托盘码' : '任务号、出库单号、排车单号、托盘码或箱码'" :placeholder="discardCurrent ? '扫描废弃订单对应托盘码' : '扫描任务号 / 出库单号 / 排车单号 / 托盘码 / 箱码'" :active="true" @update:modelValue="updateScanCode" @submit="handleScan" />
|
||||
<button class="secondary-button" :disabled="!scanCode || loading || busy" @tap="handleScan(scanCode)">{{ discardCurrent ? '打开废弃托盘' : current ? '匹配出库货物' : '打开出库任务' }}</button>
|
||||
<text class="scan-help">任务号直接恢复;进入任务后扫描 DP 托盘码,将一次拣取该托全部未拣箱件。</text>
|
||||
</view>
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<view v-if="tasks.length" class="task-list">
|
||||
<button v-for="task in tasks" :key="task.taskNo" class="task-card" @tap="openTask(task.taskNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ task.taskNo || '—' }}</text><text v-if="task.taskNo === recentTaskNo" class="recent-badge">上次作业</text></view><text class="status-badge">{{ statusLabel(task.status) }}</text></view><text class="task-meta">出库单 {{ task.outboundNo || '—' }} · {{ taskType(task) }}</text><text class="task-enter">查看并开始出库 ›</text></button>
|
||||
</view>
|
||||
<view v-else-if="!error" class="fulfillment-state"><text class="state-title">暂无出库任务</text><text>可刷新列表,或扫描任务号恢复未完成作业。</text></view>
|
||||
<view v-else-if="!error && !discardTasks.length" class="fulfillment-state"><text class="state-title">暂无出库任务</text><text>可刷新列表,或扫描任务号恢复未完成作业。</text></view>
|
||||
<view v-if="discardTasks.length" class="discard-section"><view class="list-toolbar"><view><text class="list-title">废弃订单</text><text class="list-count">{{ discardOrderCount }} 个订单</text></view></view><view class="task-list"><button v-for="order in discardOrders" :key="order.discardNo" class="task-card" @tap="openDiscardOrder(order.discardNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ order.discardNo || '—' }}</text></view><text class="status-badge">废弃订单</text></view><text class="task-meta">托盘 {{ order.palletCount }} 个 · 待作业 {{ order.pendingPalletCount }} 个</text><text class="task-enter">点击进入,扫描对应托盘 ›</text></button></view></view>
|
||||
</template>
|
||||
|
||||
<template v-if="current">
|
||||
@@ -41,6 +42,12 @@
|
||||
<view class="fulfillment-card gate-card"><text class="card-title">完成出库任务</text><text v-if="remainingPackages > 0" class="gate-warning">尚有 {{ remainingPackages }} 箱未拣,不能完成出库。</text><text v-else-if="current.status === 'COMPLETED'" class="gate-success">出库任务已完成,不可重复提交。</text><text v-else class="gate-note">全部计划货物已扫描核平,服务端将再次校验库存门禁。</text><button v-if="current.status !== 'COMPLETED'" class="primary-button" :disabled="!canComplete || busy" @tap="completeTask">{{ busyAction === 'complete' ? '完成中…' : '完成出库任务' }}</button></view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="discardCurrent">
|
||||
<view class="detail-toolbar"><view><text class="detail-kicker">废弃订单</text><text class="detail-title">{{ discardCurrent.discardNo }}</text></view><button :disabled="discardBusy" @tap="closeDiscard">‹ 返回列表</button></view>
|
||||
<view class="fulfillment-card"><ScanTargetInput name="cargo-discard" :model-value="discardPalletCode" label="废弃托盘码" placeholder="扫描对应托盘码" :active="true" @update:modelValue="updateDiscardPalletCode" @submit="resolveDiscardPallet" /><button class="secondary-button" :disabled="!discardPalletCode || discardLoading || discardBusy" @tap="resolveDiscardPallet">{{ discardLoading ? '读取中…' : '打开托盘废弃作业' }}</button><text class="scan-help">扫描该废弃订单对应的托盘,再填写每个 SKU 的实际废弃数量。</text></view>
|
||||
<view v-if="discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">托盘 {{ discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><view v-for="sku in discardTask.skus" :key="sku.itemId" class="outbound-line"><text class="line-code">{{ sku.skuCode || '—' }}</text><text class="line-meta">{{ sku.productName || '—' }} · 计划 {{ sku.plannedPackages }} 箱</text><view class="count-row"><text>实际废弃数量</text><input type="number" :value="discardActuals[sku.itemId]" :disabled="discardTask.status === 'COMPLETED'" :placeholder="`请输入 0-${sku.plannedPackages} 箱`" @input="updateDiscardActual(sku.itemId, $event)" /><text>最多 {{ sku.plannedPackages }} 箱</text></view></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard">{{ discardBusy ? '提交中…' : '废弃出库' }}</button></view>
|
||||
</template>
|
||||
|
||||
<view v-if="successMessage" class="fulfillment-success" role="status"><text>{{ successMessage }}</text></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -50,7 +57,7 @@
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { completeOutboundTask, getOutboundTask, listOutboundTasks, outboundPickPayload, pickOutboundPallet, pickOutboundTask, resolveOutboundTask } from '@/api/fulfillment.js'
|
||||
import { completeOutboundTask, confirmCargoDiscard, getOutboundTask, listCargoDiscardTasks, listOutboundTasks, normalizeCargoDiscardTask, outboundPickPayload, pickOutboundPallet, pickOutboundTask, resolveCargoDiscardPallet, resolveOutboundTask } 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'
|
||||
@@ -58,7 +65,7 @@
|
||||
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput },
|
||||
data() { return { user: session.currentUser, tasks: [], current: null, selectedItem: null, selectedPosition: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
|
||||
data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
|
||||
computed: {
|
||||
recentTaskNo() { return fulfillment.outbound.currentTaskNo || '' },
|
||||
packages() { return fulfillment.outbound.packages },
|
||||
@@ -67,7 +74,10 @@
|
||||
remainingPackages() { return Math.max(0, this.plannedPackages - this.pickedPackages) },
|
||||
selectedRemaining() { return Math.max(0, Math.min(Number(this.selectedItem?.plannedPackages || 0) - Number(this.selectedItem?.pickedPackages || 0), Number(this.selectedPosition?.availablePackages || 0))) },
|
||||
canPick() { return this.current?.status !== 'COMPLETED' && this.selectedItem && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining },
|
||||
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 && this.current?.status !== 'COMPLETED' }
|
||||
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 && this.current?.status !== 'COMPLETED' },
|
||||
discardOrders() { const groups = new Map(); for (const task of this.discardTasks) { const current = groups.get(task.discardNo) || { discardNo: task.discardNo, palletCount: 0, pendingPalletCount: 0 }; current.palletCount += 1; if (task.status !== 'COMPLETED') current.pendingPalletCount += 1; groups.set(task.discardNo, current) } return [...groups.values()] },
|
||||
discardOrderCount() { return this.discardOrders.length },
|
||||
canConfirmDiscard() { return Boolean(this.discardTask && this.discardTask.status !== 'COMPLETED' && this.discardTask.skus.length && this.discardTask.skus.every((sku) => Number.isInteger(Number(this.discardActuals[sku.itemId])) && Number(this.discardActuals[sku.itemId]) >= 0 && Number(this.discardActuals[sku.itemId]) <= sku.plannedPackages)) }
|
||||
},
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (!this.bootstrapped) { this.bootstrapped = true; this.scanCode = ''; this.loadTasks(); return } if (this.current?.taskNo) this.openTask(this.current.taskNo); else this.loadTasks() },
|
||||
onHide() { this.stopScanner() },
|
||||
@@ -77,7 +87,7 @@
|
||||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||||
updateScanCode(value) { this.scanCode = value; updateFulfillmentDraft('outbound', { scanCode: value }) },
|
||||
updatePackages(event) { const value = Math.trunc(Number(event.detail.value || 0)); updateFulfillmentDraft('outbound', { packages: Number.isFinite(value) ? Math.max(0, value) : 0 }) },
|
||||
async loadTasks() { await this.run('加载出库任务', async () => { this.tasks = await listOutboundTasks(); this.current = null }, true) },
|
||||
async loadTasks() { await this.run('加载出库任务', async () => { const [tasks, discardTasks] = await Promise.all([listOutboundTasks(), listCargoDiscardTasks()]); this.tasks = tasks; this.discardTasks = discardTasks; this.current = null; this.discardCurrent = null; this.discardTask = null }, true) },
|
||||
async openTask(taskNo) { const code = String(taskNo || '').trim(); if (!code) return; await this.run('恢复出库任务', async () => { const pendingScan = fulfillment.outbound.currentTaskNo === code ? fulfillment.outbound.scanCode : ''; this.current = await getOutboundTask(code); this.selectedItem = null; this.selectedPosition = null; this.scanCode = pendingScan; updateFulfillmentDraft('outbound', { currentTaskNo: this.current.taskNo, scanCode: pendingScan, packages: pendingScan ? fulfillment.outbound.packages : 1 }); if (pendingScan) this.findAndSelectPosition(pendingScan, false) }, true) },
|
||||
async openScannedTask(businessNo) {
|
||||
await this.run('解析出库作业', async () => {
|
||||
@@ -95,9 +105,16 @@
|
||||
}, true)
|
||||
},
|
||||
closeTask() { this.current = null; this.selectedItem = null; this.selectedPosition = null; this.error = null; this.successMessage = ''; updateFulfillmentDraft('outbound', { currentTaskNo: '', scanCode: '', packages: 1 }); this.loadTasks() },
|
||||
openDiscardOrder(discardNo) { const code = String(discardNo || '').trim(); if (!code) return; this.discardCurrent = { discardNo: code }; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.error = null },
|
||||
closeDiscard() { this.discardCurrent = null; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.loadTasks() },
|
||||
updateDiscardPalletCode(value) { this.discardPalletCode = value },
|
||||
async resolveDiscardPallet(raw) { const code = String(raw?.detail?.value ?? raw ?? this.discardPalletCode).trim(); if (!code || !this.discardCurrent || this.discardBusy) return; this.discardPalletCode = code; this.discardLoading = true; try { const task = await resolveCargoDiscardPallet(code); if (task.discardNo !== this.discardCurrent.discardNo) throw new Error('扫描的托盘不属于当前废弃订单'); this.discardTask = normalizeCargoDiscardTask(task); this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? sku.plannedPackages : sku.actualPackages])) } catch (error) { this.setError(error, '废弃托盘读取失败') } finally { this.discardLoading = false } },
|
||||
updateDiscardActual(itemId, event) { const value = Math.max(0, Math.trunc(Number(event.detail.value || 0))); this.discardActuals = { ...this.discardActuals, [itemId]: value } },
|
||||
async confirmDiscard() { if (!this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return; this.discardBusy = true; try { const lines = this.discardTask.skus.map((sku) => ({ itemId: sku.itemId, actualPackages: Number(this.discardActuals[sku.itemId]) })); await confirmCargoDiscard(this.discardCurrent.discardNo, this.discardTask.palletNo, lines); this.successMessage = '废弃出库作业已完成,等待仓库确认'; this.discardPalletCode = ''; this.discardTask = null; this.discardTasks = await listCargoDiscardTasks(); playSuccessSound() } catch (error) { this.setError(error, '废弃出库失败') } finally { this.discardBusy = false } },
|
||||
handleScan(raw) {
|
||||
const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return
|
||||
console.info('[Overseas PDA outbound] scan', { code, task: this.current?.taskNo || '' }); this.scanCode = code; updateFulfillmentDraft('outbound', { scanCode: code })
|
||||
console.info('[Overseas PDA outbound] scan', { code, task: this.current?.taskNo || '', discard: this.discardCurrent?.discardNo || '' }); this.scanCode = code; updateFulfillmentDraft('outbound', { scanCode: code })
|
||||
if (this.discardCurrent) return this.resolveDiscardPallet(code)
|
||||
if (!this.current) return this.openScannedTask(code)
|
||||
if (code === this.current.taskNo || code === this.current.outboundNo) return this.openTask(this.current.taskNo)
|
||||
if ((this.current.items || []).some((item) => item.palletNo === code)) return this.submitPalletPick(code)
|
||||
|
||||
Reference in New Issue
Block a user