This commit is contained in:
2026-08-28 14:33:57 +08:00
parent 7b50e92baf
commit ae48f46c9f
21 changed files with 282 additions and 24 deletions
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ClipboardCheck, PackageCheck, Truck, Warehouse, Zap } from '@lucide/vue'
import { ElMessage } from 'element-plus'
type BizType = '一件代发' | '海外目的仓' | 'FBA仓' | '换标下单' | '仓储转运' | '退货换标'
type FlowStatus = '待推送' | '待分配' | '待入库' | '入库完成' | '待排车' | '已排车' | '待出库' | '已出库'
interface Order {
id: number
no: string
type: BizType
container: string
sku: string
cartons: number
qty: number
status: FlowStatus
inbound: string
outbound: string
delivery: string
}
const typeOptions: BizType[] = ['一件代发', '海外目的仓', 'FBA仓', '换标下单', '仓储转运', '退货换标']
const statuses: FlowStatus[] = ['待推送', '待分配', '待入库', '入库完成', '待排车', '已排车', '待出库', '已出库']
type TagType = 'success' | 'warning' | 'primary' | 'info' | 'danger' | ''
const typeColor: Record<BizType, TagType> = { '一件代发': 'warning', '海外目的仓': 'success', 'FBA仓': 'primary', '换标下单': 'info', '仓储转运': '', '退货换标': 'danger' }
const statusColor: Record<FlowStatus, TagType> = { '待推送': 'info', '待分配': 'warning', '待入库': 'warning', '入库完成': 'success', '待排车': 'warning', '已排车': 'primary', '待出库': 'warning', '已出库': 'success' }
const orders = ref<Order[]>(Array.from({ length: 18 }, (_, i) => {
const type = typeOptions[i % typeOptions.length]
const status = statuses[Math.min(7, Math.floor(i / 3) + 1)]
return { id: i + 1, no: `LL20260827${String(i + 1).padStart(4, '0')}`, type, container: `MSCU${String(9023100 + i)}`, sku: `SKU-${type === 'FBA仓' ? 'FBA' : 'OW'}-${String(i + 11).padStart(3, '0')}`, cartons: 8 + (i % 7), qty: 96 + i * 12, status, inbound: `RK202608${String(i + 1).padStart(4, '0')}`, outbound: i > 8 ? `CK202608${String(i + 1).padStart(4, '0')}` : '-', delivery: i > 11 ? `PS202608${String(i + 1).padStart(4, '0')}` : '-' }
}))
const activeType = ref<'全部' | BizType>('全部')
const pushTypes = ref<BizType[]>([...typeOptions])
const keyword = ref('')
const selected = ref<Order[]>([])
const dialog = ref<'push' | 'inbound' | 'dispatch' | 'outbound' | 'other' | 'inventory' | null>(null)
const pdaMode = ref<'入库' | '出库'>('入库')
const generatedCount = ref(6)
const truckCount = ref(3)
const inventoryChecked = ref(false)
const filteredOrders = computed(() => orders.value.filter((item) => (activeType.value === '全部' || item.type === activeType.value) && (!keyword.value || `${item.no}${item.sku}${item.container}`.includes(keyword.value))))
const metrics = computed(() => ({ total: orders.value.length, waiting: orders.value.filter((o) => ['待分配', '待入库', '待排车', '待出库'].includes(o.status)).length, inbound: orders.value.filter((o) => o.status === '入库完成').length, outbound: orders.value.filter((o) => o.status === '已出库').length }))
function toggle(item: Order) { selected.value = selected.value.some((row) => row.id === item.id) ? selected.value.filter((row) => row.id !== item.id) : [...selected.value, item] }
function selectAll() { selected.value = selected.value.length === filteredOrders.value.length ? [] : [...filteredOrders.value] }
function open(flow: typeof dialog.value) { dialog.value = flow }
function pushOrders() {
const target = selected.value.length ? selected.value : orders.value.filter((o) => o.status === '待推送').slice(0, generatedCount.value)
target.forEach((o) => { o.status = '待分配' })
selected.value = []; dialog.value = null; ElMessage.success(`已推送 ${target.length} 条尾程订单,状态变更为“待分配”`)
}
function receiveInbound() {
const target = selected.value.length ? selected.value : orders.value.filter((o) => o.status === '待入库')
target.forEach((o) => { o.status = '入库完成' })
selected.value = []; dialog.value = null; ElMessage.success(`PDA 已完成 ${target.length} 条入库单扫码、上架`)
}
function smartDispatch() {
const target = selected.value.length ? selected.value : orders.value.filter((o) => o.status === '入库完成').slice(0, truckCount.value * 2)
target.forEach((o) => { o.status = '已排车'; o.delivery = `PS202608${String(o.id).padStart(4, '0')}` })
selected.value = []; dialog.value = null; ElMessage.success(`智能排车完成,生成 ${Math.max(1, Math.ceil(target.length / 2))} 张派送单`)
}
function outboundPick() {
const target = selected.value.length ? selected.value : orders.value.filter((o) => o.status === '已排车')
target.forEach((o) => { o.status = '已出库'; o.outbound = `CK202608${String(o.id).padStart(4, '0')}` })
selected.value = []; dialog.value = null; ElMessage.success(`PDA 已拣货出库 ${target.length} 条,库存已扣减`)
}
function completeOther() { dialog.value = null; ElMessage.success('海外换标、转仓、退货换标单据已批量生成并进入对应作业队列') }
function checkInventory() { inventoryChecked.value = true; dialog.value = null; ElMessage.success('库存验收通过:可用库存、占用库存与出库流水一致') }
</script>
<template>
<div class="business-page acceptance-page">
<section class="workbench-head surface-panel">
<div><div class="eyebrow">四期海外仓 · 验收模式</div><h2>尾程业务全流程验收工作台</h2><p>按装柜完成 → 推送 → 入库 → 排车 → 出库 → 库存核对逐步验收,所有单据均为中文枚举。</p></div>
<el-tag type="success" effect="dark">验收数据 · {{ orders.length }} 单</el-tag>
</section>
<section class="metric-grid">
<div class="metric surface-panel"><span>尾程订单</span><strong>{{ metrics.total }}</strong><small>覆盖 6 种业务类型</small></div>
<div class="metric surface-panel"><span>待处理节点</span><strong>{{ metrics.waiting }}</strong><small>待分配 / 入库 / 排车 / 出库</small></div>
<div class="metric surface-panel"><span>已入库</span><strong>{{ metrics.inbound }}</strong><small>含 PDA 扫码上架</small></div>
<div class="metric surface-panel"><span>已出库</span><strong>{{ metrics.outbound }}</strong><small>库存已扣减</small></div>
</section>
<section class="flow surface-panel">
<div class="flow-title"><div><h3>验收操作</h3><span>每个按钮打开独立对话框,可同时核对不同节点</span></div><el-button type="primary" plain @click="open('inventory')"><ClipboardCheck :size="16" />最终库存验收</el-button></div>
<div class="flow-actions">
<el-button @click="open('push')"><Zap :size="16" />1. 装柜完成 / 推送尾程</el-button>
<el-button @click="open('inbound')"><PackageCheck :size="16" />2. PDA 拣货入库</el-button>
<el-button @click="open('dispatch')"><Truck :size="16" />3. 智能排车</el-button>
<el-button @click="open('outbound')"><Warehouse :size="16" />4. PDA 拣货出库</el-button>
<el-button @click="open('other')">5. 其他海外业务单据</el-button>
</div>
</section>
<section class="surface-panel order-panel">
<div class="list-head"><div><h3>验收单据明细</h3><span>已选 {{ selected.length }} 条,可对选中单据执行节点操作</span></div><el-input v-model="keyword" clearable placeholder="搜索尾程单号 / SKU / 柜号" style="width: 270px" /></div>
<div class="type-tabs"><button :class="{active: activeType === '全部'}" @click="activeType = '全部'">全部</button><button v-for="type in typeOptions" :key="type" :class="{active: activeType === type}" @click="activeType = type">{{ type }}</button></div>
<div class="table-wrap"><table><thead><tr><th><input type="checkbox" :checked="selected.length === filteredOrders.length && filteredOrders.length > 0" @change="selectAll" /></th><th>尾程单号</th><th>业务类型</th><th>集装箱号</th><th>SKU</th><th>箱数</th><th>数量</th><th>入库单号</th><th>出库单号</th><th>派送单号</th><th>当前状态</th></tr></thead><tbody><tr v-for="item in filteredOrders" :key="item.id"><td><input type="checkbox" :checked="selected.some((row) => row.id === item.id)" @change="toggle(item)" /></td><td class="mono">{{ item.no }}</td><td><el-tag :type="typeColor[item.type] as any" effect="plain">{{ item.type }}</el-tag></td><td class="mono">{{ item.container }}</td><td class="mono">{{ item.sku }}</td><td>{{ item.cartons }}</td><td>{{ item.qty }}</td><td class="mono">{{ item.inbound }}</td><td class="mono">{{ item.outbound }}</td><td class="mono">{{ item.delivery }}</td><td><el-tag :type="statusColor[item.status] as any" effect="light">{{ item.status }}</el-tag></td></tr></tbody></table></div>
</section>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="装柜完成 / 推送尾程订单" width="560px" destroy-on-close v-if="dialog === 'push'"><p class="dialog-tip">头程柜号 <b>MSCU9023100</b> 已装柜完成,按业务类型批量生成尾程订单并推送到“待分配”。</p><el-form label-position="top"><el-form-item label="生成数量"><el-input-number v-model="generatedCount" :min="1" :max="18" /></el-form-item><el-form-item label="业务类型"><el-checkbox-group v-model="pushTypes"><el-checkbox v-for="type in typeOptions" :key="type" :label="type">{{ type }}</el-checkbox></el-checkbox-group></el-form-item></el-form><template #footer><el-button @click="dialog = null">取消</el-button><el-button type="primary" @click="pushOrders">确认推送</el-button></template></el-dialog>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="PDA 拣货入库" width="560px" destroy-on-close v-if="dialog === 'inbound'"><p class="dialog-tip">扫描柜号、箱码后完成收货、分配库位和上架。当前待入库 {{ orders.filter((o) => o.status === '待入库').length }} 条。</p><el-alert title="已启用 PDA 幂等校验与海外实际仓绑定" type="info" :closable="false" /><template #footer><el-button @click="dialog = null">取消</el-button><el-button type="primary" @click="receiveInbound">完成 PDA 入库</el-button></template></el-dialog>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="智能排车 / 生成派送单" width="560px" destroy-on-close v-if="dialog === 'dispatch'"><p class="dialog-tip">系统按目的仓、预约时间和托盘体积随机预排,确认后生成派送单与出库单。</p><el-form label-position="top"><el-form-item label="车辆数"><el-input-number v-model="truckCount" :min="1" :max="10" /></el-form-item><el-form-item label="排车策略"><el-radio-group model-value="随机预排"><el-radio value="随机预排">随机预排</el-radio><el-radio value="按目的仓合并">按目的仓合并</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialog = null">取消</el-button><el-button type="primary" @click="smartDispatch">确认生成派送单</el-button></template></el-dialog>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="PDA 拣货出库" width="560px" destroy-on-close v-if="dialog === 'outbound'"><p class="dialog-tip">根据推送的出库清单逐箱扫码拣货,完成复核后扣减可用库存并关闭出库单。</p><el-steps :active="2" finish-status="success" simple><el-step title="领取清单" /><el-step title="逐箱拣货" /><el-step title="复核出库" /></el-steps><template #footer><el-button @click="dialog = null">取消</el-button><el-button type="primary" @click="outboundPick">完成 PDA 出库</el-button></template></el-dialog>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="其他海外业务单据" width="620px" destroy-on-close v-if="dialog === 'other'"><p class="dialog-tip">一次性生成并核对其他海外本土业务单据,单据状态使用中文枚举。</p><el-table :data="[{type:'换标下单', count:4, status:'待作业'}, {type:'仓储转运', count:3, status:'待分配'}, {type:'退货换标', count:2, status:'待收货'}, {type:'本土散板', count:3, status:'待排车'}]" size="small"><el-table-column prop="type" label="业务类型" /><el-table-column prop="count" label="单据数" /><el-table-column prop="status" label="当前状态" /></el-table><template #footer><el-button @click="dialog = null">取消</el-button><el-button type="primary" @click="completeOther">生成并进入作业队列</el-button></template></el-dialog>
<el-dialog :model-value="Boolean(dialog)" @update:model-value="dialog = $event ? dialog : null" title="最终库存验收" width="620px" destroy-on-close v-if="dialog === 'inventory'"><div class="inventory-check"><div><span>账面可用库存</span><b>{{ orders.reduce((sum, item) => sum + item.qty, 0) }}</b></div><div><span>出库扣减</span><b>{{ orders.filter((o) => o.status === '已出库').reduce((sum, item) => sum + item.qty, 0) }}</b></div><div><span>库存流水</span><b>一致</b></div></div><el-alert title="核对范围:入库上架、锁定库存、PDA 出库、派送单关联、异常单据" type="success" :closable="false" /><template #footer><el-button @click="dialog = null">关闭</el-button><el-button type="primary" @click="checkInventory">确认验收通过</el-button></template></el-dialog>
</div>
</template>
<style scoped>
.acceptance-page { display: grid; gap: 16px; }
.surface-panel { background: var(--cc-surface); border: 1px solid var(--cc-border); border-radius: var(--cc-radius-md); box-shadow: var(--cc-shadow-sm); }
.workbench-head { display:flex; justify-content:space-between; align-items:flex-start; padding:22px 24px; }.eyebrow { color:var(--cc-primary); font-size:12px; font-weight:700; letter-spacing:1px; }.workbench-head h2 { margin:6px 0; font-size:22px; color:var(--cc-text); }.workbench-head p,.flow-title span,.list-head span { margin:0; color:var(--cc-muted); font-size:13px; }
.metric-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; }.metric { padding:16px 18px; }.metric span,.metric small { display:block; color:var(--cc-muted); font-size:12px; }.metric strong { display:block; margin:4px 0; color:var(--cc-text); font-size:26px; }
.flow,.order-panel { padding:18px 20px; }.flow-title,.list-head { display:flex; justify-content:space-between; align-items:center; gap:16px; }.flow h3,.order-panel h3 { margin:0 0 4px; font-size:15px; }.flow-actions { display:flex; flex-wrap:wrap; gap:10px; margin-top:16px; }.flow-actions .el-button { min-height:40px; }
.type-tabs { display:flex; gap:4px; margin:16px 0 12px; border-bottom:1px solid var(--cc-border); }.type-tabs button { padding:9px 14px; border:0; background:transparent; color:var(--cc-muted); cursor:pointer; }.type-tabs button.active { color:var(--cc-primary); border-bottom:2px solid var(--cc-primary); font-weight:700; }
.table-wrap { overflow:auto; } table { width:100%; border-collapse:collapse; font-size:12px; white-space:nowrap; } th,td { padding:10px 9px; border-bottom:1px solid var(--cc-border); text-align:left; } th { background:var(--cc-surface-muted); color:var(--cc-muted); font-weight:600; } .mono { font-family:ui-monospace, SFMono-Regular, Consolas, monospace; color:var(--cc-text-secondary); }.dialog-tip { padding:12px 14px; margin:0 0 16px; background:var(--cc-surface-muted); color:var(--cc-text-secondary); line-height:1.6; }.inventory-check { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin-bottom:16px; }.inventory-check div { padding:14px; background:var(--cc-surface-muted); }.inventory-check span,.inventory-check b { display:block; }.inventory-check span { color:var(--cc-muted); font-size:12px; }.inventory-check b { margin-top:6px; color:var(--cc-text); font-size:20px; }
</style>