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; } }