@@ -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 : 24 px ; background : var ( -- cc - page , # f4f6f8 ) ; }
. dashboard - content { display : flex ; min - width : 0 ; flex - direction : column ; gap : 18 px ; }
. dashboard - context { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 16 px ; }
. dashboard - context p { min - width : 0 ; margin : 0 ; color : var ( -- cc - muted , # 667085 ) ; font - size : 12 px ; line - height : 1.55 ; }
. dashboard - head - actions { display : flex ; align - items : center ; gap : 14 px ; color : var ( -- cc - muted , # 667085 ) ; font - size : 12 px ; white - space : nowrap ; }
. dashboard - outline - button , . dashboard - tabs button , . dashboard - task - action { display : inline - flex ; min - height : 36 px ; align - items : center ; justify - content : center ; gap : 6 px ; border : 1 px solid var ( -- cc - border - strong , # d8deea ) ; border - radius : var ( -- cc - radius - md , 6 px ) ; background : var ( -- cc - surface , # fff ) ; color : var ( -- cc - text - secondary , # 344054 ) ; cursor : pointer ; transition : border - color .18 s ease , background .18 s ease , color .18 s ease ; }
. dashboard - outline - button { display : inline - flex ; min - height : 36 px ; align - items : center ; justify - content : center ; gap : 6 px ; border : 1 px solid var ( -- cc - border - strong , # d8deea ) ; border - radius : var ( -- cc - radius - md , 6 px ) ; background : var ( -- cc - surface , # fff ) ; color : var ( -- cc - text - secondary , # 344054 ) ; cursor : pointer ; transition : border - color .18 s ease , background .18 s ease , color .18 s ease ; }
. dashboard - outline - button { padding : 0 13 px ; }
. 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 : 31 px ; line - height : 1 ; letter - spacing : - .02 em ; }
. dashboard - metric strong small { color : # 525866 ; font - size : 12 px ; font - weight : 600 ; letter - spacing : 0 ; }
. dashboard - metric > span : last - child { position : relative ; display : block ; margin - top : 17 px ; color : # 8 a91a0 ; font - size : 11 px ; }
. dashboard - grid { display : grid ; min - width : 0 ; gap : 18 px ; }
. dashboard - grid -- primary { grid - template - columns : minmax ( 0 , 1.8 fr ) minmax ( 300 px , .9 fr ) ; }
. dashboard - grid -- equal { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; }
. dashboard - panel { min - width : 0 ; padding : 20 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : var ( -- dashboard - panel ) ; }
. dashboard - panel - head { display : flex ; min - height : 36 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; }
. dashboard - panel - head h2 { display : flex ; align - items : center ; gap : 8 px ; margin : 0 ; font - size : 16 px ; line - height : 1.4 ; }
. dashboard - panel - head > span { color : # 8 a91a0 ; font - size : 11 px ; }
. dashboard - tabs { display : flex ; gap : 7 px ; }
. dashboard - tabs button { min - width : 58 px ; padding : 0 12 px ; }
. 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 : 8 px 18 px ; margin : 12 px 0 ; color : # 667085 ; font - size : 11 px ; }
. dashboard - legend span { display : inline - flex ; align - items : center ; gap : 6 px ; }
. dashboard - legend i { width : 8 px ; height : 8 px ; flex : 0 0 auto ; border - radius : 50 % ; }
. dashboard - legend -- centered { justify - content : center ; margin - top : 18 px ; }
. dashboard - line - chart { width : 100 % ; min - height : 240 px ; }
. dashboard - line - chart svg { display : block ; width : 100 % ; height : auto ; }
. dashboard - chart - grid line { stroke : # e8ebf1 ; stroke - width : 1 ; }
. dashboard - axis - labels { fill : # 8 a91a0 ; font - size : 10 px ; }
. dashboard - donut - wrap { display : grid ; min - height : 268 px ; align - content : center ; justify - items : center ; padding - top : 10 px ; }
. dashboard - donut { position : relative ; display : grid ; width : 166 px ; height : 166 px ; place - items : center ; border - radius : 50 % ; transform : rotate ( - 90 deg ) ; }
. dashboard - donut : : after { position : absolute ; width : 92 px ; height : 92 px ; border - radius : 50 % ; background : # fff ; content : '' ; }
. dashboard - donut > span { z - index : 1 ; display : flex ; align - items : center ; flex - direction : column ; color : # 667085 ; transform : rotate ( 90 deg ) ; }
. dashboard - donut > span svg { color : # 667 eea ; }
. dashboard - donut > span strong { margin - top : 3 px ; color : # 172033 ; font - size : 18 px ; }
. dashboard - donut > span small { font - size : 10 px ; }
. dashboard - donut - wrap -- revenue { min - height : 320 px ; }
. dashboard - donut -- solid { width : 210 px ; height : 210 px ; }
. dashboard - donut -- solid : : after { display : none ; }
. dashboard - bars { display : grid ; gap : 8 px ; margin - top : 16 px ; }
. dashboard - bar - row { display : grid ; grid - template - columns : 104 px minmax ( 80 px , 1 fr ) 32 px ; align - items : center ; gap : 9 px ; color : # 667085 ; font - size : 11 px ; }
. dashboard - bar - row > span { overflow : hidden ; text - align : right ; text - overflow : ellipsis ; white - space : nowrap ; }
. dashboard - bar - row > i { height : 15 px ; overflow : hidden ; border - radius : 4 px ; background : # f0f3f8 ; }
. dashboard - bar - row > i b { display : block ; height : 100 % ; border - radius : inherit ; background : # 3478 f6 ; }
. dashboard - bar - row > strong { color : # 475467 ; font - size : 10 px ; }
. dashboard - task - panel { padding : 0 ; overflow : hidden ; }
. dashboard - task - panel . dashboard - panel - head { padding : 16 px 20 px ; }
. dashboard - table - wrap { overflow - x : auto ; border - top : 1 px solid # e8ebf1 ; }
. dashboard - table - wrap table { width : 100 % ; min - width : 760 px ; border - collapse : collapse ; font - size : 12 px ; }
. dashboard - table - wrap th , . dashboard - table - wrap td { padding : 13 px 16 px ; border - bottom : 1 px 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 : 30 px ; padding : 0 9 px ; font - size : 11 px ; }
. dashboard - task - action : disabled { border - color : # e4e7ec ; background : # f9fafb ; color : # 98 a2b3 ; cursor : not - allowed ; }
. dashboard - priority { display : inline - flex ; min - height : 24 px ; align - items : center ; padding : 2 px 8 px ; border - radius : 999 px ; background : # fff3e8 ; color : # c54d0a ; font - size : 11 px ; font - weight : 700 ; }
. dashboard - priority . urgent { background : # fef3f2 ; color : # b42318 ; }
@ media ( max - width : 1180 px ) {
. dashboard - metrics { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; }
. dashboard - grid -- primary { grid - template - columns : minmax ( 0 , 1 fr ) ; }
. dashboard - donut - wrap { min - height : 240 px ; }
}
@ media ( max - width : 760 px ) {
. dashboard - context { align - items : flex - start ; flex - direction : column ; }
. dashboard - head - actions { align - items : flex - start ; flex - direction : column ; gap : 9 px ; white - space : normal ; }
. dashboard - metrics { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; }
. dashboard - grid -- equal { grid - template - columns : minmax ( 0 , 1 fr ) ; }
. dashboard - panel - head { align - items : flex - start ; flex - direction : column ; }
. dashboard - tabs { width : 100 % ; overflow - x : auto ; }
. dashboard - tabs button { min - height : 44 px ; flex : 1 ; }
}
@ media ( max - width : 420 px ) {
. dashboard - content { gap : 14 px ; }
. dashboard - metrics { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 8 px ; }
. dashboard - metric { min - height : 116 px ; padding : 13 px ; }
. dashboard - metric - mark { top : - 30 px ; right : - 24 px ; width : 80 px ; height : 80 px ; }
@@ -382,11 +151,8 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
. dashboard - metric strong { font - size : 23 px ; }
. dashboard - metric strong small { font - size : 10 px ; }
. dashboard - metric > span : last - child { margin - top : 10 px ; font - size : 10 px ; }
. dashboard - panel { padding : 16 px ; }
. dashboard - task - panel { padding : 0 ; }
. dashboard - bar - row { grid - template - columns : 90 px minmax ( 72 px , 1 fr ) 28 px ; gap : 7 px ; }
}
@ media ( prefers - reduced - motion : reduce ) {
. dashboard - outline - button , . dashboard - tabs button , . dashboard - task - action { transition : none ; }
. dashboard - outline - button { transition : none ; }
}
< / style >