This commit is contained in:
2026-09-14 20:03:27 +08:00
parent 66f49b97e5
commit 349e8f60df
44 changed files with 658 additions and 459 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, reactive, ref, watch } from 'vue'
import { ArrowRight, GripVertical, Plus, Trash2 } from '@lucide/vue'
import { GripVertical, Plus } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -391,7 +392,7 @@ watch([warehouseTab, () => query.orderType], () => {
<template>
<OperationErrorToast :details="mutationError" />
<AppDialog :model-value="modelValue" title="智能排车" width="min(1800px, calc(100vw - 40px))" :show-footer="false" @update:model-value="close($event)">
<AppDialog class="admin-density-dialog admin-density" :model-value="modelValue" title="智能排车" width="min(1800px, calc(100vw - 40px))" @update:model-value="close($event)">
<section class="dispatch-query" aria-label="智能排车筛选">
<el-input v-model="query.orderNo" clearable placeholder="订单号" @keyup.enter="load" />
<el-select v-model="query.orderType" clearable placeholder="全部订单类型"><el-option v-for="option in orderTypeOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select>
@@ -400,24 +401,24 @@ watch([warehouseTab, () => query.orderType], () => {
<el-button type="primary" :loading="loading" @click="load">查询</el-button><el-button :disabled="loading" @click="reset">重置</el-button>
</section>
<nav class="warehouse-tabs" aria-label="海外仓筛选">
<nav class="warehouse-tabs density-tabs" aria-label="海外仓筛选">
<button
v-for="tab in [
v-for="tab in [
{ value: 'ALL', label: '全部' },
{ value: 'LAX', label: '洛杉矶' },
{ value: 'NYC', label: '纽约' },
]"
:key="tab.value"
type="button"
:class="['warehouse-tab', { active: warehouseTab === tab.value }]"
@click="warehouseTab = tab.value as 'ALL' | 'LAX' | 'NYC'"
>
:key="tab.value"
type="button"
:class="['warehouse-tab', { active: warehouseTab === tab.value }]"
@click="warehouseTab = tab.value as 'ALL' | 'LAX' | 'NYC'"
>
{{ tab.label }}
</button>
</nav>
<LoadErrorState v-if="loadError" title="可排车入仓单加载失败" :message="loadError.message" :loading="loading" @retry="load"
><template #details><ApiErrorMeta :details="loadError" /></template
><template #details><ApiErrorMeta :details="loadError" /></template
></LoadErrorState>
<div v-else class="dispatch-workbench">
<section class="order-pane" aria-label="可排车入仓单">
@@ -426,7 +427,7 @@ watch([warehouseTab, () => query.orderType], () => {
<h3>
出库单列表 <span>({{ orderGroups.length }}单)</span>
</h3>
<div class="order-type-tabs">
<div class="order-type-tabs density-tabs">
<button v-for="option in [{ value: '', label: '全部' }, ...orderTypeOptions]" :key="option.value || 'all'" type="button" :class="{ active: query.orderType === option.value }" @click="query.orderType = option.value">
{{ option.label }}
</button>
@@ -436,28 +437,28 @@ watch([warehouseTab, () => query.orderType], () => {
<div v-loading="loading" class="order-table-wrap">
<el-table :data="orderGroups" border height="520" highlight-current-row row-key="key" @current-change="activeOrderKey = $event?.key || ''">
<el-table-column label="订单号" min-width="170"
><template #default="{ row }"><OrderReferenceText :value="text(row.first.orderNo)" /></template
><template #default="{ row }"><OrderReferenceText :value="text(row.first.orderNo)" /></template
></el-table-column>
<el-table-column label="换标单号" min-width="180"
><template #default="{ row }">{{ row.first.sourceStage === 'RELABEL' ? text(row.first.orderRefs) : '—' }}</template
><template #default="{ row }">{{ row.first.sourceStage === 'RELABEL' ? text(row.first.orderRefs) : '—' }}</template
></el-table-column>
<el-table-column label="原入仓码" min-width="150"
><template #default="{ row }">{{ text(row.first.inboundCode) }}</template
><template #default="{ row }">{{ text(row.first.inboundCode) }}</template
></el-table-column>
<el-table-column label="订单类型" width="100"
><template #default="{ row }">{{ orderType(row.first.orderType) }}</template></el-table-column
><template #default="{ row }">{{ orderType(row.first.orderType) }}</template></el-table-column
>
<el-table-column label="重量(lb)" width="100" align="right"
><template #default="{ row }">{{ number(Number(row.first.volumeWeightKg || 0) * 2.2046226218, 0) }}LB</template></el-table-column
><template #default="{ row }">{{ number(Number(row.first.volumeWeightKg || 0) * 2.2046226218, 0) }}LB</template></el-table-column
>
<el-table-column label="托盘数量" width="90" align="right"
><template #default="{ row }">{{ row.pallets.length }}</template></el-table-column
><template #default="{ row }">{{ row.pallets.length }}</template></el-table-column
>
<el-table-column label="目的地FBA" width="110"
><template #default="{ row }">{{ text(row.first.fbaWarehouseCode) }}</template></el-table-column
><template #default="{ row }">{{ text(row.first.fbaWarehouseCode) }}</template></el-table-column
>
<el-table-column label="体积(m³)" width="100" align="right"
><template #default="{ row }">{{
><template #default="{ row }">{{
number(
row.pallets.reduce((sum: number, item: DispatchPalletCandidateDto) => sum + Number(item.volumeCbm || 0), 0),
2,
@@ -465,13 +466,13 @@ watch([warehouseTab, () => query.orderType], () => {
}}</template></el-table-column
>
<el-table-column label="货柜号" min-width="125"
><template #default="{ row }">{{ text(row.first.containerNo) }}</template></el-table-column
><template #default="{ row }">{{ text(row.first.containerNo) }}</template></el-table-column
>
<el-table-column label="预约送仓时间" min-width="145"
><template #default="{ row }">{{ dateTime(row.first.appointmentAt) }}</template></el-table-column
><template #default="{ row }">{{ dateTime(row.first.appointmentAt) }}</template></el-table-column
>
<el-table-column label="预计装车时间" min-width="145"
><template #default="{ row }">{{ dateTime(row.first.expectedLoadingAt) }}</template></el-table-column
><template #default="{ row }">{{ dateTime(row.first.expectedLoadingAt) }}</template></el-table-column
>
</el-table>
</div>
@@ -500,10 +501,10 @@ watch([warehouseTab, () => query.orderType], () => {
</div>
<el-tag v-if="containerFor(palletId(item))" type="success" size="small">已锁定</el-tag>
<el-dropdown v-else trigger="click" :disabled="!containers.length || busyPalletIds.includes(palletId(item))" @command="movePallet(item, String($event))">
<el-button text type="primary" :loading="busyPalletIds.includes(palletId(item))">移入 <ArrowRight :size="14" /></el-button>
<el-button class="density-action" data-tone="primary" :loading="busyPalletIds.includes(palletId(item))">移入 </el-button>
<template #dropdown
><el-dropdown-menu
><el-dropdown-item v-for="(container, index) in containers" :key="container.id" :command="container.id" :disabled="container.palletIds.length >= container.capacity">装车托盘 {{ index + 1 }}({{ container.palletIds.length }}/{{ container.capacity }})</el-dropdown-item></el-dropdown-menu
><el-dropdown-menu
><el-dropdown-item v-for="(container, index) in containers" :key="container.id" :command="container.id" :disabled="container.palletIds.length >= container.capacity">装车托盘 {{ index + 1 }}({{ container.palletIds.length }}/{{ container.capacity }})</el-dropdown-item></el-dropdown-menu
></template
>
</el-dropdown>
@@ -525,28 +526,28 @@ watch([warehouseTab, () => query.orderType], () => {
</header>
<div class="container-list">
<div
v-for="(container, index) in containers"
:key="container.id"
class="loading-container"
:class="{
v-for="(container, index) in containers"
:key="container.id"
class="loading-container"
:class="{
'loading-container--overweight': containerWeightLb(container) > container.maxWeightLb * 1.1,
}"
@dragover.prevent
@drop.prevent="dropInto($event, container.id)"
>
@dragover.prevent
@drop.prevent="dropInto($event, container.id)"
>
<div class="container-head">
<strong>{{ container.vehicleLabel }}</strong
><span
>托数 <b>{{ container.palletIds.length }}</b
>托数 <b>{{ container.palletIds.length }}</b
>/{{ container.capacity }} · 重量 <b>{{ number(containerWeightLb(container), 0) }}LB</b>/{{ container.maxWeightLb }}LB <em v-if="containerWeightLb(container) > container.maxWeightLb * 1.1">超限10%</em></span
><el-button text type="danger" :aria-label="`删除装车车型${index + 1}`" @click="deleteContainer(container)"><Trash2 :size="15" /></el-button>
><el-button class="density-action" data-tone="danger" :aria-label="`删除装车车型${index + 1}`" @click="deleteContainer(container)">删除</el-button>
</div>
<div class="container-body">
<div v-for="id in container.palletIds" :key="id" class="loaded-pallet">
<span
><strong>{{ text(palletSnapshot(id)?.palletNo || '托盘号待补充') }}</strong
><strong>{{ text(palletSnapshot(id)?.palletNo || '托盘号待补充') }}</strong
><OrderReferenceText :value="text(palletSnapshot(id)?.orderNo)" /></span
><el-button text type="danger" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button>
><el-button class="density-action" data-tone="danger" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button>
</div>
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
</div>
@@ -556,12 +557,12 @@ watch([warehouseTab, () => query.orderType], () => {
</aside>
</div>
<footer class="dialog-footer">
<template #footer><footer class="dialog-footer">
<span>托盘移入即锁定,重量按 1kg = 2.2046lb 换算。</span>
<div><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="!assignedIds.length" @click="submit">预排车</el-button></div>
</footer>
</footer></template>
</AppDialog>
<el-dialog v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
<el-dialog class="admin-density-dialog" v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
<el-radio-group v-model="selectedVehicle" class="vehicle-options">
<el-radio v-for="spec in vehicleSpecs" :key="spec.value" :value="spec.value">{{ spec.label }} · 限重 {{ spec.maxWeightLb }}LB</el-radio>
</el-radio-group>
@@ -573,10 +574,10 @@ watch([warehouseTab, () => query.orderType], () => {
.dispatch-query {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
gap: var(--admin-space-5);
padding: var(--admin-space-5) var(--admin-space-5);
border: 1px solid #e6eaf2;
border-radius: 8px;
border-radius: var(--admin-radius);
background: #f7f8fb;
}
.dispatch-query :deep(.el-input),
@@ -584,40 +585,13 @@ watch([warehouseTab, () => query.orderType], () => {
width: 190px;
}
.warehouse-tabs {
display: flex;
gap: 10px;
margin-top: 16px;
}
.warehouse-tab,
.order-type-tabs button {
border: 1px solid #dfe4ed;
background: #fff;
color: #667085;
cursor: pointer;
transition: all 0.18s ease;
}
.warehouse-tab {
min-width: 92px;
padding: 9px 20px;
border-radius: 7px;
font-size: 14px;
}
.warehouse-tab:hover,
.order-type-tabs button:hover {
border-color: #7654d6;
color: #6941c6;
}
.warehouse-tab.active,
.order-type-tabs button.active {
border-color: #6f42d8;
background: #6f42d8;
color: #fff;
margin-top: var(--admin-space-5);
}
.dispatch-workbench {
display: grid;
grid-template-columns: minmax(550px, 1.5fr) minmax(330px, 0.85fr) minmax(390px, 1fr);
gap: 16px;
margin-top: 16px;
gap: var(--admin-space-5);
margin-top: var(--admin-space-5);
}
.order-pane,
.detail-pane,
@@ -631,7 +605,7 @@ watch([warehouseTab, () => query.orderType], () => {
min-height: 74px;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: var(--admin-space-4);
}
.detail-order-reference {
display: flex;
@@ -645,26 +619,17 @@ p {
margin: 0;
}
h3 {
font-size: 16px;
font-size: var(--admin-font-section);
}
h3 span,
header > span,
p {
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
font-weight: 400;
}
.order-type-tabs {
display: flex;
gap: 6px;
margin-top: 10px;
overflow-x: auto;
}
.order-type-tabs button {
flex: 0 0 auto;
padding: 5px 12px;
border-radius: 16px;
font-size: 12px;
margin-top: var(--admin-space-4);
}
.order-table-wrap {
min-height: 520px;
@@ -672,28 +637,16 @@ p {
.detail-pane,
.container-pane {
border-left: 1px solid var(--cc-border);
padding-left: 16px;
padding-left: var(--admin-space-5);
}
.pallet-list,
.container-list {
height: 520px;
overflow-y: auto;
border: 1px solid var(--cc-border);
border-radius: 6px;
border-radius: var(--admin-radius);
background: #fafbfc;
}
.pallet-row {
display: grid;
grid-template-columns: 20px minmax(0, 1fr) auto;
min-height: 94px;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--cc-border);
background: #fff;
cursor: grab;
transition: background 0.18s ease;
}
.pallet-row:hover {
background: #f8f6ff;
}
@@ -707,26 +660,26 @@ p {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
gap: var(--admin-space-1);
}
.pallet-copy > span,
.carton-marks > span {
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.carton-marks {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
gap: var(--admin-space-1);
}
.carton-mark-line {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
gap: var(--admin-space-2);
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.carton-mark-line > span {
overflow: hidden;
@@ -734,16 +687,16 @@ p {
white-space: nowrap;
}
.container-pane > .el-input {
margin-bottom: 10px;
margin-bottom: var(--admin-space-4);
}
.container-list {
height: 460px;
padding: 8px;
padding: var(--admin-space-3);
}
.loading-container {
margin-bottom: 10px;
margin-bottom: var(--admin-space-4);
border: 1px dashed #bcc6d5;
border-radius: 6px;
border-radius: var(--admin-radius);
background: #fff;
}
.loading-container--overweight {
@@ -757,44 +710,35 @@ p {
color: var(--el-color-danger);
}
.container-head em {
margin-left: 4px;
margin-left: var(--admin-space-1);
color: var(--el-color-danger);
font-style: normal;
font-weight: 700;
}
.container-head {
display: grid;
grid-template-columns: 1fr auto 32px;
min-height: 44px;
align-items: center;
gap: 8px;
padding: 0 8px 0 12px;
background: #f4f6fa;
}
.container-head span {
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.container-head b {
color: var(--el-color-primary);
}
.container-body {
min-height: 76px;
padding: 8px;
padding: var(--admin-space-3);
}
.drop-hint {
display: grid;
min-height: 60px;
place-items: center;
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.loaded-pallet {
display: flex;
min-height: 52px;
align-items: center;
justify-content: space-between;
gap: 8px;
gap: var(--admin-space-3);
border-bottom: 1px solid var(--cc-border);
}
.loaded-pallet > span {
@@ -803,29 +747,19 @@ p {
flex: 1;
flex-direction: column;
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.loaded-pallet strong {
color: var(--cc-text);
font-size: 13px;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin: 16px -20px -20px;
padding: 14px 20px;
border-top: 1px solid var(--cc-border);
background: #fafbfc;
font-size: var(--admin-font-body);
}
.dialog-footer > span {
color: var(--cc-muted);
font-size: 12px;
font-size: var(--admin-font-caption);
}
.vehicle-options {
display: grid;
gap: 14px;
gap: var(--admin-space-5);
}
@media (max-width: 1280px) {
.dispatch-workbench {
@@ -840,4 +774,30 @@ p {
height: 300px;
}
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--admin-space-5); margin: 0; padding: 0;
border-top: 1px solid var(--cc-border); background: transparent; border: 0; text-align: left; }
.container-head {
display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
min-height: 44px;
align-items: center;
gap: var(--admin-space-3);
padding: 0 var(--admin-space-3) 0 var(--admin-space-5);
background: #f4f6fa; }
.pallet-row {
display: grid;
grid-template-columns: 20px minmax(0, 1fr) auto; min-height: var(--admin-empty-height);
align-items: center;
gap: var(--admin-space-3);
padding: var(--admin-space-4) var(--admin-space-5);
border-bottom: 1px solid var(--cc-border);
background: #fff;
cursor: grab;
transition: background 0.18s ease; }
.order-pane header, .detail-pane header, .container-pane header { min-height: 60px; }
</style>