This commit is contained in:
2026-08-14 19:02:34 +08:00
parent 44ee2be246
commit 4dec54feef
14 changed files with 215 additions and 54 deletions
@@ -31,12 +31,15 @@ import type {
SmartLoadingPlan,
} from '../types'
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
const access = useAccessPreviewStore()
const router = useRouter()
const containers = ref<ContainerBookingResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const selectedContainerId = ref('')
const activeChannelCode = ref('')
const plan = ref<SmartLoadingPlan | null>(null)
const candidates = ref<SmartLoadingCandidate[]>([])
const loading = ref(false)
@@ -59,6 +62,21 @@ 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 selectedContainer = computed(() => containers.value.find((item) => item.id === selectedContainerId.value) || null)
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
const current = channels.get(item.channelCode)
if (current) current.containerCount += 1
else channels.set(item.channelCode, {
code: item.channelCode,
name: item.channelName || item.channelCode,
containerCount: 1,
})
}
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 selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
@@ -88,6 +106,7 @@ const filteredContainers = computed(() => {
const status = String(appliedQuery.value.status || '')
return containers.value.filter((item) => {
if (item.planStatus === 'COMPLETED') 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]
@@ -215,6 +234,9 @@ async function loadAll(preserved: Record<string, number> = {}) {
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 })))
: []
containers.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
if (!channelTabs.value.some((item) => item.code === activeChannelCode.value)) {
activeChannelCode.value = channelTabs.value[0]?.code || ''
}
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
await loadSelectedData(preserved)
} catch (error) {
@@ -241,6 +263,13 @@ async function selectContainer(containerId: string) {
mutationError.value = null
await loadSelectedData()
}
async function selectChannel(channelCode: string) {
if (activeChannelCode.value === channelCode) return
activeChannelCode.value = channelCode
selectedContainerId.value = preferredContainerId()
mutationError.value = null
await loadSelectedData()
}
async function applyFilters(model: QueryModel) {
appliedQuery.value = { ...model }
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
@@ -347,8 +376,23 @@ onMounted(() => { void load(); void loadCustomers() })
</script>
<template>
<AdminPage>
<component :is="props.embedded ? 'div' : AdminPage" :class="{ 'embedded-smart-loading': props.embedded }">
<section class="global-loading-page">
<nav v-if="channelTabs.length" class="channel-tabs" aria-label="物流渠道" role="tablist">
<button
v-for="channel in channelTabs"
:key="channel.code"
type="button"
role="tab"
:aria-selected="channel.code === activeChannelCode"
:class="{ active: channel.code === activeChannelCode }"
@click="selectChannel(channel.code)"
>
<span>{{ channel.name }}</span>
<small>{{ channel.containerCount }} 个柜次</small>
</button>
</nav>
<el-alert v-else-if="containers.length" type="warning" :closable="false" show-icon title="当前柜次缺少渠道快照,暂时不能参与智能装柜" />
<QueryToolbar
v-model="query"
ariaLabel="智能装柜筛选"
@@ -376,7 +420,7 @@ onMounted(() => { void load(); void loadCustomers() })
<section class="dispatch-grid" aria-label="智能装柜调度台">
<section class="dispatch-pane candidate-pane">
<header class="pane-header"><div><strong>候选订单与货物</strong><small>{{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
<header class="pane-header"><div><strong>候选入库单与货物</strong><small>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
<DataTable
:data="filteredCandidates"
:columns="candidateColumns"
@@ -407,7 +451,7 @@ onMounted(() => { void load(); void loadCustomers() })
</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
@@ -420,6 +464,7 @@ onMounted(() => { void load(); void loadCustomers() })
>
<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>{{ 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>
@@ -478,16 +523,23 @@ onMounted(() => { void load(); void loadCustomers() })
</AppDialog>
</template>
</section>
</AdminPage>
</component>
</template>
<style scoped>
.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.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; }
.summary-band { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1px; border: 1px solid var(--cc-border); background: var(--cc-border); }
.summary-band > div { display: grid; gap: 4px; padding: 12px 14px; background: #fff; }
.summary-band span,.pane-header small,.container-option span,.global-loading-page small { color: var(--cc-muted); font-size: 12px; }
.summary-band strong { font-size: 16px; }
.dispatch-grid { display: grid; grid-template-columns: minmax(390px, 1.1fr) 270px minmax(460px, 1.35fr); min-height: 620px; overflow-x: auto; border: 1px solid var(--cc-border); background: #fff; }
.embedded-smart-loading .dispatch-grid { min-height: 520px; height: min(62vh, 700px); }
.dispatch-pane { display: flex; min-width: 0; flex-direction: column; }
.dispatch-pane + .dispatch-pane { border-left: 1px solid var(--cc-border); }
.pane-header { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--cc-border); background: #fff; }
@@ -522,5 +574,6 @@ 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; }
}
</style>