From d8b59ecca5708683fb835c4b3a9e6d3d35ee8633 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 3 Sep 2026 11:47:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/SmartLoadingDialog.test.ts | 2 +- .../components/SmartLoadingDialog.vue | 21 +-- .../pages/GlobalSmartLoadingView.test.ts | 58 ++++---- .../pages/GlobalSmartLoadingView.vue | 100 ++++++-------- .../unified-orders/api/unifiedOrders.test.ts | 22 ++- .../unified-orders/api/unifiedOrders.ts | 23 ++++ .../OrderDeclarationEditorDialog.test.ts | 4 +- .../OrderDeclarationEditorDialog.vue | 10 +- .../pages/UnifiedOrdersView.requests.test.ts | 47 ++++++- .../pages/UnifiedOrdersView.test.ts | 18 ++- .../pages/UnifiedOrdersView.vue | 128 +++++++++++++++++- 11 files changed, 314 insertions(+), 119 deletions(-) diff --git a/src/modules/container-loading/components/SmartLoadingDialog.test.ts b/src/modules/container-loading/components/SmartLoadingDialog.test.ts index 9090f471..5283eedd 100644 --- a/src/modules/container-loading/components/SmartLoadingDialog.test.ts +++ b/src/modules/container-loading/components/SmartLoadingDialog.test.ts @@ -17,7 +17,7 @@ describe('SmartLoadingDialog', () => { }) await flushPromises() - expect(wrapper.findAll('.smart-loading-dialog__warehouses [role="tab"]').map((tab) => tab.text())).toEqual(['义乌仓', '东莞仓']) + expect(wrapper.find('.smart-loading-dialog__warehouses').exists()).toBe(false) const expand = wrapper.get('button[aria-label="展开全屏"]') await expand.trigger('click') expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).not.toBeNull() diff --git a/src/modules/container-loading/components/SmartLoadingDialog.vue b/src/modules/container-loading/components/SmartLoadingDialog.vue index eca851e0..62657aca 100644 --- a/src/modules/container-loading/components/SmartLoadingDialog.vue +++ b/src/modules/container-loading/components/SmartLoadingDialog.vue @@ -3,23 +3,15 @@ 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({ default: false }) const fullscreen = ref(false) -const warehouseCode = ref('') -const workbench = ref | null>(null) watch(open, (value) => { if (!value) { fullscreen.value = false - warehouseCode.value = '' } }) - -function selectWarehouse(code: string) { - void workbench.value?.selectWarehouse(code) -} - + diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index e8230cab..dcec868d 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -1,4 +1,4 @@ -import ElementPlus, { ElInputNumber, ElMessage, ElMessageBox } from 'element-plus' +import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox } from 'element-plus' import { AxiosError } from 'axios' import { createPinia, setActivePinia } from 'pinia' import { flushPromises, mount } from '@vue/test-utils' @@ -48,7 +48,7 @@ vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: const container = { id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1, - billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, + billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船', departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null, destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, @@ -88,7 +88,7 @@ describe('GlobalSmartLoadingView', () => { expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId) expect(wrapper.text()).toContain('候选装柜明细') expect(wrapper.text()).toContain('美森正班') - expect(wrapper.findAll('.channel-tabs [role="tab"]')).toHaveLength(9) + expect(wrapper.findAll('.channel-tags .el-checkbox-button')).toHaveLength(9) expect(wrapper.text()).not.toContain('美森正班海卡包税') expect(wrapper.text()).toContain('另一企业') expect(wrapper.text()).toContain('MSCU-1 装柜方案') @@ -105,7 +105,7 @@ describe('GlobalSmartLoadingView', () => { it('uses the top-level container channel snapshot without expecting a nested snapshot', async () => { vi.mocked(listContainers).mockResolvedValue({ - records: [{ ...container, channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }], + records: [{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }], page: 1, size: 100, total: 1, @@ -167,7 +167,7 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).not.toContain('真实柜号') }) - it('groups containers by channel and reloads candidates when the channel changes', async () => { + it('keeps nine fixed channel tags and allows selecting multiple channels', async () => { const other = { ...container, id: '9007199254740998', @@ -185,17 +185,19 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.findAll('.container-option')).toHaveLength(1) expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).not.toContain('MSCU-2') - expect(wrapper.text()).not.toContain('个柜次') + expect(wrapper.text()).toContain('柜子数量1 个') - const tab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '合德') - expect(tab).toBeDefined() - await tab!.trigger('click') + expect(wrapper.findAll('.channel-tags .el-checkbox-button').map((item) => item.text())).toEqual([ + '美森正班', '美森加班', 'EXX', '合德', '洛杉矶普船', '芝加哥普船', '休斯顿普船', '萨凡纳普船', '纽约普船海卡', + ]) + const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')! + await hedeChannel.find('input').setValue(true) await flushPromises() - expect(wrapper.findAll('.container-option')).toHaveLength(1) + expect(wrapper.findAll('.container-option')).toHaveLength(2) + expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).toContain('MSCU-2') - expect(wrapper.text()).not.toContain('MSCU-1') - expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(other.id, other.warehouseId) + expect(wrapper.text()).toContain('柜子数量2 个') }) it('merges detailed pricing channels into the same confirmed display group', async () => { @@ -219,7 +221,7 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).not.toContain('美森正班海派') }) - it('isolates Yiwu and Dongguan containers when the host changes the warehouse', async () => { + it('moves the shipping warehouse into the query filters', async () => { const dongguanContainer = { ...container, id: '9007199254740998', @@ -235,9 +237,9 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() expect(wrapper.text()).toContain('MSCU-1') - expect(wrapper.text()).not.toContain('MSCU-DG') + expect(wrapper.text()).toContain('MSCU-DG') - await (wrapper.vm as unknown as { selectWarehouse: (code: string) => Promise }).selectWarehouse('DONGGUAN') + wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) await flushPromises() expect(wrapper.text()).toContain('MSCU-DG') @@ -261,10 +263,10 @@ describe('GlobalSmartLoadingView', () => { const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() - await (wrapper.vm as unknown as { selectWarehouse: (code: string) => Promise }).selectWarehouse('DONGGUAN') + wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) await flushPromises() - expect(wrapper.get('.channel-tabs [role="tab"][aria-selected="true"]').text()).toBe('美森正班') + expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班') expect(wrapper.findAll('.container-option')).toHaveLength(0) expect(wrapper.text()).not.toContain('MSCU-DG-MAX') expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(1) @@ -391,7 +393,8 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }> - expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus']) + expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus']) + expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([{ label: '义乌仓', value: '8' }]) expect(source).not.toContain('目的仓') expect(wrapper.text()).not.toContain('税率') @@ -659,20 +662,23 @@ describe('GlobalSmartLoadingView', () => { expect(source).toContain(':page-sizes="[20, 50, 100]"') }) - it('uses overflow-aware channel tabs with a compact right-aligned summary', () => { + it('uses nine fixed multi-select channel tags with a compact right-aligned summary', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') - expect(source).toContain('候选明细') - expect(source).toContain('
柜次
') + expect(source).toContain('
柜子数量
') expect(source).toContain('
已装
') expect(source).toContain('
总重量
') expect(source).toContain('
总体积
') - expect(source).toContain('.channel-tabs :deep(.el-tabs__content) { display: none; }') + expect(source).toContain('.channel-tags :deep(.el-checkbox-button__inner)') + expect(source).toContain('flex-wrap: nowrap; gap: 6px;') + expect(source).toContain('height: 34px; padding: 0 10px;') + expect(source).toContain('font-size: 13px; line-height: 32px;') expect(source).not.toContain('class="summary-band"') expect(source).toContain('class="capacity-grid"') expect(source).toContain('智能推荐') diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 79c1e372..6afec1a8 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -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('warehouseCode', { default: '' }) const access = useAccessPreviewStore() const router = useRouter() @@ -46,7 +43,7 @@ const containers = ref([]) const customers = ref([]) const customersLoading = ref(false) const selectedContainerId = ref('') -const activeChannelCode = ref('') +const selectedChannelCodes = ref([]) const plan = ref(null) const candidates = ref([]) const loading = ref(false) @@ -61,7 +58,7 @@ const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) const planGroupsByInboundId = ref(new Map()) -const query = ref({ customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) +const query = ref({ warehouseId: '', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' }) const appliedQuery = ref({ ...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() + 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(() => [ + { 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(() => [ 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 = } } -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 = {}) { ? 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() })