This commit is contained in:
2026-09-14 13:48:15 +08:00
parent ba31dd0563
commit 85b3606eca
5 changed files with 32 additions and 284 deletions
@@ -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('静态演示数据')
})
})
+14 -248
View File
@@ -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<HTMLElement | null>(null)
const activeMode = ref<TransportMode>('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<TransportMode, TrendSeries[]> = {
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
<template>
<!--
THESIS: 用一屏可扫描的经营驾驶舱承载平台概览,拒绝把数据拆成互不关联的装饰卡片。
THESIS: 首页只承载已经接通真实业务数据的四项核心指标。
OWN-WORLD: 延续 Admin 冷灰工作面、白色数据面板和柏悦蓝主色;状态色只承担分类与告警。
STORY: 运营人员先识别核心体量,再查看趋势与构成,最后定位待处理事项。
FIRST VIEWPORT: 顶部状态栏与四项核心指标占据首屏上半部,主趋势图和状态环图紧随其后。
STORY: 运营人员进入首页后直接识别当前核心体量。
FIRST VIEWPORT: 顶部状态栏与四项核心指标完整占据首屏。
FORM: 既有后台视觉体系的经营驾驶舱结构,基于用户提供的正式原型直接扩展。
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md
-->
<section ref="dashboardRef" class="dashboard-view">
<AdminPage as="div" spacing="normal">
<section class="dashboard-context" aria-label="数据看板说明">
<p>{{ metricsError ? '部分核心指标加载失败,可刷新重试;下方图表及待办仍为静态演示数据。' : '四项核心指标已接入实时业务数据;下方图表及待办仍为静态演示数据。' }}</p>
<p>{{ metricsError ? '部分核心指标加载失败,可刷新重试。' : '核心指标来自实时业务数据。' }}</p>
<div class="dashboard-head-actions">
<span>数据更新时间:{{ metricsUpdatedAt ? dayjs(metricsUpdatedAt).format('YYYY-MM-DD HH:mm') : '加载中' }}</span>
<button type="button" class="dashboard-outline-button" :disabled="metricsLoading" @click="loadMetrics">
@@ -187,106 +103,14 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
</div>
</section>
<ListPageLayout ariaLabel="平台运营工作台">
<template #metrics>
<div class="dashboard-metrics" aria-label="核心业务指标">
<article v-for="metric in metrics" :key="metric.label" class="dashboard-metric" :class="`is-${metric.tone}`">
<span class="dashboard-metric-mark" aria-hidden="true" />
<p>{{ metric.label }}</p>
<strong>{{ metric.value }} <small>{{ metric.unit }}</small></strong>
<span>{{ metric.note }}</span>
</article>
</div>
</template>
<div class="dashboard-content">
<div class="dashboard-grid dashboard-grid--primary">
<article class="dashboard-panel dashboard-trend-panel">
<div class="dashboard-panel-head">
<h2>近 30 天运单量趋势</h2>
<div class="dashboard-tabs" aria-label="运输方式">
<button v-for="mode in ([['sea', '海运'], ['air', '空运'], ['land', '陆运']] as const)" :key="mode[0]" type="button" :class="{ active: activeMode === mode[0] }" :aria-pressed="activeMode === mode[0]" @click="activeMode = mode[0]">{{ mode[1] }}</button>
</div>
</div>
<div class="dashboard-legend">
<span v-for="series in currentSeries" :key="series.name"><i :style="{ background: series.color }" />{{ series.name }}</span>
</div>
<div class="dashboard-line-chart">
<svg viewBox="0 0 700 228" role="img" :aria-label="`${activeMode === 'sea' ? '海运' : activeMode === 'air' ? '空运' : '陆运'}近 30 天运单量趋势演示图`">
<g class="dashboard-chart-grid">
<line v-for="y in [32, 61, 90, 119, 148, 177, 206]" :key="`y-${y}`" x1="12" :y1="y" x2="688" :y2="y" />
<line v-for="x in [12, 124, 237, 350, 463, 576, 688]" :key="`x-${x}`" :x1="x" y1="32" :x2="x" y2="206" />
</g>
<template v-for="series in currentSeries" :key="series.name">
<polygon :points="areaPoints(series.values)" :fill="series.fill" />
<polyline :points="linePoints(series.values)" fill="none" :stroke="series.color" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</template>
<g class="dashboard-axis-labels">
<text x="12" y="224">1 日</text><text x="124" y="224">5 日</text><text x="237" y="224">10 日</text>
<text x="350" y="224">15 日</text><text x="463" y="224">20 日</text><text x="576" y="224">25 日</text><text x="663" y="224">30 日</text>
</g>
</svg>
<table class="sr-only">
<caption>{{ activeMode === 'sea' ? '海运' : activeMode === 'air' ? '空运' : '陆运' }}近 30 天运单量演示数据</caption>
<thead><tr><th>日期</th><th v-for="series in currentSeries" :key="series.name">{{ series.name }}</th></tr></thead>
<tbody><tr v-for="(_, index) in currentSeries[0]?.values" :key="index"><th>{{ index + 1 }} 日</th><td v-for="series in currentSeries" :key="series.name">{{ series.values[index] }}</td></tr></tbody>
</table>
</div>
</article>
<article class="dashboard-panel">
<div class="dashboard-panel-head"><h2>运单状态分布</h2></div>
<div class="dashboard-donut-wrap">
<div class="dashboard-donut" :style="{ background: donutBackground(statusSegments) }"><span><Ship :size="24" /><strong>100%</strong><small>演示样本</small></span></div>
<div class="dashboard-legend dashboard-legend--centered">
<span v-for="item in statusSegments" :key="item.label"><i :style="{ background: item.color }" />{{ item.label }} {{ item.value }}%</span>
</div>
</div>
</article>
</div>
<div class="dashboard-grid dashboard-grid--equal">
<article class="dashboard-panel">
<div class="dashboard-panel-head"><h2>TOP10 航线货量(TEU)</h2></div>
<div class="dashboard-bars" aria-label="TOP10 航线货量静态演示数据">
<div v-for="route in routes" :key="route[0]" class="dashboard-bar-row">
<span>{{ route[0] }}</span><i><b :style="{ width: `${(route[1] / 900) * 100}%` }" /></i><strong>{{ route[1] }}</strong>
</div>
</div>
</article>
<article class="dashboard-panel">
<div class="dashboard-panel-head"><h2>收入构成</h2></div>
<div class="dashboard-donut-wrap dashboard-donut-wrap--revenue">
<div class="dashboard-donut dashboard-donut--solid" :style="{ background: donutBackground(revenueSegments) }" role="img" aria-label="收入构成静态演示饼图" />
<div class="dashboard-legend dashboard-legend--centered">
<span v-for="item in revenueSegments" :key="item.label"><i :style="{ background: item.color }" />{{ item.label }} {{ item.value }}%</span>
</div>
</div>
</article>
</div>
<article class="dashboard-panel dashboard-task-panel">
<div class="dashboard-panel-head">
<h2><Clock3 :size="18" aria-hidden="true" />待办事项</h2>
<span>功能入口待真实业务模块接入</span>
</div>
<div class="dashboard-table-wrap">
<table>
<thead><tr><th>操作</th><th>类型</th><th>内容</th><th>优先级</th><th>截止时间</th></tr></thead>
<tbody>
<tr v-for="task in tasks" :key="task.content">
<td><button type="button" class="dashboard-task-action" disabled>待接入 <ArrowUpRight :size="14" /></button></td>
<td>{{ task.type }}</td><td>{{ task.content }}</td>
<td><span class="dashboard-priority" :class="{ urgent: task.priority === '紧急' }">{{ task.priority }}</span></td>
<td>{{ task.due }}</td>
</tr>
</tbody>
</table>
</div>
</article>
</div>
</ListPageLayout>
<div class="dashboard-metrics" aria-label="核心业务指标">
<article v-for="metric in metrics" :key="metric.label" class="dashboard-metric" :class="`is-${metric.tone}`">
<span class="dashboard-metric-mark" aria-hidden="true" />
<p>{{ metric.label }}</p>
<strong>{{ metric.value }} <small>{{ metric.unit }}</small></strong>
<span>{{ metric.note }}</span>
</article>
</div>
</AdminPage>
</section>
</template>
@@ -294,11 +118,10 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
<style scoped>
.dashboard-view { --dashboard-panel: var(--cc-surface, #fff); display: block; min-width: 0; color: var(--cc-text, #172033); }
.dashboard-view:fullscreen { overflow: auto; padding: 24px; background: var(--cc-page, #f4f6f8); }
.dashboard-content { display: flex; min-width: 0; flex-direction: column; gap: 18px; }
.dashboard-context { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; }
.dashboard-context p { min-width: 0; margin: 0; color: var(--cc-muted, #667085); font-size: 12px; line-height: 1.55; }
.dashboard-head-actions { display: flex; align-items: center; gap: 14px; color: var(--cc-muted, #667085); font-size: 12px; white-space: nowrap; }
.dashboard-outline-button, .dashboard-tabs button, .dashboard-task-action { display: inline-flex; min-height: 36px; align-items: center; justify-content: center; gap: 6px; border: 1px solid var(--cc-border-strong, #d8deea); border-radius: var(--cc-radius-md, 6px); background: var(--cc-surface, #fff); color: var(--cc-text-secondary, #344054); cursor: pointer; transition: border-color .18s ease, background .18s ease, color .18s ease; }
.dashboard-outline-button { display: inline-flex; min-height: 36px; align-items: center; justify-content: center; gap: 6px; border: 1px solid var(--cc-border-strong, #d8deea); border-radius: var(--cc-radius-md, 6px); background: var(--cc-surface, #fff); color: var(--cc-text-secondary, #344054); cursor: pointer; transition: border-color .18s ease, background .18s ease, color .18s ease; }
.dashboard-outline-button { padding: 0 13px; }
.dashboard-outline-button:hover:not(:disabled) { border-color: var(--cc-primary); color: var(--cc-primary); }
.dashboard-outline-button:disabled { cursor: wait; opacity: .65; }
@@ -312,69 +135,15 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
.dashboard-metric strong { position: relative; display: block; overflow-wrap: anywhere; font-size: 31px; line-height: 1; letter-spacing: -.02em; }
.dashboard-metric strong small { color: #525866; font-size: 12px; font-weight: 600; letter-spacing: 0; }
.dashboard-metric > span:last-child { position: relative; display: block; margin-top: 17px; color: #8a91a0; font-size: 11px; }
.dashboard-grid { display: grid; min-width: 0; gap: 18px; }
.dashboard-grid--primary { grid-template-columns: minmax(0, 1.8fr) minmax(300px, .9fr); }
.dashboard-grid--equal { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dashboard-panel { min-width: 0; padding: 20px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--dashboard-panel); }
.dashboard-panel-head { display: flex; min-height: 36px; align-items: center; justify-content: space-between; gap: 16px; }
.dashboard-panel-head h2 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 16px; line-height: 1.4; }
.dashboard-panel-head > span { color: #8a91a0; font-size: 11px; }
.dashboard-tabs { display: flex; gap: 7px; }
.dashboard-tabs button { min-width: 58px; padding: 0 12px; }
.dashboard-tabs button:hover { border-color: var(--cc-primary-light, #b8c2ef); color: var(--cc-primary); }
.dashboard-tabs button.active { border-color: var(--cc-primary); background: var(--cc-primary); color: #fff; }
.dashboard-legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 12px 0; color: #667085; font-size: 11px; }
.dashboard-legend span { display: inline-flex; align-items: center; gap: 6px; }
.dashboard-legend i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; }
.dashboard-legend--centered { justify-content: center; margin-top: 18px; }
.dashboard-line-chart { width: 100%; min-height: 240px; }
.dashboard-line-chart svg { display: block; width: 100%; height: auto; }
.dashboard-chart-grid line { stroke: #e8ebf1; stroke-width: 1; }
.dashboard-axis-labels { fill: #8a91a0; font-size: 10px; }
.dashboard-donut-wrap { display: grid; min-height: 268px; align-content: center; justify-items: center; padding-top: 10px; }
.dashboard-donut { position: relative; display: grid; width: 166px; height: 166px; place-items: center; border-radius: 50%; transform: rotate(-90deg); }
.dashboard-donut::after { position: absolute; width: 92px; height: 92px; border-radius: 50%; background: #fff; content: ''; }
.dashboard-donut > span { z-index: 1; display: flex; align-items: center; flex-direction: column; color: #667085; transform: rotate(90deg); }
.dashboard-donut > span svg { color: #667eea; }
.dashboard-donut > span strong { margin-top: 3px; color: #172033; font-size: 18px; }
.dashboard-donut > span small { font-size: 10px; }
.dashboard-donut-wrap--revenue { min-height: 320px; }
.dashboard-donut--solid { width: 210px; height: 210px; }
.dashboard-donut--solid::after { display: none; }
.dashboard-bars { display: grid; gap: 8px; margin-top: 16px; }
.dashboard-bar-row { display: grid; grid-template-columns: 104px minmax(80px, 1fr) 32px; align-items: center; gap: 9px; color: #667085; font-size: 11px; }
.dashboard-bar-row > span { overflow: hidden; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
.dashboard-bar-row > i { height: 15px; overflow: hidden; border-radius: 4px; background: #f0f3f8; }
.dashboard-bar-row > i b { display: block; height: 100%; border-radius: inherit; background: #3478f6; }
.dashboard-bar-row > strong { color: #475467; font-size: 10px; }
.dashboard-task-panel { padding: 0; overflow: hidden; }
.dashboard-task-panel .dashboard-panel-head { padding: 16px 20px; }
.dashboard-table-wrap { overflow-x: auto; border-top: 1px solid #e8ebf1; }
.dashboard-table-wrap table { width: 100%; min-width: 760px; border-collapse: collapse; font-size: 12px; }
.dashboard-table-wrap th, .dashboard-table-wrap td { padding: 13px 16px; border-bottom: 1px solid #edf0f4; text-align: left; }
.dashboard-table-wrap th { background: #f8fafc; color: #475467; font-weight: 650; }
.dashboard-table-wrap tbody tr:last-child td { border-bottom: 0; }
.dashboard-task-action { min-height: 30px; padding: 0 9px; font-size: 11px; }
.dashboard-task-action:disabled { border-color: #e4e7ec; background: #f9fafb; color: #98a2b3; cursor: not-allowed; }
.dashboard-priority { display: inline-flex; min-height: 24px; align-items: center; padding: 2px 8px; border-radius: 999px; background: #fff3e8; color: #c54d0a; font-size: 11px; font-weight: 700; }
.dashboard-priority.urgent { background: #fef3f2; color: #b42318; }
@media (max-width: 1180px) {
.dashboard-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.dashboard-grid--primary { grid-template-columns: minmax(0, 1fr); }
.dashboard-donut-wrap { min-height: 240px; }
}
@media (max-width: 760px) {
.dashboard-context { align-items: flex-start; flex-direction: column; }
.dashboard-head-actions { align-items: flex-start; flex-direction: column; gap: 9px; white-space: normal; }
.dashboard-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dashboard-grid--equal { grid-template-columns: minmax(0, 1fr); }
.dashboard-panel-head { align-items: flex-start; flex-direction: column; }
.dashboard-tabs { width: 100%; overflow-x: auto; }
.dashboard-tabs button { min-height: 44px; flex: 1; }
}
@media (max-width: 420px) {
.dashboard-content { gap: 14px; }
.dashboard-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.dashboard-metric { min-height: 116px; padding: 13px; }
.dashboard-metric-mark { top: -30px; right: -24px; width: 80px; height: 80px; }
@@ -382,11 +151,8 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
.dashboard-metric strong { font-size: 23px; }
.dashboard-metric strong small { font-size: 10px; }
.dashboard-metric > span:last-child { margin-top: 10px; font-size: 10px; }
.dashboard-panel { padding: 16px; }
.dashboard-task-panel { padding: 0; }
.dashboard-bar-row { grid-template-columns: 90px minmax(72px, 1fr) 28px; gap: 7px; }
}
@media (prefers-reduced-motion: reduce) {
.dashboard-outline-button, .dashboard-tabs button, .dashboard-task-action { transition: none; }
.dashboard-outline-button { transition: none; }
}
</style>
+1 -1
View File
@@ -7,7 +7,7 @@ export const dashboardRoutes = [
component: () => import('@/modules/dashboard/pages/DashboardView.vue'),
meta: {
title: '首页数据看板',
description: '平台业务概览(当前展示静态演示数据)',
description: '平台实时核心业务指标',
showPageHeading: false,
},
},
@@ -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('<AppDialog v-model="coLoadCreateOpen"'), source.indexOf('<AppDrawer v-model="relationOpen"'))
const dialogSource = source.slice(source.indexOf('v-model="coLoadCreateOpen"'), source.indexOf('<AppDrawer v-model="relationOpen"'))
expect(dialogSource).toContain('同一国内仓、同一渠道')
expect(dialogSource).toContain('目的仓或目的港可以不同')
expect(dialogSource).toContain('资格由服务端按完整入仓片段校验')
@@ -403,9 +403,9 @@ describe('UnifiedOrdersView contract', () => {
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); }')
})
@@ -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; } }
</style>
<style src="../styles/orderDensity.css"></style>