From 85b3606eca887d9ff08e82cf5ae900e5e45cce2f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 14 Sep 2026 13:48:15 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/pages/DashboardView.test.ts | 36 +-- src/modules/dashboard/pages/DashboardView.vue | 262 +----------------- src/modules/dashboard/routes.ts | 2 +- .../pages/UnifiedOrdersView.test.ts | 10 +- .../pages/UnifiedOrdersView.vue | 6 +- 5 files changed, 32 insertions(+), 284 deletions(-) diff --git a/src/modules/dashboard/pages/DashboardView.test.ts b/src/modules/dashboard/pages/DashboardView.test.ts index 83d255f4..d63e88ac 100644 --- a/src/modules/dashboard/pages/DashboardView.test.ts +++ b/src/modules/dashboard/pages/DashboardView.test.ts @@ -17,11 +17,11 @@ vi.mock('../api/dashboard', () => ({ describe('DashboardView', () => { beforeEach(() => vi.clearAllMocks()) - it('renders the four live metrics and keeps unfinished task actions non-operational', async () => { + it('renders only the four live metrics', async () => { const wrapper = mount(DashboardView) await flushPromises() - expect(wrapper.text()).toContain('四项核心指标已接入实时业务数据') + expect(wrapper.text()).toContain('核心指标来自实时业务数据') expect(wrapper.text()).toContain('数据更新时间:2026-09-11 12:00') expect(wrapper.findAll('.dashboard-metric')).toHaveLength(4) expect(wrapper.text()).toContain('12 票') @@ -29,31 +29,11 @@ describe('DashboardView', () => { expect(wrapper.text()).toContain('8 家') expect(wrapper.text()).toContain('7 票') expect(wrapper.text()).toContain('1 柜箱型未知') - const taskActions = wrapper.findAll('.dashboard-task-action') - expect(taskActions).toHaveLength(3) - expect(taskActions.every((button) => button.attributes('disabled') !== undefined)).toBe(true) - }) - - it('switches transport series and exposes the chart values as a data table', async () => { - const wrapper = mount(DashboardView) - const airButton = wrapper.findAll('button').find((button) => button.text() === '空运') - - expect(airButton).toBeDefined() - await airButton!.trigger('click') - - expect(airButton!.attributes('aria-pressed')).toBe('true') - expect(wrapper.text()).toContain('空运普货') - const caption = wrapper.get('.dashboard-line-chart table caption') - expect(caption.text()).toBe('空运近 30 天运单量演示数据') - expect(wrapper.findAll('.dashboard-line-chart tbody tr')).toHaveLength(30) - }) - - it('preserves semantic route names and values in the volume ranking', () => { - const wrapper = mount(DashboardView) - const ranking = wrapper.get('.dashboard-bars') - - expect(ranking.attributes('role')).toBeUndefined() - expect(ranking.text()).toContain('上海 → 洛杉矶') - expect(ranking.text()).toContain('850') + expect(wrapper.text()).not.toContain('近 30 天运单量趋势') + expect(wrapper.text()).not.toContain('运单状态分布') + expect(wrapper.text()).not.toContain('TOP10 航线货量') + expect(wrapper.text()).not.toContain('收入构成') + expect(wrapper.text()).not.toContain('待办事项') + expect(wrapper.text()).not.toContain('静态演示数据') }) }) diff --git a/src/modules/dashboard/pages/DashboardView.vue b/src/modules/dashboard/pages/DashboardView.vue index fef7fab8..12a25631 100644 --- a/src/modules/dashboard/pages/DashboardView.vue +++ b/src/modules/dashboard/pages/DashboardView.vue @@ -2,15 +2,11 @@ import dayjs from 'dayjs' import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { - ArrowUpRight, - Clock3, Expand, Minimize, RefreshCw, - Ship, } from '@lucide/vue' import AdminPage from '@/shared/components/AdminPage.vue' -import ListPageLayout from '@/shared/components/ListPageLayout.vue' import { getActiveCustomerCount, getDashboardOperationalMetrics, @@ -18,17 +14,7 @@ import { type DashboardOperationalMetrics, } from '../api/dashboard' -type TransportMode = 'sea' | 'air' | 'land' - -interface TrendSeries { - name: string - color: string - fill: string - values: number[] -} - const dashboardRef = ref(null) -const activeMode = ref('sea') const isFullscreen = ref(false) const metricsLoading = ref(true) const metricsError = ref(false) @@ -56,76 +42,6 @@ const metrics = computed(() => [ { label: '待清关票数', value: displayNumber(operationalMetrics.value?.pendingCustomsOrderCount), unit: '票', note: '已进入清关且尚未完成', tone: 'rose' }, ]) -const trendSeries: Record = { - sea: [ - { name: '海运 FCL', color: '#3478f6', fill: 'rgba(52, 120, 246, .13)', values: [28, 32, 25, 38, 35, 42, 30, 36, 40, 45, 33, 48, 38, 42, 50, 35, 40, 45, 38, 52, 42, 48, 55, 40, 45, 50, 38, 42, 48, 55] }, - { name: '海运 LCL', color: '#6d3bd1', fill: 'rgba(109, 59, 209, .09)', values: [12, 15, 10, 18, 14, 20, 16, 15, 18, 22, 14, 20, 16, 18, 25, 15, 18, 20, 16, 22, 18, 20, 24, 16, 18, 22, 15, 18, 20, 24] }, - ], - air: [ - { name: '空运普货', color: '#f79009', fill: 'rgba(247, 144, 9, .12)', values: [18, 20, 17, 24, 19, 26, 22, 25, 28, 23, 20, 29, 25, 27, 32, 26, 23, 30, 28, 34, 29, 31, 36, 30, 33, 35, 28, 32, 34, 38] }, - { name: '空运敏感货', color: '#d92d20', fill: 'rgba(217, 45, 32, .08)', values: [6, 8, 7, 9, 8, 12, 10, 9, 11, 13, 9, 12, 10, 14, 15, 11, 10, 13, 12, 16, 13, 15, 17, 12, 14, 16, 13, 15, 17, 18] }, - ], - land: [ - { name: '中欧班列', color: '#079455', fill: 'rgba(7, 148, 85, .11)', values: [12, 14, 13, 18, 16, 20, 17, 19, 22, 20, 18, 23, 21, 24, 27, 22, 20, 25, 23, 28, 25, 27, 30, 26, 28, 31, 25, 29, 32, 34] }, - { name: '跨境卡航', color: '#0891b2', fill: 'rgba(8, 145, 178, .08)', values: [8, 10, 9, 12, 11, 14, 12, 13, 16, 14, 13, 17, 15, 18, 20, 16, 15, 19, 17, 21, 18, 20, 23, 19, 21, 24, 20, 22, 25, 27] }, - ], -} - -const statusSegments = [ - { label: '已确认', value: 16, color: '#3478f6' }, - { label: '已订舱', value: 12, color: '#6d3bd1' }, - { label: '已做柜', value: 9, color: '#f79009' }, - { label: '已清关', value: 11, color: '#55b715' }, - { label: '运输中', value: 23, color: '#168cf0' }, - { label: '已完成', value: 29, color: '#18b7b9' }, -] as const - -const revenueSegments = [ - { label: '海运费', value: 58, color: '#3478f6' }, - { label: '附加费', value: 16, color: '#6d3bd1' }, - { label: '仓储费', value: 10, color: '#55b715' }, - { label: '清关费', value: 8, color: '#f79009' }, - { label: '陆运费', value: 5, color: '#e72b8c' }, - { label: '其他', value: 3, color: '#18b7b9' }, -] as const - -const routes = [ - ['上海 → 洛杉矶', 850], ['宁波 → 长滩', 620], ['深圳 → 汉堡', 480], ['上海 → 纽约', 420], - ['深圳 → 洛杉矶', 380], ['青岛 → 鹿特丹', 350], ['上海 → 汉堡', 310], ['天津 → 长滩', 280], - ['宁波 → 纽约', 250], ['青岛 → 汉堡', 220], -] as const - -const tasks = [ - { type: '订舱确认', content: 'BY20260528003 · 美森快船待确认 S/O', priority: '重要', due: '2026-06-01' }, - { type: '账单到期', content: '3 笔应收账单即将到期,合计 ¥128,500', priority: '重要', due: '2026-06-03' }, - { type: '报关提醒', content: 'BY20260525001 · 截关时间临近,请尽快申报', priority: '紧急', due: '2026-06-01' }, -] as const - -const currentSeries = computed(() => trendSeries[activeMode.value]) - -function linePoints(values: number[]) { - const max = 60 - return values.map((value, index) => { - const x = 12 + (index * 676) / (values.length - 1) - const y = 206 - (value / max) * 174 - return `${x.toFixed(1)},${y.toFixed(1)}` - }).join(' ') -} - -function areaPoints(values: number[]) { - return `12,206 ${linePoints(values)} 688,206` -} - -function donutBackground(items: ReadonlyArray<{ value: number; color: string }>) { - let offset = 0 - const stops = items.map((item) => { - const start = offset - offset += item.value - return `${item.color} ${start}% ${offset}%` - }) - return `conic-gradient(${stops.join(', ')})` -} - async function toggleFullscreen() { if (!document.fullscreenElement) await dashboardRef.value?.requestFullscreen() else await document.exitFullscreen() @@ -162,17 +78,17 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls @@ -294,11 +118,10 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls diff --git a/src/modules/dashboard/routes.ts b/src/modules/dashboard/routes.ts index 882161a8..512ba71a 100644 --- a/src/modules/dashboard/routes.ts +++ b/src/modules/dashboard/routes.ts @@ -7,7 +7,7 @@ export const dashboardRoutes = [ component: () => import('@/modules/dashboard/pages/DashboardView.vue'), meta: { title: '首页数据看板', - description: '平台业务概览(当前展示静态演示数据)', + description: '平台实时核心业务指标', showPageHeading: false, }, }, diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 023acef9..51d51110 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -181,7 +181,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('row.transport?.effectivePortArrivalAtEstimated') expect(source).not.toContain("transport.sailingAt") expect(source).not.toContain("transport.portArrivalAt") - expect(source).toContain('.table-card { overflow: hidden; }') + expect(source).toMatch(/\.table-card \{[^}]*overflow: hidden;/) expect(source).toContain('slotName: \'measurementReviewStatus\'') expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')') expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注'") @@ -225,7 +225,7 @@ describe('UnifiedOrdersView contract', () => { it('states only the domestic warehouse and channel qualification without local destination or split blocking', () => { const createSource = source.slice(source.indexOf('function openCoLoadCreate'), source.indexOf('async function batchApproveOrders')) - const dialogSource = source.slice(source.indexOf(' { expect(source).toContain("updateMeasurementSectionMode('customer', $event)") expect(source).toContain("updateMeasurementSectionMode('actual', $event)") expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)") - expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2) + expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(1) expect(source).not.toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') - expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !measurementStrategyEditable"/g)).toHaveLength(2) + expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !measurementStrategyEditable"/g)).toHaveLength(1) expect(source).toContain('const measurementStrategyEditable = computed') expect(source).toContain('客户已确认且该票已装柜完成后,箱规计算规则将冻结') expect(source).toContain('if (!measurementStrategyEditable.value)') @@ -620,7 +620,7 @@ describe('UnifiedOrdersView contract', () => { it('keeps detail tabs compact on one horizontally scrollable row', () => { expect(source).toContain('.detail-tabs { display: flex; overflow-x: auto; overflow-y: hidden;') - expect(source).toContain('min-width: max-content; min-height: 56px; flex: 0 0 auto;') + expect(source).toContain('min-width: max-content; min-height: 38px; flex: 0 0 auto;') expect(source).not.toContain('grid-template-columns: repeat(6, minmax(124px, 1fr))') expect(source).not.toContain('.detail-tabs { grid-template-columns: repeat(6, 132px); }') }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 914b5ac9..92d72531 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -2872,6 +2872,8 @@ onBeforeUnmount(() => { .table-card :deep(.el-table__body tr:hover > td.el-table__cell) { background: #f3f7fb; } .row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; } .row-actions :deep(.el-button) { margin-left: 0; } +.table-card :deep(.data-table__system-cell .row-actions .el-button) { height: 28px; margin-left: 0; padding: 4px 6px; font-size: 13px; } +.table-card :deep(.data-table__system-cell .row-actions .el-dropdown) { margin-left: 0; } .planning-cell { display: grid; min-width: 0; gap: 4px; } .planning-cell > small { color: var(--cc-muted); font-size: 11px; } .planning-volume-input,.planning-arrival-input { width: 100%; max-width: 100%; } @@ -2992,12 +2994,12 @@ onBeforeUnmount(() => { .notes-list li:last-child { padding-bottom: 0; border-bottom: 0; } .notes-list li > p { margin: 0; color: var(--cc-text); font-size: 13px; } .notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: 12px; } -@media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 960px) { .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { flex-wrap: wrap; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } } @media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } } @media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } } @media (max-width: 720px) { .measurement-section-title { align-items: stretch; flex-direction: column; } .measurement-section-toggle { align-items: flex-start; flex-wrap: wrap; } .measurement-section-toggle > svg { margin-left: auto; } .measurement-section-mode { width: 100%; flex-basis: auto; } } -@media (max-width: 420px) { .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } } +@media (max-width: 420px) { .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } } From afa49bf1f1373e71c59a7034c21821342802fa75 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 14 Sep 2026 14:35:30 +0800 Subject: [PATCH 2/8] fix: remove sealed smart-loading rows by pallet --- .../api/platformContainerLoading.test.ts | 13 +++++++++- .../api/platformContainerLoading.ts | 8 ++++++ .../pages/GlobalSmartLoadingView.test.ts | 25 +++++++++++++++++++ .../pages/GlobalSmartLoadingView.vue | 15 +++++++++-- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index df860ff6..812ad0ff 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path' import { http } from '@/shared/api/http' import { adjustSmartLoadingItem, assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, assignSmartLoadingPallet, getContainer, - listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, + listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, removeSmartLoadingPallet, getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, updateContainerPickupCode, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, @@ -131,6 +131,17 @@ describe('platform container loading API', () => { ) }) + it('removes a sealed pallet through the whole-pallet command', async () => { + vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) + + await removeSmartLoadingPallet('10', '9007199254740998', 6) + + expect(http.delete).toHaveBeenCalledWith( + '/platform/v1/first-leg/smart-loading/plans/10/pallet-assignments/9007199254740998', + { data: { expectedPlanVersion: 6, operationId: expect.stringContaining('remove-pallet-') } }, + ) + }) + it('uses the dedicated provisional assignment command for a pending-review order', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index 19c383ac..c5c163ca 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -395,6 +395,14 @@ export async function removeSmartLoadingItem(planId: string, itemId: string, exp return plan(unwrapApiResponse(response)) } +export async function removeSmartLoadingPallet(planId: string, palletId: string, expectedPlanVersion: number) { + const response = await http.delete, ApiEnvelope>( + `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/pallet-assignments/${encodeURIComponent(palletId)}`, + { data: { expectedPlanVersion, operationId: operationId('remove-pallet') } }, + ) + return plan(unwrapApiResponse(response)) +} + export async function removeSmartLoadingGroup(planId: string, groupId: string, expectedPlanVersion: number) { const response = await http.delete, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/group-assignments/${encodeURIComponent(groupId)}`, diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index aff760e1..ac393792 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -22,6 +22,7 @@ import { removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, + removeSmartLoadingPallet, } from '../api/platformContainerLoading' const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() })) @@ -50,6 +51,7 @@ vi.mock('../api/platformContainerLoading', () => ({ adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), + removeSmartLoadingPallet: vi.fn(), removeAllUnpickedSmartLoadingItems: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) @@ -861,6 +863,29 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.findAll('button').some((button) => button.text().includes('移除未拣'))).toBe(false) }) + it('removes a sealed pallet row through the whole-pallet command', async () => { + const palletItem = { + ...plan.items[0], unitType: 'SEALED_PALLET' as const, palletId: '801', palletNo: 'DP-260914-0021', + indivisible: true, packageCount: 10, pickedPackageCount: 0, minPackageCount: 10, + canAdjust: false, canRemove: true, + } + const palletPlan = { ...plan, items: [palletItem] } + const emptyPlan = { ...palletPlan, version: 3, items: [] } + vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(palletPlan) + vi.mocked(removeSmartLoadingPallet).mockResolvedValue(emptyPlan) + + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + const row = wrapper.findAll('tr').find((candidateRow) => candidateRow.text().includes('DP-260914-0021')) + expect(row).toBeDefined() + + await row!.find('.el-button--danger').trigger('click') + await flushPromises() + + expect(removeSmartLoadingPallet).toHaveBeenCalledWith(plan.id, '801', 2) + expect(removeSmartLoadingItem).not.toHaveBeenCalled() + }) + it('renders final completion capability as read-only with the server reason', async () => { vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 53714eb2..3e05c6a7 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -26,6 +26,7 @@ import { removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, + removeSmartLoadingPallet, } from '../api/platformContainerLoading' import type { SmartLoadingCandidate, @@ -585,10 +586,14 @@ async function remove(source: object) { const item = itemRow(source) if (!plan.value || !item.canRemove) return const row = source as SmartLoadingPlanRow + const sealedPallet = item.unitType === 'SEALED_PALLET' && Boolean(item.palletId) const confirmed = await ElMessageBox.confirm(row.coLoadGroup ? `确认整组移除 ${row.coLoadGroup.groupNo}(${groupMemberLabel(row.coLoadGroup)}、${row.packageCount} 箱)吗?组内所有入仓片段将联动移除。` - : `确认从当前柜次移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, row.coLoadGroup ? '移除同柜不可拆组' : '移除装柜明细', { - type: 'warning', confirmButtonText: row.coLoadGroup ? '移除整组' : '移除', cancelButtonText: '取消', + : sealedPallet + ? `确认从当前柜次整托移除托盘“${item.palletNo || '—'}”(${item.packageCount} 箱)吗?` + : `确认从当前柜次移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, + row.coLoadGroup ? '移除同柜不可拆组' : sealedPallet ? '整托移除' : '移除装柜明细', { + type: 'warning', confirmButtonText: row.coLoadGroup ? '移除整组' : sealedPallet ? '整托移除' : '移除', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { @@ -598,6 +603,12 @@ async function remove(source: object) { ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`) return } + if (sealedPallet && item.palletId) { + acceptPlan(await removeSmartLoadingPallet(plan.value!.id, item.palletId, plan.value!.version)) + await loadCandidateData() + ElMessage.success(`托盘 ${item.palletNo || '—'} 已整托移除`) + return + } let current = plan.value! for (const original of item.sourceItems) { const currentItem = current.items.find((value) => value.id === original.id) From 23d956ba154f5a0178fba60a574e68bcb6e12cf7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 14 Sep 2026 14:44:01 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/UnifiedOrdersView.test.ts | 2 +- .../pages/UnifiedOrdersView.vue | 26 +++++++------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 51d51110..5ca2e046 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -520,7 +520,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('子单 · 第 ${row.relation.splitSequence} 次') expect(source).toContain('title="父子订单关联"') expect(source).toContain('@click="openOrderRelation(row as UnifiedOrderResource)"') - expect(source).toContain("{ key: 'fulfillment', label: '拆分履约', icon: OrderFulfillmentIcon }") + expect(source).toContain("{ key: 'fulfillment', label: '拆分履约', icon: '🧩' }") expect(source).toContain("activeDetailTab === 'fulfillment'") expect(source).toContain('根单、已完成分片与当前余量') }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 92d72531..8b957f70 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -85,15 +85,6 @@ import { } from '@/modules/inbounds' import { listCustomerChargeRules, type CustomerChargeRule } from '@/modules/fba-pricing' import { getCustomerDimensionConfig, type CustomerDimensionConfig, type CustomerDimensionRule } from '@/modules/dimension-config' -import { - OrderBasicInfoIcon, - OrderCargoInfoIcon, - OrderFeeInfoIcon, - OrderFulfillmentIcon, - OrderHistoryIcon, - OrderMeasurementIcon, - OrderOtherIcon, -} from '@/core/navigation/prototypeMenuIcons' type FilterModel = QueryModel & { tenantId: string @@ -334,13 +325,13 @@ const canReleaseInbound = computed(() => Boolean( )) const detailTabs = [ - { key: 'basic', label: '基本信息', icon: OrderBasicInfoIcon }, - { key: 'cargo', label: '货品信息', icon: OrderCargoInfoIcon }, - { key: 'fees', label: '费用信息', icon: OrderFeeInfoIcon }, - { key: 'history', label: '状态历史', icon: OrderHistoryIcon }, - { key: 'measurement', label: '测量数据', icon: OrderMeasurementIcon }, - { key: 'other', label: '其他', icon: OrderOtherIcon }, - { key: 'fulfillment', label: '拆分履约', icon: OrderFulfillmentIcon }, + { key: 'basic', label: '基本信息', icon: '📋' }, + { key: 'cargo', label: '货品信息', icon: '📦' }, + { key: 'fees', label: '费用信息', icon: '💰' }, + { key: 'history', label: '状态历史', icon: '📝' }, + { key: 'measurement', label: '测量数据', icon: '📐' }, + { key: 'other', label: '其他', icon: '🗂️' }, + { key: 'fulfillment', label: '拆分履约', icon: '🧩' }, ] const customerMeasurementColumns = [ @@ -2257,7 +2248,7 @@ onBeforeUnmount(() => {
- +
@@ -2898,6 +2889,7 @@ onBeforeUnmount(() => { .detail-overview { display: flex; flex-wrap: wrap; gap: 6px 24px; margin: 0; padding: 8px 12px; border: 1px solid var(--cc-border); border-top: 0; background: #fff; } .detail-tabs { display: flex; overflow-x: auto; overflow-y: hidden; border: 1px solid var(--cc-border); border-block: 0; background: #fff; scrollbar-width: thin; } .detail-tabs button { position: relative; display: inline-flex; min-width: max-content; min-height: 38px; flex: 0 0 auto; align-items: center; justify-content: center; gap: 6px; padding: 0 16px; border: 0; background: transparent; color: #475467; font-size: 13px; font-weight: 500; cursor: pointer; } +.detail-tab-icon { display: inline-flex; width: 18px; height: 18px; flex: 0 0 18px; align-items: center; justify-content: center; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; font-size: 17px; font-weight: 400; line-height: 1; } .detail-tabs button:last-child { border-right: 0; } .detail-tabs button::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 2px; background: transparent; content: ''; } .detail-tabs button:hover { background: #fafbff; color: #344054; } From b0de9c026f6278d6e89fbe9ebec1998d1c473a0e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 14 Sep 2026 15:33:35 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + .../components/ContainerSimulationCreate.vue | 4 +- .../pages/CustomerDetailView.test.ts | 2 +- .../customers/pages/CustomerDetailView.vue | 1 + .../customers/pages/CustomersView.test.ts | 4 +- src/modules/customers/pages/CustomersView.vue | 20 ++++-- .../DomesticContainerInventoryDetailView.vue | 23 +++--- .../pages/DomesticContainersView.test.ts | 8 ++- .../pages/DomesticContainersView.vue | 17 ++--- .../pages/domesticContainersSummary.css | 70 ++----------------- src/modules/iam/api/platformIam.test.ts | 2 +- src/modules/iam/api/platformIam.ts | 4 +- .../inbounds/components/InboundTaskList.vue | 12 ++-- .../InboundMeasurementDialog.test.ts | 4 +- .../measurement/InboundMeasurementDialog.vue | 10 +-- .../measurement/InboundMeasurementPanel.vue | 5 +- .../measurement/measurementDensity.css | 30 ++++++++ .../review/MeasurementReviewPanel.test.ts | 4 +- .../review/MeasurementReviewPanel.vue | 23 +++--- .../weight/components/WeightOcrPanel.vue | 7 +- .../components/LoadingListDetailDrawer.vue | 42 +++++++---- .../components/LoadingListsView.test.ts | 6 +- .../components/LoadingListsView.vue | 5 +- .../components/PalletLabelPrintDialog.vue | 5 +- .../pages/PreloadPlanDetailView.vue | 13 ++-- .../pages/UnifiedOrdersView.vue | 18 ++--- .../unified-orders/styles/orderDensity.css | 60 +--------------- .../locations/pages/LocationsView.vue | 21 +++--- .../pallets/pages/PalletsView.vue | 27 +++---- src/styles/adminDensity.css | 62 ++++++++++++++++ 30 files changed, 270 insertions(+), 240 deletions(-) create mode 100644 src/modules/inbounds/measurement/measurementDensity.css create mode 100644 src/styles/adminDensity.css diff --git a/AGENTS.md b/AGENTS.md index f3f769a7..4d69de1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ 1. 用“模块快速路由”选择最小业务模块,进入后读取该目录最近的 `AGENTS.md`;按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不先扫描全部 `src/`。 2. 页面、布局或交互任务同时读取 `../../docs/Admin-Client前端原型还原验收规则.md` 和后台原型 `../../docs/悦/货代SAAS平台后台/AGENTS.md`。 + 订单样板和国内仓样式统一还须读取 `../../docs/Admin高密度界面规范.md`;以其已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式,公共样式按已确认菜单范围接入。 3. 标准列表或公共组件任务读取 `src/shared/components/AGENTS.md`;国内/海外 HTTP、Token 或刷新任务读取 `src/shared/api/AGENTS.md`;Finance 任务读取 `src/modules/finance/AGENTS.md`;海外仓全流程任务先读 `../../docs/海外仓业务/README.md`。 4. 平台/客户层级与权限公共语义读取 `../../docs/平台客户层级与权限契约.md`;双区域 API、海外 Token 和跨区事件读取 `../../docs/跨区域认证与服务通信契约.md`。 diff --git a/src/modules/container-loading/components/ContainerSimulationCreate.vue b/src/modules/container-loading/components/ContainerSimulationCreate.vue index 0f67a4f2..235fc793 100644 --- a/src/modules/container-loading/components/ContainerSimulationCreate.vue +++ b/src/modules/container-loading/components/ContainerSimulationCreate.vue @@ -1,4 +1,5 @@ + diff --git a/src/modules/domestic-containers/pages/domesticContainersSummary.css b/src/modules/domestic-containers/pages/domesticContainersSummary.css index b21d4173..82421555 100644 --- a/src/modules/domestic-containers/pages/domesticContainersSummary.css +++ b/src/modules/domestic-containers/pages/domesticContainersSummary.css @@ -1,67 +1,9 @@ .domestic-containers-page .table-panel .inventory-summary { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - align-items: stretch; - gap: 12px; - margin: 0; - padding: 14px 18px; - border: 0; - background: transparent; + display: flex; flex-wrap: wrap; gap: 6px 28px; margin: 0; + padding: 8px 12px; border: 0; border-bottom: 1px solid var(--cc-border); background: #fff; } - -.domestic-containers-page .table-panel .inventory-summary > div, -.domestic-containers-page .table-panel .inventory-summary > div:last-child { - display: block; - min-width: 0; - padding: 12px 14px; - border: 1px solid #e7edf5; - border-radius: 8px; - background: #f8fafc; -} - -.domestic-containers-page .table-panel .inventory-summary span { - display: block; - color: #64748b; - font-size: 12px; - line-height: 1.4; -} - -.domestic-containers-page .table-panel .inventory-summary strong { - display: block; - margin-top: 6px; - color: #1e293b; - font-size: 20px; - line-height: 1.25; -} - -@media (min-width: 721px) { - .domestic-containers-page .table-panel:has(.inventory-summary) { - position: relative; - } - - .domestic-containers-page .table-panel .inventory-summary { - padding-right: 70px; - } - - .domestic-containers-page .inventory-summary + .data-table > .data-table__toolbar { - position: absolute; - z-index: 1; - top: 35px; - right: 18px; - min-height: 34px; - padding: 0; - border-bottom: 0; - } -} - -@media (max-width: 720px) { - .domestic-containers-page .table-panel .inventory-summary { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 360px) { - .domestic-containers-page .table-panel .inventory-summary { - grid-template-columns: 1fr; - } +.domestic-containers-page .table-panel .inventory-summary > div { + display: flex; align-items: baseline; min-width: 0; gap: 8px; padding: 0; border: 0; } +.domestic-containers-page .table-panel .inventory-summary span { color: var(--cc-muted); font-size: 12px; } +.domestic-containers-page .table-panel .inventory-summary strong { font-size: 14px; font-weight: 600; } diff --git a/src/modules/iam/api/platformIam.test.ts b/src/modules/iam/api/platformIam.test.ts index 4c31434a..86774b90 100644 --- a/src/modules/iam/api/platformIam.test.ts +++ b/src/modules/iam/api/platformIam.test.ts @@ -314,7 +314,7 @@ describe('platform IAM roles', () => { it('creates, updates, and changes customer status with the platform tenant contract', async () => { const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE' } const create = { - tenantName: '柏悦物流', legalName: '柏悦物流有限公司', legalRepresentative: '张三', + tenantName: '柏悦物流', customerNickname: '柏', legalName: '柏悦物流有限公司', legalRepresentative: '张三', contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '标准版', contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司', establishedDate: '2020-01-01', registeredCapital: 1000, registeredCapitalCurrency: 'CNY', diff --git a/src/modules/iam/api/platformIam.ts b/src/modules/iam/api/platformIam.ts index 589b449e..e6f4cf31 100644 --- a/src/modules/iam/api/platformIam.ts +++ b/src/modules/iam/api/platformIam.ts @@ -28,6 +28,7 @@ export interface PlatformTenantResource { id: number | string tenantCode: string tenantName: string + customerNickname?: string | null status: PlatformTenantStatus dimensionInitializationStatus?: 'PENDING' | 'SENDING' | 'READY' | 'FAILED' | string legalName: string | null @@ -52,6 +53,7 @@ export interface PlatformTenantResource { export interface CreatePlatformTenantRequest { tenantName: string + customerNickname?: string legalName: string legalRepresentative?: string contactName: string @@ -69,7 +71,7 @@ export interface CreatePlatformTenantRequest { internalRemark: string } -export type UpdatePlatformTenantRequest = CreatePlatformTenantRequest +export type UpdatePlatformTenantRequest = Omit & { customerNickname: string } export interface PlatformTenantOrderCount { tenantId: number | string diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index 651cf77b..884e5421 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -1,4 +1,6 @@