优化
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import DashboardView from './DashboardView.vue'
|
||||
|
||||
describe('DashboardView', () => {
|
||||
it('labels placeholder values and keeps task actions non-operational', () => {
|
||||
const wrapper = mount(DashboardView)
|
||||
|
||||
expect(wrapper.text()).toContain('静态演示数据')
|
||||
expect(wrapper.text()).toContain('演示快照:2026-06-02 08:45')
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,342 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import {
|
||||
ArrowUpRight,
|
||||
BarChart3,
|
||||
Clock3,
|
||||
Expand,
|
||||
Info,
|
||||
Minimize,
|
||||
Ship,
|
||||
} from '@lucide/vue'
|
||||
|
||||
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 metrics = [
|
||||
{ label: '今日新运单', value: '128', unit: '票', note: '较昨日', change: '↑ 12.3%', tone: 'blue', warning: false },
|
||||
{ label: '在途柜量', value: '3,842', unit: 'TEU', note: '较上周', change: '↑ 8.5%', tone: 'green', warning: false },
|
||||
{ label: '本月营收', value: '¥856', unit: '万', note: '较上月', change: '↑ 15.2%', tone: 'orange', warning: false },
|
||||
{ label: '活跃客户', value: '368', unit: '家', note: '本周新增', change: '+6 家', tone: 'purple', warning: false },
|
||||
{ label: '待清关票数', value: '47', unit: '票', note: '即将超期', change: '3 票', tone: 'rose', warning: true },
|
||||
] as const
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
function syncFullscreenState() {
|
||||
isFullscreen.value = document.fullscreenElement === dashboardRef.value
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('fullscreenchange', syncFullscreenState))
|
||||
onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFullscreenState))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!--
|
||||
THESIS: 用一屏可扫描的经营驾驶舱承载平台概览,拒绝把数据拆成互不关联的装饰卡片。
|
||||
OWN-WORLD: 延续 Admin 冷灰工作面、白色数据面板和柏悦蓝主色;状态色只承担分类与告警。
|
||||
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">
|
||||
<header class="dashboard-head">
|
||||
<div>
|
||||
<div class="dashboard-title"><BarChart3 :size="21" aria-hidden="true" /><h1>实时数据看板</h1></div>
|
||||
<p><Info :size="14" aria-hidden="true" />当前为原型占位,以下均为静态演示数据,尚未接入业务接口。</p>
|
||||
</div>
|
||||
<div class="dashboard-head-actions">
|
||||
<span>演示快照:2026-06-02 08:45</span>
|
||||
<button type="button" class="dashboard-outline-button" :aria-pressed="isFullscreen" @click="toggleFullscreen">
|
||||
<Minimize v-if="isFullscreen" :size="16" aria-hidden="true" />
|
||||
<Expand v-else :size="16" aria-hidden="true" />
|
||||
{{ isFullscreen ? '退出全屏' : '全屏展示' }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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 }} <b :class="{ 'is-warning': metric.warning }">{{ metric.change }}</b></span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-view { --dashboard-panel: #fff; display: flex; min-width: 0; flex-direction: column; gap: 18px; color: #172033; }
|
||||
.dashboard-view:fullscreen { overflow: auto; padding: 24px; background: #f4f6f8; }
|
||||
.dashboard-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }
|
||||
.dashboard-title { display: flex; align-items: center; gap: 9px; }
|
||||
.dashboard-title > svg { color: #667eea; }
|
||||
.dashboard-title h1 { margin: 0; font-size: 20px; line-height: 1.3; }
|
||||
.dashboard-head p { display: flex; align-items: center; gap: 6px; margin: 7px 0 0; color: #667085; font-size: 12px; line-height: 1.5; }
|
||||
.dashboard-head-actions { display: flex; align-items: center; gap: 14px; color: #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 #d8deea; border-radius: 8px; background: #fff; color: #344054; cursor: pointer; transition: border-color .18s ease, background .18s ease, color .18s ease; }
|
||||
.dashboard-outline-button { padding: 0 13px; }
|
||||
.dashboard-outline-button:hover { border-color: #667eea; color: #5366c8; }
|
||||
.dashboard-metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px; }
|
||||
.dashboard-metric { position: relative; min-width: 0; overflow: hidden; padding: 20px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--dashboard-panel); }
|
||||
.dashboard-metric-mark { position: absolute; top: -38px; right: -30px; width: 110px; height: 110px; border-radius: 50%; background: #edf1ff; }
|
||||
.dashboard-metric.is-green .dashboard-metric-mark { background: #e8f7ee; }
|
||||
.dashboard-metric.is-orange .dashboard-metric-mark { background: #fff3e3; }
|
||||
.dashboard-metric.is-purple .dashboard-metric-mark { background: #f1eaff; }
|
||||
.dashboard-metric.is-rose .dashboard-metric-mark { background: #fdeaf1; }
|
||||
.dashboard-metric p { position: relative; margin: 0 0 25px; color: #5f6573; font-size: 13px; font-weight: 700; }
|
||||
.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-metric > span b { margin-left: 4px; color: #ef4444; font-weight: 700; }
|
||||
.dashboard-metric > span b.is-warning { color: #d92d20; }
|
||||
.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: #b8c2ef; color: #5366c8; }
|
||||
.dashboard-tabs button.active { border-color: #667eea; background: #667eea; 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-head { align-items: stretch; 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-view { gap: 14px; }
|
||||
.dashboard-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.dashboard-metric { min-height: 116px; padding: 13px; }
|
||||
.dashboard-metric:last-child { grid-column: 1 / -1; }
|
||||
.dashboard-metric-mark { top: -30px; right: -24px; width: 80px; height: 80px; }
|
||||
.dashboard-metric p { margin-bottom: 12px; font-size: 11px; }
|
||||
.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; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const dashboardRoutes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('@/modules/dashboard/pages/DashboardView.vue'),
|
||||
meta: {
|
||||
title: '首页数据看板',
|
||||
description: '平台业务概览(当前展示静态演示数据)',
|
||||
showPageHeading: false,
|
||||
},
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
Reference in New Issue
Block a user