This commit is contained in:
2026-10-21 14:30:33 +08:00
parent 3953453090
commit 8498798a9c
47 changed files with 791 additions and 409 deletions
@@ -162,6 +162,7 @@ watch(() => [props.disabled, props.warehouseSelected, props.draft, props.activeC
<div class="dispatch-cargo-board" aria-label="智能排车三栏选货">
<section class="cargo-board-panel cargo-board-orders" aria-label="待排订单">
<header class="cargo-board-heading"><h3>待排订单</h3><span class="cargo-board-muted">本页 {{ orders.length }} 单</span><InlineHelp title="货物分页说明"><p>货物分页,同一订单可能分布在多页。点击订单查看本页货物关联托盘;关联托数包含必须同车的完整托盘组。</p></InlineHelp></header>
<div class="cargo-board-order-content">
<div class="cargo-board-table-wrap">
<table class="density-table cargo-board-table">
<thead><tr><th class="cargo-board-operation">操作</th><th>订单号 / 来源</th><th>客户</th><th>关联托数</th><th>目的仓</th></tr></thead>
@@ -178,27 +179,36 @@ watch(() => [props.disabled, props.warehouseSelected, props.draft, props.activeC
</table>
</div>
<footer class="cargo-board-pagination"><span class="cargo-board-muted">货物分页,同一订单可能分布在多页</span><el-pagination aria-label="候选货物分页" :current-page="page" :page-size="size" :page-sizes="[10, 20, 50]" :total="total" :disabled="disabled" layout="total, sizes, prev, pager, next" @update:current-page="changePage" @update:page-size="changeSize" /></footer>
</section>
<section class="cargo-board-panel cargo-board-pallets" aria-label="托盘明细" aria-live="polite">
<header class="cargo-board-heading"><h3>托盘明细</h3><span class="cargo-board-muted">本页货物关联托盘</span></header>
<div v-if="selectedOrder" class="cargo-board-order-caption">{{ businessNo(selectedOrder.source) }}<small v-if="!selectedOrder.source.orderNo" class="cargo-board-muted">未关联订单</small></div>
<p v-if="!selectedOrder" class="cargo-board-empty">请选择左侧订单查看托盘明细</p>
<div v-else class="cargo-board-pallet-list">
<article v-for="source in palletGroups" :key="source.selectionGroup.groupKey" class="cargo-board-pallet" :class="{ 'is-assigned': assignedVehicle(source), 'is-draggable': canAdd(source) }" :draggable="canAdd(source)" :aria-label="`托盘 ${palletLabel(source)}`" @dragstart="dragStart($event, source)" @dragend="clearDrag">
<div class="cargo-board-pallet-title"><strong>{{ palletLabel(source) }}</strong><span>{{ groupPackages(source) ?? '—' }} 箱</span></div>
<div class="cargo-board-muted">{{ destinationLabel(source) }}</div>
<details v-if="source.selectionGroup.sources.length > 1" class="cargo-board-group-details"><summary>整托关联货物一起加入</summary><ul><li v-for="item in source.selectionGroup.sources" :key="item.sourceKey">{{ item.sourceBusinessNo || '业务号未提供' }} · {{ item.customerName || '—' }} · {{ item.packages }} 箱</li></ul></details>
<p v-if="selectionGroupBlockReason(source)" class="cargo-board-block">{{ selectionGroupBlockReason(source) }}</p>
<el-button link :disabled="!canAdd(source) || !validTarget(activeContainerId)" @click="add(source, activeContainerId)">{{ assignedVehicle(source) ? `已加入第 ${assignedVehicle(source)!.sequenceNo} 辆车` : '加入这辆车' }}</el-button>
</article>
<p v-if="!palletGroups.length" class="cargo-board-empty">该订单本页暂无托盘</p>
</div>
</section>
<slot name="vehicles" :drag-over="dragOver" :drop="drop" :drop-target="dropTarget">
<section class="cargo-board-vehicles" aria-label="车辆装载">
<header class="cargo-board-heading"><h3>车辆装载</h3><InlineHelp title="车辆装载说明"><p>将中间托盘拖入任意车辆,或选中车辆后点击“加入这辆车”。已装货物需先移除才能加入其他车辆。体积、重量以装车核对为准,本区不将缺失测量按零汇总。</p></InlineHelp><el-button type="primary" :disabled="disabled || !warehouseSelected" @click="emit('addVehicle')">添加车辆</el-button></header>
<section class="cargo-board-panel cargo-board-pallets" aria-label="托盘明细" aria-live="polite">
<header class="cargo-board-heading"><h3>托盘明细</h3><span class="cargo-board-order-caption">订单号:{{ selectedOrder ? businessNo(selectedOrder.source) : '未选择订单' }}<small v-if="selectedOrder && !selectedOrder.source.orderNo" class="cargo-board-muted">未关联订单</small></span></header>
<div class="cargo-board-pallet-list">
<table class="density-table cargo-board-pallet-table">
<thead><tr><th>托盘号</th><th class="cargo-board-pallet-count">箱数</th></tr></thead>
<tbody>
<tr v-if="!selectedOrder"><td colspan="2" class="cargo-board-empty">请选择左侧订单查看托盘明细</td></tr>
<tr v-for="source in palletGroups" :key="source.selectionGroup.groupKey" class="cargo-board-pallet" :class="{ 'is-assigned': assignedVehicle(source), 'is-draggable': canAdd(source) }" :draggable="canAdd(source)" :aria-label="`托盘 ${palletLabel(source)}`" @dragstart="dragStart($event, source)" @dragend="clearDrag">
<td>
<strong>{{ palletLabel(source) }}</strong>
<div class="cargo-board-muted">{{ destinationLabel(source) }}</div>
<details v-if="source.selectionGroup.sources.length > 1" class="cargo-board-group-details"><summary>整托关联货物一起加入</summary><ul><li v-for="item in source.selectionGroup.sources" :key="item.sourceKey">{{ item.sourceBusinessNo || '业务号未提供' }} · {{ item.customerName || '—' }} · {{ item.packages }} 箱</li></ul></details>
<p v-if="selectionGroupBlockReason(source)" class="cargo-board-block">{{ selectionGroupBlockReason(source) }}</p>
<el-button link :disabled="!canAdd(source) || !validTarget(activeContainerId)" @click="add(source, activeContainerId)">{{ assignedVehicle(source) ? `已加入第 ${assignedVehicle(source)!.sequenceNo} 辆车` : '加入这辆车' }}</el-button>
</td>
<td class="cargo-board-pallet-count">{{ groupPackages(source) ?? '—' }} 箱</td>
</tr>
<tr v-if="selectedOrder && !palletGroups.length"><td colspan="2" class="cargo-board-empty">该订单本页暂无托盘</td></tr>
</tbody>
</table>
</div>
</section>
<section class="cargo-board-panel cargo-board-vehicles" aria-label="车辆装载">
<header class="cargo-board-heading"><slot name="vehicle-heading"><h3>装车托盘</h3><InlineHelp title="车辆装载说明"><p>将中间托盘拖入任意车辆,或选中车辆后点击“加入这辆车”。已装货物需先移除才能加入其他车辆。计划托位仅用于新车辆预排,不代表真实载重或容积;体积、重量以装车核对为准。</p></InlineHelp><div class="cargo-board-vehicle-tools"><slot name="vehicle-controls" /><el-button type="primary" :disabled="disabled || !warehouseSelected" @click="emit('addVehicle')">添加车辆</el-button></div></slot></header>
<div class="cargo-board-vehicle-content">
<slot name="vehicles" :drag-over="dragOver" :drop="drop" :drop-target="dropTarget">
<div class="cargo-board-vehicle-list">
<article v-for="vehicle in vehicles" :key="vehicle.draftContainerId" class="cargo-board-vehicle" :class="{ 'is-current': vehicle.draftContainerId === activeContainerId, 'is-drop-target': dropTarget === vehicle.draftContainerId }" :aria-label="`第 ${vehicle.sequenceNo} 辆车`" @dragover="dragOver($event, vehicle.draftContainerId)" @dragleave.self="dropTarget = null" @drop="drop($event, vehicle.draftContainerId)">
<header class="cargo-board-vehicle-heading"><label><input type="radio" :name="vehicleRadioName" :checked="activeContainerId === vehicle.draftContainerId" :disabled="disabled" :aria-label="`选择第 ${vehicle.sequenceNo} 辆车`" @change="emit('selectContainer', vehicle.draftContainerId)" /> 第 {{ vehicle.sequenceNo }} 辆车 · {{ vehicle.capacity }} 托位</label><span>托数 {{ vehiclePalletCount(vehicle) ?? '—' }}/{{ vehicle.capacity }}</span></header>
@@ -213,19 +223,25 @@ watch(() => [props.disabled, props.warehouseSelected, props.draft, props.activeC
<footer v-if="vehicle.draftContainerId" class="cargo-board-vehicle-footer"><el-button link type="danger" :disabled="disabled" @click="emit('removeContainer', vehicle.draftContainerId)">删除车辆</el-button></footer>
</article>
</div>
</slot>
</div>
</section>
</slot>
</div>
</template>
<style scoped>
.dispatch-cargo-board { display: grid; grid-template-columns: minmax(0, 42fr) minmax(0, 22fr) minmax(0, 36fr); align-items: start; gap: var(--admin-space-6); }
.cargo-board-panel { min-width: 0; border: 1px solid var(--admin-table-border); border-radius: var(--admin-dialog-section-radius); overflow: hidden; background: var(--admin-surface); }
.cargo-board-heading { display: flex; align-items: center; gap: var(--admin-space-3); min-height: var(--admin-control-height); padding: var(--admin-space-3) var(--admin-space-5); flex-wrap: wrap; }
.dispatch-cargo-board { display: grid; grid-template-columns: minmax(0, 44fr) minmax(0, 22fr) minmax(0, 34fr); grid-template-rows: auto auto; align-items: start; gap: var(--admin-space-5) var(--admin-space-6); }
.cargo-board-panel { display: grid; grid-row: 1 / span 2; grid-template-rows: subgrid; min-width: 0; background: var(--admin-surface); }
.cargo-board-orders { grid-column: 1; }
.cargo-board-pallets { grid-column: 2; }
.cargo-board-vehicles { grid-column: 3; }
.cargo-board-order-content, .cargo-board-vehicle-content { min-width: 0; }
.cargo-board-heading { display: flex; align-items: center; gap: var(--admin-space-3); min-height: var(--admin-control-height); box-sizing: border-box; margin: 0; flex-wrap: wrap; }
.cargo-board-heading h3 { margin: 0; font-size: var(--admin-font-section); }
.cargo-board-heading > .el-button { margin-left: auto; }
.cargo-board-vehicle-tools { display: flex; align-items: center; gap: var(--admin-space-3); margin-left: auto; flex-wrap: wrap; }
.cargo-board-vehicle-tools > .el-button { margin-left: 0; }
.cargo-board-muted { color: var(--cc-muted); font-size: var(--admin-font-caption); }
.cargo-board-table-wrap { overflow: auto; max-height: 48vh; }
.cargo-board-table-wrap { overflow: auto; max-height: min(390px, 40vh); border: 1px solid var(--admin-table-border); border-radius: var(--admin-dialog-section-radius); }
.cargo-board-table { width: 100%; table-layout: fixed; }
.cargo-board-table td { overflow-wrap: anywhere; }
.cargo-board-table th:nth-child(1) { width: 82px; }
@@ -236,16 +252,16 @@ watch(() => [props.disabled, props.warehouseSelected, props.draft, props.activeC
.cargo-board-table tr.is-selected td { background: var(--admin-action-primary-hover); }
.cargo-board-business-no { font-weight: 600; }
.cargo-board-pagination { display: flex; flex-direction: column; align-items: flex-start; gap: var(--admin-space-3); padding: var(--admin-space-4); overflow-x: auto; }
.cargo-board-order-caption { padding: var(--admin-space-3) var(--admin-space-5); border-top: 1px solid var(--admin-table-border); overflow-wrap: anywhere; }
.cargo-board-order-caption { margin-left: auto; max-width: 65%; overflow-wrap: anywhere; color: var(--cc-muted); font-size: var(--admin-font-caption); }
.cargo-board-order-caption small { display: block; }
.cargo-board-empty { padding: var(--admin-space-7); color: var(--cc-muted); text-align: center; margin: 0; }
.cargo-board-pallet-list { max-height: 52vh; overflow-y: auto; }
.cargo-board-pallet { padding: var(--admin-space-5); border-top: 1px solid var(--admin-table-border); }
.cargo-board-pallet-list { max-height: min(500px, 48vh); overflow-y: auto; border: 1px solid var(--admin-table-border); border-radius: var(--admin-dialog-section-radius); }
.cargo-board-pallet-table { width: 100%; table-layout: fixed; }
.cargo-board-pallet-table .cargo-board-pallet-count { width: 72px; text-align: right; white-space: nowrap; }
.cargo-board-pallet-table td { vertical-align: top; overflow-wrap: anywhere; }
.cargo-board-pallet-table .cargo-board-empty { text-align: center; padding: var(--admin-space-5); }
.cargo-board-pallet.is-draggable { cursor: grab; }
.cargo-board-pallet.is-assigned { background: var(--admin-label-bg); }
.cargo-board-pallet-title { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--admin-space-3); overflow-wrap: anywhere; }
.cargo-board-pallet-title strong { min-width: 0; }
.cargo-board-pallet-title > span { white-space: nowrap; }
.cargo-board-pallet.is-assigned td { background: var(--admin-label-bg); }
.cargo-board-group-details { margin-top: var(--admin-space-2); font-size: var(--admin-font-caption); overflow-wrap: anywhere; }
.cargo-board-group-details summary { cursor: pointer; }
.cargo-board-group-details ul { margin: var(--admin-space-2) 0; padding-left: var(--admin-space-6); }