优化
This commit is contained in:
@@ -14,7 +14,7 @@ vi.mock('@/shared/api/http', () => ({
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
|
||||
const container = { id: 9007199254740993, planId: 9007199254740994, warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, version: '2', capacityCbm: '68', maxWeightKg: '28000' }
|
||||
const container = { id: 9007199254740993, planId: 9007199254740994, warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', version: '2', capacityCbm: '68', maxWeightKg: '28000' }
|
||||
const plan = { id: 9007199254740993, bookingId: 9007199254740994, version: '3', status: 'EDITING', items: [{ id: 9007199254740995, loadingPlanId: 9007199254740993, accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, destinationWarehouseId: 5, packageCount: '6', version: '1' }] }
|
||||
|
||||
describe('platform container loading API', () => {
|
||||
@@ -32,7 +32,7 @@ describe('platform container loading API', () => {
|
||||
it('normalizes paged container ids and numeric metadata', async () => {
|
||||
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [container], page: '1', size: '20', total: '1', pages: '1' } })
|
||||
const result = await listContainers({ page: 1, size: 20 })
|
||||
expect(result.records[0]).toMatchObject({ id: String(container.id), planId: String(container.planId), warehouseId: String(container.warehouseId), version: 2, capacityCbm: 68 })
|
||||
expect(result.records[0]).toMatchObject({ id: String(container.id), planId: String(container.planId), warehouseId: String(container.warehouseId), channelCode: 'NY_EXX', channelName: '纽约EXX海铁', version: 2, capacityCbm: 68 })
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/containers', { params: { page: 1, size: 20 } })
|
||||
})
|
||||
|
||||
@@ -49,9 +49,9 @@ describe('platform container loading API', () => {
|
||||
it('normalizes every long id in candidates and plans', async () => {
|
||||
vi.mocked(http.get)
|
||||
.mockResolvedValueOnce({ code: '200', data: container })
|
||||
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, warehouseId: 5, destinationWarehouseId: 6, availablePackageCount: '7' }] })
|
||||
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', availablePackageCount: '7' }] })
|
||||
expect((await getContainer('9')).id).toBe(String(container.id))
|
||||
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundDetailId: '3', destinationWarehouseId: '6', availablePackageCount: 7 }])
|
||||
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundDetailId: '3', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', availablePackageCount: 7 }])
|
||||
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/candidates', { params: { bookingId: '7', warehouseId: '5' } })
|
||||
})
|
||||
|
||||
|
||||
@@ -143,7 +143,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
|
||||
expect(wrapper.text()).not.toContain('税率')
|
||||
expect(wrapper.text()).not.toContain('是否退税')
|
||||
expect(wrapper.text()).not.toContain('税率')
|
||||
})
|
||||
|
||||
it('prefers an editable container on first load so pushed plans do not show a false editing error', async () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from '../api/platformContainerLoading'
|
||||
import type {
|
||||
ContainerBookingResource,
|
||||
ContainerPlanStatus,
|
||||
SmartLoadingCandidate,
|
||||
SmartLoadingItem,
|
||||
SmartLoadingPlan,
|
||||
@@ -65,7 +64,7 @@ const selectedContainer = computed(() => containers.value.find((item) => item.id
|
||||
const channelTabs = computed(() => {
|
||||
const channels = new Map<string, { code: string; name: string; containerCount: number }>()
|
||||
for (const item of containers.value) {
|
||||
if (item.planStatus === 'COMPLETED' || !item.channelCode) continue
|
||||
if (!['WAIT_LOADING', 'EDITING'].includes(item.planStatus) || !item.channelCode) continue
|
||||
const current = channels.get(item.channelCode)
|
||||
if (current) current.containerCount += 1
|
||||
else channels.set(item.channelCode, {
|
||||
@@ -77,7 +76,7 @@ const channelTabs = computed(() => {
|
||||
return [...channels.values()]
|
||||
})
|
||||
const activeChannel = computed(() => channelTabs.value.find((item) => item.code === activeChannelCode.value) || null)
|
||||
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalPackages = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
@@ -93,11 +92,10 @@ const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、货物', width: 250 },
|
||||
{ key: 'containerKeyword', type: 'input', label: '柜次', placeholder: '柜次号、集装箱号、目的仓', width: 230 },
|
||||
{ key: 'containerKeyword', type: 'input', label: '柜号', placeholder: '请输入柜号', width: 230 },
|
||||
{ key: 'status', type: 'select', label: '柜次状态', width: 160, options: [
|
||||
{ label: '待装柜', value: 'WAIT_LOADING' },
|
||||
{ label: '预装柜中', value: 'EDITING' },
|
||||
{ label: '拣货中', value: 'LOADING' },
|
||||
] },
|
||||
])
|
||||
|
||||
@@ -105,12 +103,11 @@ const filteredContainers = computed(() => {
|
||||
const keyword = String(appliedQuery.value.containerKeyword || '').trim().toLowerCase()
|
||||
const status = String(appliedQuery.value.status || '')
|
||||
return containers.value.filter((item) => {
|
||||
if (item.planStatus === 'COMPLETED') return false
|
||||
if (!['WAIT_LOADING', 'EDITING'].includes(item.planStatus)) return false
|
||||
if (!activeChannelCode.value || item.channelCode !== activeChannelCode.value) return false
|
||||
if (status && item.planStatus !== status) return false
|
||||
if (!keyword) return true
|
||||
return [item.cabinetNo, item.containerNo, item.bookingNo, item.destinationWarehouseCode, item.destinationWarehouseName]
|
||||
.some((value) => String(value || '').toLowerCase().includes(keyword))
|
||||
return displayContainerNo(item).toLowerCase().includes(keyword)
|
||||
})
|
||||
})
|
||||
const filteredCandidates = computed(() => {
|
||||
@@ -139,7 +136,6 @@ const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 100 },
|
||||
{ key: 'unitVolumeCbm', label: '单箱体积', width: 105, slotName: 'unitVolume' },
|
||||
{ key: 'unitWeightKg', label: '单箱重量', width: 105, slotName: 'unitWeight' },
|
||||
{ key: 'destination', label: '目的仓', minWidth: 130, slotName: 'destination' },
|
||||
]
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' },
|
||||
@@ -148,7 +144,6 @@ const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' },
|
||||
{ key: 'volumeCbm', label: '体积', width: 105, slotName: 'volume' },
|
||||
{ key: 'grossWeightKg', label: '毛重', width: 110, slotName: 'weight' },
|
||||
{ key: 'destination', label: '目的仓', minWidth: 130, slotName: 'destination' },
|
||||
]
|
||||
|
||||
function formatNumber(value: number, digits = 3) {
|
||||
@@ -157,16 +152,11 @@ function formatNumber(value: number, digits = 3) {
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function statusLabel(status: ContainerPlanStatus) {
|
||||
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as const)[status]
|
||||
}
|
||||
function statusType(status: ContainerPlanStatus) {
|
||||
return status === 'WAIT_LOADING' ? 'info' : status === 'EDITING' ? 'primary' : status === 'LOADING' ? 'warning' : 'success'
|
||||
}
|
||||
function routeMatches(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
const target = selectedContainer.value?.destinationWarehouseCode
|
||||
return !target || target === candidate.destinationWarehouseCode
|
||||
function displayContainerNo(container: ContainerBookingResource | null | undefined) {
|
||||
if (!container) return '柜号待生成'
|
||||
const actual = String(container.containerNo || '').trim()
|
||||
const internalReference = /^(?:MAN|SPLIT)-/i.test(actual)
|
||||
return actual && !internalReference ? actual : String(container.cabinetNo || '').trim() || '柜号待生成'
|
||||
}
|
||||
function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
|
||||
@@ -412,7 +402,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
|
||||
<section class="summary-band" aria-label="当前装柜汇总">
|
||||
<div><span>候选货物</span><strong>{{ filteredCandidates.length }}</strong></div>
|
||||
<div><span>可见柜次</span><strong>{{ filteredContainers.length }}</strong></div>
|
||||
<div><span>可见空柜</span><strong>{{ filteredContainers.length }}</strong></div>
|
||||
<div><span>当前已装箱数</span><strong>{{ totalPackages }}</strong></div>
|
||||
<div><span>当前总体积</span><strong>{{ formatNumber(totalVolume) }} m³</strong></div>
|
||||
<div><span>当前总重量</span><strong>{{ formatNumber(totalWeight, 1) }} kg</strong></div>
|
||||
@@ -436,22 +426,18 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!routeMatches(row) || assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1"
|
||||
:disabled="assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1"
|
||||
@click="openAssign(row)"
|
||||
><Plus :size="15" />加入</el-button>
|
||||
</template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
<template #unitVolume="{ row }">{{ formatNumber(Number(row.unitVolumeCbm || 0)) }} m³</template>
|
||||
<template #unitWeight="{ row }">{{ formatNumber(Number(row.unitWeightKg || 0), 1) }} kg</template>
|
||||
<template #destination="{ row }">
|
||||
<el-tag :type="routeMatches(row) ? 'success' : 'danger'">{{ routeMatches(row) ? '匹配' : '不匹配' }}</el-tag>
|
||||
<small>{{ row.destinationWarehouseName || row.destinationWarehouseCode || '未设置' }}</small>
|
||||
</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
|
||||
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
|
||||
<header class="pane-header"><div><strong>当前渠道柜次</strong><small>待装柜、预装柜中与拣货中</small></div></header>
|
||||
<header class="pane-header"><div><strong>当前渠道空柜</strong><small>仅显示待装柜和预装编辑中的柜次</small></div></header>
|
||||
<div v-loading="loading" class="container-list">
|
||||
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
|
||||
<button
|
||||
@@ -462,11 +448,10 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
:class="{ selected: item.id === selectedContainerId }"
|
||||
@click="selectContainer(item.id)"
|
||||
>
|
||||
<span class="container-option__top"><strong>{{ item.cabinetNo }}</strong><el-tag size="small" :type="statusType(item.planStatus)">{{ statusLabel(item.planStatus) }}</el-tag></span>
|
||||
<span>{{ item.containerNo || '集装箱号待录入' }} · {{ item.containerType }}</span>
|
||||
<span class="container-option__top"><strong>{{ displayContainerNo(item) }}</strong></span>
|
||||
<span>箱型 {{ item.containerType }}</span>
|
||||
<span>{{ item.channelName || item.channelCode || '渠道未设置' }}</span>
|
||||
<span>{{ item.warehouseName || '仓库未设置' }}</span>
|
||||
<span>目的仓 {{ item.destinationWarehouseName || item.destinationWarehouseCode || '不限' }}</span>
|
||||
<span v-if="item.id === selectedContainerId && plan">体积 {{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</span>
|
||||
<span v-else>容量 {{ formatNumber(Number(item.capacityCbm || 0)) }} m³ · {{ formatNumber(Number(item.maxWeightKg || 0), 0) }} kg</span>
|
||||
</button>
|
||||
@@ -475,7 +460,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
|
||||
<section class="dispatch-pane plan-pane">
|
||||
<header class="pane-header plan-header">
|
||||
<div><strong>{{ selectedContainer?.cabinetNo || '当前柜次' }} 装柜方案</strong><small>{{ plan ? `${plan.planNo} · 版本 ${plan.version}` : '尚未创建方案' }}</small></div>
|
||||
<div><strong>{{ displayContainerNo(selectedContainer) }} 装柜方案</strong><small>{{ plan ? '当前柜号的装柜明细' : '尚未创建方案' }}</small></div>
|
||||
<div class="plan-actions">
|
||||
<el-tooltip content="刷新当前柜次" placement="top"><el-button aria-label="刷新当前柜次" :loading="selectionLoading" @click="loadSelectedData()"><RefreshCw :size="16" /></el-button></el-tooltip>
|
||||
<el-button v-if="canAssign && editable" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
@@ -508,7 +493,6 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="1" :step="1" :disabled="!canAssign || !editable" controls-position="right" /></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
<template #destination="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
</section>
|
||||
@@ -530,7 +514,8 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.global-loading-page { display: grid; gap: 16px; min-width: 0; }
|
||||
.embedded-smart-loading { min-width: 0; }
|
||||
.channel-tabs { display: flex; min-height: 48px; overflow-x: auto; border-bottom: 2px solid var(--cc-border); background: #fff; }
|
||||
.channel-tabs button { display: flex; min-width: 150px; min-height: 48px; align-items: center; justify-content: center; gap: 7px; padding: 8px 18px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--cc-text); cursor: pointer; }
|
||||
.channel-tabs button { display: flex; min-width: 150px; min-height: 48px; flex: 0 0 auto; align-items: center; justify-content: center; gap: 7px; padding: 8px 18px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--cc-text); cursor: pointer; }
|
||||
.channel-tabs button > span { overflow: hidden; max-width: 220px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.channel-tabs button.active { margin-bottom: -2px; border-bottom-color: var(--el-color-primary); color: var(--el-color-primary); font-weight: 600; }
|
||||
.channel-tabs button:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: -3px; }
|
||||
.channel-tabs small { color: var(--cc-muted); font-weight: 400; white-space: nowrap; }
|
||||
@@ -566,7 +551,9 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.assign-dialog :deep(.el-input-number) { width: 100%; }
|
||||
@media (max-width: 1180px) {
|
||||
.dispatch-grid { grid-template-columns: 1fr; overflow: visible; }
|
||||
.embedded-smart-loading .dispatch-grid { height: auto; }
|
||||
.dispatch-pane + .dispatch-pane { border-top: 1px solid var(--cc-border); border-left: 0; }
|
||||
.candidate-pane,.plan-pane { height: min(70vh, 600px); }
|
||||
.container-list { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); max-height: 360px; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
@@ -574,6 +561,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.pane-header,.plan-actions,.meter-title { align-items: stretch; flex-direction: column; }
|
||||
.plan-actions :deep(.el-button) { width: 100%; min-height: 40px; margin-left: 0; }
|
||||
.container-list { grid-template-columns: 1fr; }
|
||||
.channel-tabs button { min-width: 132px; padding-inline: 12px; }
|
||||
.channel-tabs button { min-width: 156px; align-items: flex-start; flex-direction: column; gap: 2px; padding-inline: 12px; text-align: left; }
|
||||
.channel-tabs button > span { max-width: 132px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,7 @@ const canAssign = computed(() => access.can('platform:firstleg:smart-loading:ass
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list'))
|
||||
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0))
|
||||
@@ -59,7 +59,6 @@ const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'packageCount', label: '分配箱数', width: 150, slotName: 'packageCount' },
|
||||
{ key: 'volumeCbm', label: '体积', width: 120, slotName: 'volume' },
|
||||
{ key: 'grossWeightKg', label: '毛重', width: 130, slotName: 'weight' },
|
||||
{ key: 'destination', label: '目的仓', minWidth: 160, slotName: 'destination' },
|
||||
]
|
||||
const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
@@ -69,27 +68,26 @@ const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 110 },
|
||||
{ key: 'unitVolumeCbm', label: '单箱体积', width: 120, slotName: 'unitVolume' },
|
||||
{ key: 'unitWeightKg', label: '单箱重量', width: 120, slotName: 'unitWeight' },
|
||||
{ key: 'route', label: '目的仓匹配', minWidth: 170, slotName: 'route' },
|
||||
]
|
||||
|
||||
function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0' }
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function routeMatches(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
const target = container.value?.destinationWarehouseCode
|
||||
return !target || target === candidate.destinationWarehouseCode
|
||||
}
|
||||
function explainCandidate(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
if (!routeMatches(candidate)) return `目的仓不一致(候选:${candidate.destinationWarehouseCode || '未设置'})`
|
||||
const volume = Number(candidate.unitVolumeCbm || 0)
|
||||
const weight = Number(candidate.unitWeightKg || 0)
|
||||
const volumeRoom = volumeLimit.value ? Math.floor(Math.max(volumeLimit.value - totalVolume.value, 0) / Math.max(volume, 0.000001)) : candidate.availablePackageCount
|
||||
const weightRoom = weightLimit.value ? Math.floor(Math.max(weightLimit.value - totalWeight.value, 0) / Math.max(weight, 0.000001)) : candidate.availablePackageCount
|
||||
return `按 110% 上限,当前最多可装 ${Math.max(Math.min(candidate.availablePackageCount, volumeRoom, weightRoom), 0)} 箱`
|
||||
}
|
||||
function displayContainerNo(value: ContainerBookingResource | null | undefined) {
|
||||
if (!value) return '柜号待生成'
|
||||
const actual = String(value.containerNo || '').trim()
|
||||
const internalReference = /^(?:MAN|SPLIT)-/i.test(actual)
|
||||
return actual && !internalReference ? actual : String(value.cabinetNo || '').trim() || '柜号待生成'
|
||||
}
|
||||
function syncDrafts() { for (const item of plan.value?.items || []) quantityDrafts[item.id] = item.packageCount }
|
||||
function acceptPlan(next: SmartLoadingPlan) { plan.value = next; noDraft.value = false; syncDrafts() }
|
||||
|
||||
@@ -97,7 +95,7 @@ async function load() {
|
||||
loading.value = true; loadError.value = null; noDraft.value = false
|
||||
try {
|
||||
container.value = await getContainer(containerId.value)
|
||||
if (container.value.planStatus === 'COMPLETED') {
|
||||
if (['LOADING', 'COMPLETED'].includes(container.value.planStatus)) {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
return
|
||||
@@ -159,7 +157,7 @@ async function adjust(source: object) {
|
||||
async function remove(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
await ElMessageBox.confirm(`确认从计划移除“${item.productNameZh || item.orderNo || item.id}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => {
|
||||
await ElMessageBox.confirm(`确认从计划移除“${item.productNameZh || item.orderNo || '该货物'}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => {
|
||||
if (!confirmed || !plan.value) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
@@ -202,9 +200,8 @@ onMounted(load)
|
||||
<section class="business-page workbench">
|
||||
<section class="surface-panel workbench-command-bar" aria-label="排柜作业摘要">
|
||||
<div class="workbench-identity">
|
||||
<span>当前柜次</span>
|
||||
<strong>{{ container?.cabinetNo || '正在加载' }}</strong>
|
||||
<small>集装箱号 {{ container?.containerNo || '待录入' }}</small>
|
||||
<span>当前柜号</span>
|
||||
<strong>{{ loading ? '正在加载' : displayContainerNo(container) }}</strong>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button text aria-label="返回货柜列表" @click="router.back()"><ArrowLeft :size="18" />返回</el-button>
|
||||
@@ -222,26 +219,29 @@ onMounted(load)
|
||||
<p>智能装柜方案已锁定,不能继续调整。请进入装柜清单查看完整记录。</p>
|
||||
<el-button v-if="canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />查看装柜清单</el-button>
|
||||
</section>
|
||||
<section v-else-if="container?.planStatus === 'LOADING'" class="readonly-status-card" aria-label="已进入拣货流程">
|
||||
<strong>该柜号已进入拣货流程</strong>
|
||||
<p>智能装柜仅处理待装柜和预装编辑中的空柜,请在预装清单继续查看。</p>
|
||||
<el-button v-if="canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />查看预装清单</el-button>
|
||||
</section>
|
||||
<template v-else>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div><div><span>目的仓</span><strong>{{ container.destinationWarehouseName || container.destinationWarehouseCode || '不限' }}</strong></div><div><span>计划状态</span><strong>{{ plan?.status === 'LOADING' ? '拣货中' : plan ? '预装柜中' : '待装柜' }}</strong></div></section>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
<template v-else-if="plan">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" stripe preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<template #toolbar-start><div><h2>装柜方案</h2><p>计划 {{ plan.planNo }} · 版本 {{ plan.version }};智能建议必须经人工核对。</p></div></template>
|
||||
<template #toolbar-start><div><h2>{{ displayContainerNo(container) }} 装柜方案</h2><p>智能建议必须经人工核对。</p></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="1" :step="1" :disabled="!canAssign || !editable" controls-position="right" /></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
<template #destination="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template>
|
||||
</DataTable>
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" stripe preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于仓库、目的仓、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="!routeMatches(row) || assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于装柜仓库、物流渠道、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
<template #unitVolume="{ row }">{{ formatNumber(Number(row.unitVolumeCbm || 0)) }} m³</template><template #unitWeight="{ row }">{{ formatNumber(Number(row.unitWeightKg || 0),1) }} kg</template>
|
||||
<template #route="{ row }"><el-tag :type="routeMatches(row) ? 'success' : 'danger'">{{ routeMatches(row) ? '匹配' : '冲突' }}</el-tag><small>{{ explainCandidate(row) }}</small></template>
|
||||
</DataTable>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user