Release/test 合并上生产 #2
@@ -0,0 +1,30 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import SmartLoadingDialog from './SmartLoadingDialog.vue'
|
||||
|
||||
describe('SmartLoadingDialog', () => {
|
||||
afterEach(() => { document.body.innerHTML = '' })
|
||||
|
||||
it('expands to the viewport and can return to the normal dialog size', async () => {
|
||||
const wrapper = mount(SmartLoadingDialog, {
|
||||
attachTo: document.body,
|
||||
props: { modelValue: true },
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
stubs: { GlobalSmartLoadingView: { template: '<div>智能装柜工作台</div>' } },
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.smart-loading-dialog__warehouses [role="tab"]').map((tab) => tab.text())).toEqual(['义乌仓', '东莞仓'])
|
||||
const expand = wrapper.get('button[aria-label="展开全屏"]')
|
||||
await expand.trigger('click')
|
||||
expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).not.toBeNull()
|
||||
expect(wrapper.find('button[aria-label="退出全屏"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('button[aria-label="退出全屏"]').trigger('click')
|
||||
expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).toBeNull()
|
||||
expect(wrapper.find('button[aria-label="展开全屏"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { Maximize2, Minimize2 } from '@lucide/vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import GlobalSmartLoadingView from '../pages/GlobalSmartLoadingView.vue'
|
||||
import { SMART_LOADING_WAREHOUSE_GROUPS } from '../smartLoadingChannelGroups'
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const fullscreen = ref(false)
|
||||
const warehouseCode = ref('')
|
||||
const workbench = ref<InstanceType<typeof GlobalSmartLoadingView> | null>(null)
|
||||
|
||||
watch(open, (value) => {
|
||||
if (!value) {
|
||||
fullscreen.value = false
|
||||
warehouseCode.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
function selectWarehouse(code: string) {
|
||||
void workbench.value?.selectWarehouse(code)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -11,18 +28,44 @@ const open = defineModel<boolean>({ default: false })
|
||||
class="smart-loading-dialog"
|
||||
title="智能装柜"
|
||||
width="min(1600px, 98vw)"
|
||||
:fullscreen="fullscreen"
|
||||
cancel-text="关闭"
|
||||
:show-confirm="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<GlobalSmartLoadingView embedded />
|
||||
<template #header>
|
||||
<div class="smart-loading-dialog__header">
|
||||
<div class="smart-loading-dialog__heading">
|
||||
<span>智能装柜</span>
|
||||
<el-tabs :model-value="warehouseCode" class="smart-loading-dialog__warehouses" aria-label="装柜仓库" @tab-change="selectWarehouse(String($event))">
|
||||
<el-tab-pane v-for="warehouse in SMART_LOADING_WAREHOUSE_GROUPS" :key="warehouse.code" :label="warehouse.name" :name="warehouse.code" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tooltip :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
|
||||
<el-button text :aria-label="fullscreen ? '退出全屏' : '展开全屏'" @click="fullscreen = !fullscreen">
|
||||
<Minimize2 v-if="fullscreen" :size="18" />
|
||||
<Maximize2 v-else :size="18" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<GlobalSmartLoadingView ref="workbench" v-model:warehouse-code="warehouseCode" embedded />
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: min(1600px, 98vw); max-height: 94vh; margin-block: 3vh !important; }
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog.is-fullscreen) { width: 100%; height: 100vh; max-height: 100vh; margin: 0 !important; border-radius: 0; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 16px 16px; }
|
||||
:global(.smart-loading-dialog .app-dialog__body) { min-width: 0; }
|
||||
.smart-loading-dialog__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; color: var(--cc-text); font-size: 18px; font-weight: 600; line-height: 24px; }
|
||||
.smart-loading-dialog__heading { display: flex; min-width: 0; align-items: center; gap: 24px; }
|
||||
.smart-loading-dialog__heading > span { flex: 0 0 auto; }
|
||||
.smart-loading-dialog__warehouses { min-width: 0; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__header) { margin: 0; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__nav-wrap::after), .smart-loading-dialog__warehouses :deep(.el-tabs__content) { display: none; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__item) { height: 38px; padding-inline: 16px; font-size: 14px; font-weight: 600; }
|
||||
.smart-loading-dialog__header :deep(.el-button) { flex: 0 0 auto; margin-right: 24px; color: var(--cc-muted); }
|
||||
@media (max-width: 720px) {
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: calc(100vw - 12px); max-height: calc(100svh - 12px); margin-block: 6px !important; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 8px 8px; }
|
||||
|
||||
@@ -87,7 +87,9 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
|
||||
expect(wrapper.text()).toContain('候选装柜明细')
|
||||
expect(wrapper.text()).toContain('美森正班海卡包税')
|
||||
expect(wrapper.text()).toContain('美森正班')
|
||||
expect(wrapper.findAll('.channel-tabs [role="tab"]')).toHaveLength(9)
|
||||
expect(wrapper.text()).not.toContain('美森正班海卡包税')
|
||||
expect(wrapper.text()).toContain('另一企业')
|
||||
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
|
||||
expect(wrapper.text()).not.toContain('LP-99')
|
||||
@@ -113,7 +115,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('纽约EXX海铁')
|
||||
expect(wrapper.text()).toContain('EXX')
|
||||
expect(wrapper.text()).not.toContain('纽约EXX海铁')
|
||||
expect(wrapper.text()).not.toContain('缺少渠道快照')
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
|
||||
})
|
||||
@@ -157,7 +160,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(option.text()).toContain('柜次号:待生成')
|
||||
expect(option.text()).toContain('集装箱号:待补录')
|
||||
expect(option.text()).not.toContain('开始装柜前录入集装箱号')
|
||||
expect(option.text()).toContain('渠道:美森正班海卡包税')
|
||||
expect(option.text()).toContain('渠道:美森正班')
|
||||
expect(option.text()).toContain('仓库:义乌国内仓')
|
||||
expect(option.text()).toContain('柜型:40HC')
|
||||
expect(option.text()).not.toContain('2088475410134773762')
|
||||
@@ -184,7 +187,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('MSCU-2')
|
||||
expect(wrapper.text()).not.toContain('个柜次')
|
||||
|
||||
const tab = wrapper.findAll('[role="tab"]').find((item) => item.text().includes('合德海卡包税'))
|
||||
const tab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '合德')
|
||||
expect(tab).toBeDefined()
|
||||
await tab!.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -195,6 +198,53 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(other.id, other.warehouseId)
|
||||
})
|
||||
|
||||
it('merges detailed pricing channels into the same confirmed display group', async () => {
|
||||
const parcelContainer = {
|
||||
...container,
|
||||
id: '9007199254740998',
|
||||
planId: '9007199254740999',
|
||||
cabinetNo: 'CB-2',
|
||||
containerNo: 'MSCU-2',
|
||||
channelCode: 'MATSON_CLX_PARCEL',
|
||||
channelName: '美森正班海派',
|
||||
}
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container, parcelContainer], page: 1, size: 100, total: 2, pages: 1 })
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(2)
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).toContain('MSCU-2')
|
||||
expect(wrapper.text()).not.toContain('美森正班海卡包税')
|
||||
expect(wrapper.text()).not.toContain('美森正班海派')
|
||||
})
|
||||
|
||||
it('isolates Yiwu and Dongguan containers when the host changes the warehouse', async () => {
|
||||
const dongguanContainer = {
|
||||
...container,
|
||||
id: '9007199254740998',
|
||||
planId: '9007199254740999',
|
||||
cabinetNo: 'CB-DG',
|
||||
containerNo: 'MSCU-DG',
|
||||
warehouseId: '18',
|
||||
warehouseName: '东莞仓',
|
||||
}
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanContainer], page: 1, size: 100, total: 2, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).not.toContain('MSCU-DG')
|
||||
|
||||
await (wrapper.vm as unknown as { selectWarehouse: (code: string) => Promise<void> }).selectWarehouse('DONGGUAN')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('MSCU-DG')
|
||||
expect(wrapper.text()).not.toContain('MSCU-1')
|
||||
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(dongguanContainer.id, dongguanContainer.warehouseId)
|
||||
})
|
||||
|
||||
it('filters candidates by an active name-only customer selector and accountId', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
SMART_LOADING_CHANNEL_GROUPS,
|
||||
SMART_LOADING_WAREHOUSE_GROUPS,
|
||||
resolveSmartLoadingChannelGroup,
|
||||
resolveSmartLoadingWarehouseGroup,
|
||||
} from './smartLoadingChannelGroups'
|
||||
|
||||
describe('smartLoadingChannelGroups', () => {
|
||||
it('exposes the nine confirmed display groups in a fixed order', () => {
|
||||
expect(SMART_LOADING_CHANNEL_GROUPS.map((group) => group.name)).toEqual([
|
||||
'美森正班', '美森加班', 'EXX', '合德', '洛杉矶普船', '芝加哥普船', '休斯顿普船', '萨凡纳普船', '纽约普船海卡',
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['MATSON_CLX_TRUCK', '美森正班海卡', '美森正班'],
|
||||
['MATSON_CLX_PARCEL', '美森正班海派', '美森正班'],
|
||||
['BYHP', '美森加班海派', '美森加班'],
|
||||
['EXX_RAIL_NY', '纽约EXX海铁', 'EXX'],
|
||||
['HDHK', '合德海卡包税', '合德'],
|
||||
['OA_LA_FULL_TRUCK', '洛杉矶普船全卡', '洛杉矶普船'],
|
||||
['OA_PARCEL', '普船海派', '洛杉矶普船'],
|
||||
['CPHT', '芝加哥普船海铁', '芝加哥普船'],
|
||||
['HPHK', '休斯顿普船海卡', '休斯顿普船'],
|
||||
['SPHK', '萨凡纳普船海卡', '萨凡纳普船'],
|
||||
['NPHK', '纽约普船海卡', '纽约普船海卡'],
|
||||
])('maps %s / %s into %s', (channelCode, channelName, expected) => {
|
||||
expect(resolveSmartLoadingChannelGroup(channelCode, channelName)?.name).toBe(expected)
|
||||
})
|
||||
|
||||
it('does not leak an unconfirmed channel into the fixed display groups', () => {
|
||||
expect(resolveSmartLoadingChannelGroup('UNKNOWN', '未确认新渠道')).toBeNull()
|
||||
})
|
||||
|
||||
it('exposes Yiwu and Dongguan as the fixed top-level warehouses', () => {
|
||||
expect(SMART_LOADING_WAREHOUSE_GROUPS.map((group) => group.name)).toEqual(['义乌仓', '东莞仓'])
|
||||
expect(resolveSmartLoadingWarehouseGroup('义乌国内仓')?.code).toBe('YIWU')
|
||||
expect(resolveSmartLoadingWarehouseGroup('东莞仓')?.code).toBe('DONGGUAN')
|
||||
expect(resolveSmartLoadingWarehouseGroup('其他仓')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
export type SmartLoadingChannelGroup = {
|
||||
code: string
|
||||
name: string
|
||||
channelCodes: readonly string[]
|
||||
}
|
||||
|
||||
export type SmartLoadingWarehouseGroup = {
|
||||
code: string
|
||||
name: string
|
||||
nameKeyword: string
|
||||
}
|
||||
|
||||
export const SMART_LOADING_WAREHOUSE_GROUPS: readonly SmartLoadingWarehouseGroup[] = [
|
||||
{ code: 'YIWU', name: '义乌仓', nameKeyword: '义乌' },
|
||||
{ code: 'DONGGUAN', name: '东莞仓', nameKeyword: '东莞' },
|
||||
] as const
|
||||
|
||||
export const SMART_LOADING_CHANNEL_GROUPS: readonly SmartLoadingChannelGroup[] = [
|
||||
{
|
||||
code: 'MATSON_CLX',
|
||||
name: '美森正班',
|
||||
channelCodes: ['MATSON_CLX_TRUCK', 'MATSON_CLX_PARCEL', 'ZBYHK', 'ZBYHWK', 'ZBYHP'],
|
||||
},
|
||||
{
|
||||
code: 'MATSON_MAX',
|
||||
name: '美森加班',
|
||||
channelCodes: ['MATSON_MAX_TRUCK', 'MATSON_MAX_PARCEL', 'BYHK', 'BYHWK', 'BYHP'],
|
||||
},
|
||||
{
|
||||
code: 'EXX',
|
||||
name: 'EXX',
|
||||
channelCodes: ['EXX_TRUCK', 'EXX_RAIL_NY', 'EXXHK', 'EXXHWK', 'NYEXX'],
|
||||
},
|
||||
{
|
||||
code: 'HEDE',
|
||||
name: '合德',
|
||||
channelCodes: ['HEDE_TRUCK', 'HEDE_PARCEL', 'HDHK', 'HDHWK', 'HDHP'],
|
||||
},
|
||||
{
|
||||
code: 'OA_LA',
|
||||
name: '洛杉矶普船',
|
||||
channelCodes: ['OA_LA_TRUCK', 'OA_LA_FULL_TRUCK', 'OA_PARCEL', 'PCHK', 'PCHWK', 'PCKK', 'PCHP'],
|
||||
},
|
||||
{
|
||||
code: 'OA_CHICAGO',
|
||||
name: '芝加哥普船',
|
||||
channelCodes: ['OA_CHICAGO_RAIL', 'CPHT'],
|
||||
},
|
||||
{
|
||||
code: 'OA_HOUSTON',
|
||||
name: '休斯顿普船',
|
||||
channelCodes: ['OA_HOUSTON_TRUCK', 'HPHK'],
|
||||
},
|
||||
{
|
||||
code: 'OA_SAVANNAH',
|
||||
name: '萨凡纳普船',
|
||||
channelCodes: ['OA_SAVANNAH_TRUCK', 'SPHK'],
|
||||
},
|
||||
{
|
||||
code: 'OA_NEW_YORK_TRUCK',
|
||||
name: '纽约普船海卡',
|
||||
channelCodes: ['OA_NEW_YORK_TRUCK', 'NPHK', 'NPHWK'],
|
||||
},
|
||||
] as const
|
||||
|
||||
export function resolveSmartLoadingChannelGroup(channelCode?: string | null, channelName?: string | null) {
|
||||
const normalizedCode = String(channelCode || '').trim().toUpperCase()
|
||||
if (normalizedCode) {
|
||||
const codeMatch = SMART_LOADING_CHANNEL_GROUPS.find((group) => group.channelCodes.includes(normalizedCode))
|
||||
if (codeMatch) return codeMatch
|
||||
}
|
||||
|
||||
const normalizedName = String(channelName || '').replace(/\s+/g, '')
|
||||
if (!normalizedName) return null
|
||||
if (normalizedName.includes('美森正班')) return SMART_LOADING_CHANNEL_GROUPS[0]
|
||||
if (normalizedName.includes('美森加班')) return SMART_LOADING_CHANNEL_GROUPS[1]
|
||||
if (normalizedName.toUpperCase().includes('EXX')) return SMART_LOADING_CHANNEL_GROUPS[2]
|
||||
if (normalizedName.includes('合德')) return SMART_LOADING_CHANNEL_GROUPS[3]
|
||||
if (normalizedName.includes('洛杉矶普船') || normalizedName === '普船海派') return SMART_LOADING_CHANNEL_GROUPS[4]
|
||||
if (normalizedName.includes('芝加哥普船')) return SMART_LOADING_CHANNEL_GROUPS[5]
|
||||
if (normalizedName.includes('休斯顿普船')) return SMART_LOADING_CHANNEL_GROUPS[6]
|
||||
if (normalizedName.includes('萨凡纳普船')) return SMART_LOADING_CHANNEL_GROUPS[7]
|
||||
if (normalizedName.includes('纽约普船')) return SMART_LOADING_CHANNEL_GROUPS[8]
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveSmartLoadingWarehouseGroup(warehouseName?: string | null) {
|
||||
const normalizedName = String(warehouseName || '').replace(/\s+/g, '')
|
||||
if (!normalizedName) return null
|
||||
return SMART_LOADING_WAREHOUSE_GROUPS.find((group) => normalizedName.includes(group.nameKeyword)) || null
|
||||
}
|
||||
Reference in New Issue
Block a user