优化
This commit is contained in:
@@ -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('静态演示数据')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const dashboardRoutes = [
|
||||
component: () => import('@/modules/dashboard/pages/DashboardView.vue'),
|
||||
meta: {
|
||||
title: '首页数据看板',
|
||||
description: '平台业务概览(当前展示静态演示数据)',
|
||||
description: '平台实时核心业务指标',
|
||||
showPageHeading: false,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user