优化
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import {
|
||||
ArrowUpRight,
|
||||
BarChart3,
|
||||
Clock3,
|
||||
Expand,
|
||||
Info,
|
||||
Minimize,
|
||||
Ship,
|
||||
} from '@lucide/vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
|
||||
type TransportMode = 'sea' | 'air' | 'land'
|
||||
|
||||
@@ -124,130 +124,133 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
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>
|
||||
<AdminPage as="div" spacing="normal">
|
||||
<section class="dashboard-context" aria-label="数据看板说明">
|
||||
<p>当前为原型占位,以下均为静态演示数据,尚未接入业务接口。</p>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<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 }} <b :class="{ 'is-warning': metric.warning }">{{ metric.change }}</b></span>
|
||||
</article>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 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>
|
||||
</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 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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</ListPageLayout>
|
||||
</AdminPage>
|
||||
</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-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 { padding: 0 13px; }
|
||||
.dashboard-outline-button:hover { border-color: #667eea; color: #5366c8; }
|
||||
.dashboard-outline-button:hover { border-color: var(--cc-primary); color: var(--cc-primary); }
|
||||
.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; }
|
||||
@@ -270,8 +273,8 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
.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-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%; }
|
||||
@@ -314,7 +317,7 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
.dashboard-donut-wrap { min-height: 240px; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.dashboard-head { align-items: stretch; flex-direction: column; }
|
||||
.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); }
|
||||
@@ -323,7 +326,7 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
.dashboard-tabs button { min-height: 44px; flex: 1; }
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.dashboard-view { gap: 14px; }
|
||||
.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:last-child { grid-column: 1 / -1; }
|
||||
|
||||
Reference in New Issue
Block a user