This commit is contained in:
2026-09-03 10:27:31 +08:00
parent 6270573095
commit 3c68ef4ddf
6 changed files with 316 additions and 25 deletions
@@ -30,8 +30,15 @@ import type {
} from '../types'
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
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()
@@ -66,17 +73,19 @@ const selectedContainer = computed(() => containers.value.find((item) => item.id
const orchestratableContainers = computed(() => containers.value.filter((item) => ['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)))
const completedContainerCount = computed(() => containers.value.filter((item) => item.planStatus === 'COMPLETED').length)
const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length)
const channelTabs = computed(() => {
const channels = new Map<string, { code: string; name: string }>()
for (const item of orchestratableContainers.value) {
if (!item.channelCode) continue
if (!channels.has(item.channelCode)) channels.set(item.channelCode, {
code: item.channelCode,
name: item.channelName || item.channelCode,
})
}
return [...channels.values()]
})
const unmappedChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
)).length)
const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
resolveSmartLoadingWarehouseGroup(item.warehouseName)
&& 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 editable = computed(() => plan.value?.canEdit === true)
const assignable = computed(() => plan.value?.canAssign === true)
@@ -194,8 +203,9 @@ 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)
&& item.channelCode === activeChannelCode.value
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === activeChannelCode.value
)))
const filteredCandidates = computed(() => {
const customerId = String(appliedQuery.value.customerId || '')
@@ -204,7 +214,7 @@ const filteredCandidates = computed(() => {
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
: []
const channelCode = selectedContainer.value?.channelCode || activeChannelCode.value
const channelCode = selectedContainer.value?.channelCode
return displayedCandidates.value.filter((item) => {
if (!channelCode || item.channelCode !== channelCode) return false
if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false
@@ -287,6 +297,13 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
if (syncContainerStatus) container.planStatus = next.status
}
}
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 = []
@@ -346,8 +363,13 @@ 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 = channelTabs.value[0]?.code || ''
activeChannelCode.value = preferredChannelCode(activeWarehouseCode.value)
}
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
await loadSelectedData(preserved)
@@ -374,6 +396,14 @@ async function selectContainer(containerId: string) {
selectedContainerId.value = containerId
await loadSelectedData()
}
async function selectWarehouse(warehouseCode: string) {
if (activeWarehouseCode.value === warehouseCode) return
activeWarehouseCode.value = warehouseCode
activeChannelCode.value = preferredChannelCode(warehouseCode)
selectedContainerId.value = preferredContainerId()
await loadSelectedData()
}
defineExpose({ selectWarehouse })
async function selectChannel(channelCode: string) {
if (activeChannelCode.value === channelCode) return
activeChannelCode.value = channelCode
@@ -558,10 +588,13 @@ onMounted(() => { void load(); void loadCustomers() })
<div><dt>总体积</dt><dd><strong>{{ formatNumber(totalVolume) }}</strong> m³</dd></div>
</dl>
</div>
<el-alert v-else-if="missingChannelContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少渠道快照,已隔离;请补齐后再参与智能装柜`" />
<el-alert v-else-if="completedContainerCount" type="success" :closable="false" show-icon title="当前柜次已完成装柜,不可继续编辑" />
<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"
class="smart-loading-query"
ariaLabel="智能装柜筛选"
:fields="queryFields"
preference-key="global-smart-loading-query-v2"
@@ -578,7 +611,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>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeWarehouse?.name || '未选择仓库' }} · {{ activeChannel?.name || '未选择渠道' }}</small></div></header>
<DataTable
v-model:page="candidatePage"
v-model:page-size="candidatePageSize"
@@ -629,7 +662,7 @@ onMounted(() => { void load(); void loadCustomers() })
</span>
<span class="container-option__identity">柜次号:{{ displayCabinetNo(item) }}</span>
<span>集装箱号:{{ displayActualContainerNo(item) }}</span>
<span>渠道:{{ item.channelName || item.channelCode || '渠道待补充' }}</span>
<span>渠道:{{ resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.name || '渠道待补充' }}</span>
<span>仓库:{{ item.warehouseName || '仓库待补充' }} · 柜型:{{ item.containerType || '柜型待补充' }}</span>
<!-- 空柜的操作时机由状态和操作按钮表达,不在每张柜次卡片重复提示。 -->
</button>
@@ -707,9 +740,9 @@ onMounted(() => { void load(); void loadCustomers() })
</template>
<style scoped>
.global-loading-page { display: grid; gap: 16px; min-width: 0; }
.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(240px, 1fr) auto; min-width: 0; align-items: end; border-bottom: 2px solid var(--cc-border); background: #fff; }
.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; }
@@ -720,6 +753,7 @@ onMounted(() => { void load(); void loadCustomers() })
.compact-summary dt { color: var(--cc-muted); font-size: 12px; }
.compact-summary dd { margin: 0; color: var(--cc-text); font-size: 12px; }
.compact-summary strong { color: var(--el-color-primary); font-size: 14px; }
:deep(.smart-loading-query.query-toolbar) { padding: 0; border: 0; background: transparent; }
.pane-header small,.container-option span,.global-loading-page small { color: var(--cc-muted); font-size: 12px; }
.dispatch-grid { display: grid; grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr); height: clamp(520px, 68vh, 720px); min-height: 0; overflow-x: auto; border: 1px solid var(--cc-border); background: #fff; }
.embedded-smart-loading .dispatch-grid { min-height: 520px; height: min(62vh, 700px); }
@@ -764,6 +798,7 @@ onMounted(() => { void load(); void loadCustomers() })
}
@media (max-width: 720px) {
.channel-summary-row { grid-template-columns: minmax(0, 1fr); align-items: stretch; }
.compact-summary { grid-column: 1 / -1; }
.compact-summary { min-height: 0; justify-content: flex-start; flex-wrap: wrap; gap: 6px 14px; padding: 8px 12px; white-space: normal; }
.capacity-grid { grid-template-columns: 1fr; }
.pane-header,.plan-actions,.meter-title { align-items: stretch; flex-direction: column; }