This commit is contained in:
2026-09-03 11:47:32 +08:00
parent 246cbc115e
commit d8b59ecca5
11 changed files with 314 additions and 119 deletions
@@ -32,13 +32,10 @@ import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLo
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
import {
SMART_LOADING_CHANNEL_GROUPS,
SMART_LOADING_WAREHOUSE_GROUPS,
resolveSmartLoadingChannelGroup,
resolveSmartLoadingWarehouseGroup,
} from '../smartLoadingChannelGroups'
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
const activeWarehouseCode = defineModel<string>('warehouseCode', { default: '' })
const access = useAccessPreviewStore()
const router = useRouter()
@@ -46,7 +43,7 @@ const containers = ref<ContainerBookingResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const selectedContainerId = ref('')
const activeChannelCode = ref('')
const selectedChannelCodes = ref<string[]>([])
const plan = ref<SmartLoadingPlan | null>(null)
const candidates = ref<SmartLoadingCandidate[]>([])
const loading = ref(false)
@@ -61,7 +58,7 @@ const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
const assignForm = reactive({ packageCount: 1 })
const quantityDrafts = reactive<Record<string, number>>({})
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
const query = ref<QueryModel>({ customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
const query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
const appliedQuery = ref<QueryModel>({ ...query.value })
const candidatePage = ref(1)
const candidatePageSize = ref(20)
@@ -77,16 +74,16 @@ const unmappedChannelContainerCount = computed(() => orchestratableContainers.va
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
)).length)
const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
resolveSmartLoadingWarehouseGroup(item.warehouseName)
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
)).length)
const unmappedWarehouseContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
!resolveSmartLoadingWarehouseGroup(item.warehouseName)
)).length)
const warehouseTabs = computed(() => SMART_LOADING_WAREHOUSE_GROUPS)
const channelTabs = computed(() => SMART_LOADING_CHANNEL_GROUPS)
const activeWarehouse = computed(() => warehouseTabs.value.find((item) => item.code === activeWarehouseCode.value) || null)
const activeChannel = computed(() => channelTabs.value.find((item) => item.code === activeChannelCode.value) || null)
const channelOptions = computed(() => SMART_LOADING_CHANNEL_GROUPS)
const warehouseOptions = computed(() => {
const options = new Map<string, string>()
for (const item of orchestratableContainers.value) {
if (item.warehouseId && !options.has(item.warehouseId)) options.set(item.warehouseId, item.warehouseName?.trim() || '未命名发货仓')
}
return [...options].map(([value, label]) => ({ value, label }))
})
const editable = computed(() => plan.value?.canEdit === true)
const assignable = computed(() => plan.value?.canAssign === true)
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
@@ -195,6 +192,7 @@ const candidateOrderStatusOptions = computed(() => [...new Set(candidates.value
.filter(Boolean))]
.map((status) => ({ value: status, label: candidateOrderStatusLabel(status) })))
const queryFields = computed<QueryField[]>(() => [
{ key: 'warehouseId', type: 'select', label: '发货仓', placeholder: '全部发货仓', width: 180, options: warehouseOptions.value, props: { filterable: true } },
{ 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: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 },
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 300 },
@@ -203,9 +201,8 @@ const queryFields = computed<QueryField[]>(() => [
const filteredContainers = computed(() => containers.value.filter((item) => (
['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)
&& resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === activeWarehouseCode.value
&& Boolean(activeChannelCode.value)
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === activeChannelCode.value
&& (!String(appliedQuery.value.warehouseId || '') || item.warehouseId === String(appliedQuery.value.warehouseId))
&& selectedChannelCodes.value.includes(resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code || '')
)))
const filteredCandidates = computed(() => {
const customerId = String(appliedQuery.value.customerId || '')
@@ -298,12 +295,6 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
}
}
function preferredChannelCode(warehouseCode: string) {
return channelTabs.value.find((group) => orchestratableContainers.value.some((item) => (
resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === warehouseCode
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === group.code
)))?.code || channelTabs.value[0]?.code || ''
}
function clearSelectionData() {
plan.value = null
candidates.value = []
@@ -363,13 +354,12 @@ 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 (!warehouseTabs.value.some((item) => item.code === activeWarehouseCode.value)) {
activeWarehouseCode.value = warehouseTabs.value.find((group) => orchestratableContainers.value.some((item) => (
resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === group.code
)))?.code || warehouseTabs.value[0]?.code || ''
}
if (!channelTabs.value.some((item) => item.code === activeChannelCode.value)) {
activeChannelCode.value = preferredChannelCode(activeWarehouseCode.value)
const availableChannelCodes = new Set(channelOptions.value.map((item) => item.code))
selectedChannelCodes.value = selectedChannelCodes.value.filter((code) => availableChannelCodes.has(code))
if (!selectedChannelCodes.value.length) {
selectedChannelCodes.value = [channelOptions.value.find((channel) => orchestratableContainers.value.some((item) => (
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === channel.code
)))?.code || channelOptions.value[0]?.code || '']
}
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
await loadSelectedData(preserved)
@@ -396,16 +386,12 @@ async function selectContainer(containerId: string) {
selectedContainerId.value = containerId
await loadSelectedData()
}
async function selectWarehouse(warehouseCode: string) {
if (activeWarehouseCode.value === warehouseCode) return
activeWarehouseCode.value = warehouseCode
selectedContainerId.value = preferredContainerId()
await loadSelectedData()
}
defineExpose({ selectWarehouse })
async function selectChannel(channelCode: string) {
if (activeChannelCode.value === channelCode) return
activeChannelCode.value = channelCode
async function selectChannels(codes: string[]) {
if (!codes.length) {
ElMessage.warning('至少选择一个渠道')
return
}
selectedChannelCodes.value = codes
selectedContainerId.value = preferredContainerId()
await loadSelectedData()
}
@@ -575,13 +561,14 @@ onMounted(() => { void load(); void loadCustomers() })
<template>
<component :is="props.embedded ? 'div' : AdminPage" :class="{ 'embedded-smart-loading': props.embedded }">
<section class="global-loading-page">
<div v-if="channelTabs.length" class="channel-summary-row">
<el-tabs :model-value="activeChannelCode" class="channel-tabs" aria-label="物流渠道" @tab-change="selectChannel(String($event))">
<el-tab-pane v-for="channel in channelTabs" :key="channel.code" :label="channel.name" :name="channel.code" />
</el-tabs>
<dl class="compact-summary" aria-label="当前渠道装柜统计">
<div v-if="channelOptions.length" class="channel-summary-row">
<el-checkbox-group :model-value="selectedChannelCodes" class="channel-tags" aria-label="渠道筛选,至少选择一项" @change="selectChannels($event as string[])">
<span class="channel-tags__label">渠道</span>
<el-checkbox-button v-for="channel in channelOptions" :key="channel.code" :label="channel.code" :value="channel.code">{{ channel.name }}</el-checkbox-button>
</el-checkbox-group>
<dl class="compact-summary" aria-label="已选渠道装柜统计">
<div><dt>候选明细</dt><dd><strong>{{ filteredCandidates.length }}</strong> 条</dd></div>
<div><dt>柜次</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
<div><dt>柜子数量</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
<div><dt>已装</dt><dd><strong>{{ totalPackages }}</strong> 箱</dd></div>
<div><dt>总重量</dt><dd><strong>{{ formatNumber(totalWeight, 1) }}</strong> kg</dd></div>
<div><dt>总体积</dt><dd><strong>{{ formatNumber(totalVolume) }}</strong> m³</dd></div>
@@ -589,7 +576,6 @@ onMounted(() => { void load(); void loadCustomers() })
</div>
<el-alert v-if="missingChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少渠道快照,已隔离;请补齐后再参与智能装柜`" />
<el-alert v-if="unmappedChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedChannelContainerCount} 个可编排柜次的渠道未纳入固定 9 类,已隔离`" />
<el-alert v-if="unmappedWarehouseContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedWarehouseContainerCount} 个可编排柜次不属于义乌仓或东莞仓,已隔离`" />
<el-alert v-if="completedContainerCount && !orchestratableContainers.length" type="success" :closable="false" show-icon title="当前柜次已完成装柜,不可继续编辑" />
<QueryToolbar
v-model="query"
@@ -599,6 +585,7 @@ onMounted(() => { void load(); void loadCustomers() })
preference-key="global-smart-loading-query-v2"
:search-on-reset="false"
@search="applyFilters"
@reset="applyFilters"
/>
<LoadErrorState v-if="loadError" title="智能装柜工作台加载失败" :message="loadError.message" :loading="loading" @retry="load">
@@ -610,7 +597,7 @@ onMounted(() => { void load(); void loadCustomers() })
</LoadErrorState>
<section class="dispatch-grid" aria-label="智能装柜调度台">
<section class="dispatch-pane candidate-pane">
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeWarehouse?.name || '未选择仓库' }} · {{ activeChannel?.name || '未选择渠道' }}</small></div></header>
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ selectedContainer?.warehouseName || '未选择发货仓' }} · {{ resolveSmartLoadingChannelGroup(selectedContainer?.channelCode, selectedContainer?.channelName)?.name || '未选择渠道' }}</small></div></header>
<DataTable
v-model:page="candidatePage"
v-model:page-size="candidatePageSize"
@@ -642,7 +629,7 @@ onMounted(() => { void load(); void loadCustomers() })
</section>
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
<header class="pane-header"><div><strong>当前渠道柜次</strong></div></header>
<header class="pane-header"><div><strong>已选渠道柜次</strong></div></header>
<div v-loading="loading" class="container-list">
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
<button
@@ -741,13 +728,12 @@ onMounted(() => { void load(); void loadCustomers() })
<style scoped>
.global-loading-page { display: grid; gap: 12px; min-width: 0; }
.embedded-smart-loading { min-width: 0; }
.channel-summary-row { display: grid; grid-template-columns: minmax(360px, 1fr) auto; min-width: 0; align-items: end; border-bottom: 2px solid var(--cc-border); background: #fff; }
.channel-tabs { min-width: 0; }
.channel-tabs :deep(.el-tabs__header) { margin: 0 0 -2px; }
.channel-tabs :deep(.el-tabs__nav-wrap::after) { display: none; }
.channel-tabs :deep(.el-tabs__item) { overflow: hidden; max-width: 240px; height: 48px; padding-inline: 18px; text-overflow: ellipsis; white-space: nowrap; }
.channel-tabs :deep(.el-tabs__content) { display: none; }
.compact-summary { display: flex; min-height: 48px; align-items: center; justify-content: flex-end; gap: 16px; margin: 0; padding: 6px 12px; white-space: nowrap; }
.channel-summary-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; min-width: 0; align-items: center; border-bottom: 2px solid var(--cc-border); background: #fff; }
.channel-tags { display: flex; min-width: 0; align-items: center; flex-wrap: nowrap; gap: 6px; padding: 6px 0; overflow-x: auto; }
.channel-tags__label { flex: 0 0 auto; margin-right: 2px; color: var(--cc-text); font-size: 13px; font-weight: 600; }
.channel-tags :deep(.el-checkbox-button) { flex: 0 0 auto; }
.channel-tags :deep(.el-checkbox-button__inner) { height: 34px; padding: 0 10px; border: 1px solid var(--cc-border) !important; border-radius: 6px !important; box-shadow: none !important; font-size: 13px; line-height: 32px; }
.compact-summary { display: flex; min-height: 46px; align-items: center; justify-content: flex-end; gap: 12px; margin: 0; padding: 5px 8px; white-space: nowrap; }
.compact-summary > div { display: flex; align-items: baseline; gap: 4px; }
.compact-summary dt { color: var(--cc-muted); font-size: 12px; }
.compact-summary dd { margin: 0; color: var(--cc-text); font-size: 12px; }
@@ -803,6 +789,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 :deep(.el-tabs__item) { max-width: 180px; padding-inline: 12px; }
.channel-tags { padding-inline: 8px; }
}
</style>