From a70cf6addd047d38de390735d7507bb74ff0500f Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Tue, 14 Jul 2026 16:10:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Dashboard=20=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E4=B8=8E=E5=9B=BE=E8=A1=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DashboardView 调整看板卡片与栅格展示,ECharts 补充图表类型,主布局 dashboard 区域由内嵌滚动改为自适应可滚动以避免内容被裁剪,回归脚本适配。 --- frontend/scripts/regression-dashboard.mjs | 22 +- frontend/src/layouts/MainLayout.vue | 6 +- frontend/src/plugins/echarts.ts | 3 +- .../src/views/dashboard/DashboardView.vue | 276 ++++++++++++++++-- 4 files changed, 270 insertions(+), 37 deletions(-) diff --git a/frontend/scripts/regression-dashboard.mjs b/frontend/scripts/regression-dashboard.mjs index 1c43f08..759be26 100644 --- a/frontend/scripts/regression-dashboard.mjs +++ b/frontend/scripts/regression-dashboard.mjs @@ -19,6 +19,8 @@ assert.match( assert.match(echartsSource, /import\s*\{[^}]*BarChart[^}]*\}\s*from\s*['"]echarts\/charts['"]/, 'ECharts 未注册 BarChart') assert.match(echartsSource, /use\(\[[\s\S]*?BarChart[\s\S]*?\]\)/, 'BarChart 未加入 ECharts 按需注册列表') +assert.match(echartsSource, /import\s*\{[^}]*PieChart[^}]*\}\s*from\s*['"]echarts\/charts['"]/, 'ECharts 未注册 PieChart') +assert.match(echartsSource, /use\(\[[\s\S]*?PieChart[\s\S]*?\]\)/, 'PieChart 未加入 ECharts 按需注册列表') for (const copy of [ '平台运行状态', @@ -39,16 +41,24 @@ assert.match(dashboardSource, /router\.push\(['"]\/fine-tune['"]\)/, '查看全 assert.match(dashboardSource, /router\.push\(`\/training-log\/\$\{task\.id\}`\)/, '训练任务详情应进入训练日志页') assert.doesNotMatch(dashboardSource, /class=["']dashboard-heading["']/, '服务看板不应重复展示页面标题栏') assert.doesNotMatch(dashboardSource, /查看告警/, '服务看板不应保留冗余的顶部告警按钮') -assert.match(dashboardSource, /\.dashboard-view\s*\{[\s\S]*?gap:\s*10px;/, '服务看板应使用紧凑的区块间距') -assert.match(dashboardSource, /\.dashboard-middle\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;/, '中间区域应弹性占满剩余高度') +assert.match(dashboardSource, /\.dashboard-view\s*\{[\s\S]*?gap:\s*16px;/, '服务看板区块间距应保持舒展') +assert.match(dashboardSource, /\.dashboard-view\s*\{[\s\S]*?min-height:\s*100%;/, '服务看板应至少填满页面可用高度') +assert.match(dashboardSource, /\.dashboard-middle\s*\{[\s\S]*?flex:\s*0 0 auto;[\s\S]*?min-height:\s*0;/, '中间区域不应因新增统计卡片被压缩') assert.match(dashboardSource, /grid-template-columns:\s*minmax\(0,\s*1\.9fr\)\s*minmax\(300px,\s*0\.82fr\);/, '服务状态列应收窄,为训练图表释放更多宽度') -assert.match(dashboardSource, /\.training-chart\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?height:\s*auto;[\s\S]*?min-height:\s*230px;/, '训练统计图应随可用空间自动拉伸') -assert.match(dashboardSource, /@media\s*\(max-height:\s*900px\)[\s\S]*?\.training-chart\s*\{[\s\S]*?min-height:\s*200px;/, '较矮桌面视口应保留图表最小可读高度') +assert.match(dashboardSource, /\.training-chart\s*\{[\s\S]*?height:\s*300px;[\s\S]*?min-height:\s*300px;/, '训练统计图应保持舒展、稳定的展示高度') assert.match(dashboardSource, /\.service-table\s*\{[\s\S]*?grid-template-rows:[^;]*repeat\(4,\s*minmax\(48px,\s*1fr\)\)/, '服务状态行应随中间区域同步拉伸') assert.match(dashboardSource, /\.tasks-heading\s*\{[\s\S]*?min-height:\s*32px;[\s\S]*?padding:\s*0 14px 10px;/, '训练任务标题区应适当加高') assert.match(dashboardSource, /\.tasks-table\s*\{[\s\S]*?th,\s*td\s*\{[\s\S]*?height:\s*42px;[\s\S]*?th\s*\{[\s\S]*?height:\s*36px;/, '训练任务表格行应适当加高') assert.match(dashboardSource, /@media\s*\(max-height:\s*900px\)[\s\S]*?\.tasks-heading\s*\{[\s\S]*?min-height:\s*28px;[\s\S]*?padding:\s*0 12px 8px;[\s\S]*?\.tasks-table\s*\{[\s\S]*?height:\s*36px;[\s\S]*?th\s*\{[\s\S]*?height:\s*32px;/, '低高度桌面视口也应提高训练任务占比') -assert.match(mainLayoutSource, /\.layout-content:has\(\.dashboard-view\)[\s\S]*?overflow-y:\s*hidden;/, '服务看板容器应避免产生页面纵向滚动条') -assert.match(mainLayoutSource, /\.page-canvas\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?overflow:\s*hidden;/, '服务看板页面画布应约束在可用视口内') +assert.match(dashboardSource, /class=["']user-stats-row["'][\s\S]*?用户操作分布[\s\S]*?登录时长排行[\s\S]*?最近登录用户/, '服务看板应展示三块用户统计卡片') +assert.match(dashboardSource, /class=["']duration-chart["'][\s\S]*?loginDurationChartOption/, '登录时长应使用 ECharts 图表展示') +assert.match(dashboardSource, /const loginDurationChartOption[\s\S]*?type:\s*['"]bar['"][\s\S]*?formatter:\s*['"]\{c\} 小时['"]/, '登录时长应以横向柱状图展示具体小时数') +assert.match(dashboardSource, /const operationChartOption[\s\S]*?position:\s*['"]outside['"][\s\S]*?labelLine:\s*\{[\s\S]*?show:\s*true/, '饼图应以外侧引导线标注操作名称') +assert.match(dashboardSource, /const loginDurationChartOption[\s\S]*?grid:\s*\{\s*top:\s*8,\s*right:\s*12,\s*bottom:\s*6,\s*left:\s*8,[\s\S]*?max:\s*Math\.ceil\(Math\.max[\s\S]*?position:\s*['"]insideRight['"]/, '登录时长图应收紧左右边距、按数据范围拉伸,并将数值置于柱内') +assert.match(dashboardSource, /\.user-stats-row\s*\{[\s\S]*?repeat\(3,\s*minmax\(0,\s*1fr\)\)/, '宽屏用户统计卡片应保持三列') +assert.match(dashboardSource, /@media\s*\(max-width:\s*1180px\)[\s\S]*?\.user-stats-row\s*\{[\s\S]*?repeat\(2,\s*minmax\(0,\s*1fr\)\)/, '中等宽度应将用户统计卡片降为两列') +assert.match(dashboardSource, /@media\s*\(max-width:\s*720px\)[\s\S]*?\.user-stats-row\s*\{[\s\S]*?grid-template-columns:\s*1fr;/, '窄屏应将用户统计卡片降为单列') +assert.match(mainLayoutSource, /\.layout-content:has\(\.dashboard-view\)[\s\S]*?overflow-y:\s*auto;/, '服务看板应允许在内容超出视口时纵向滚动') +assert.match(mainLayoutSource, /\.page-canvas\s*\{[\s\S]*?flex:\s*1 0 auto;[\s\S]*?overflow:\s*visible;/, '服务看板画布应允许新增卡片完整显示') console.log('服务看板回归检查通过') diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index b6051a2..0a67cda 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -84,13 +84,13 @@ onUnmounted(() => { @media (min-width: 1181px) { .layout-content:has(.dashboard-view) { - overflow-y: hidden; + overflow-y: auto; .page-canvas { - flex: 1 1 auto; + flex: 1 0 auto; min-height: 0; padding: 16px; - overflow: hidden; + overflow: visible; } } } diff --git a/frontend/src/plugins/echarts.ts b/frontend/src/plugins/echarts.ts index 7292175..827115c 100644 --- a/frontend/src/plugins/echarts.ts +++ b/frontend/src/plugins/echarts.ts @@ -4,7 +4,7 @@ */ import { use } from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' -import { BarChart, LineChart } from 'echarts/charts' +import { BarChart, LineChart, PieChart } from 'echarts/charts' import { GridComponent, TooltipComponent, @@ -17,6 +17,7 @@ use([ CanvasRenderer, BarChart, LineChart, + PieChart, GridComponent, TooltipComponent, LegendComponent, diff --git a/frontend/src/views/dashboard/DashboardView.vue b/frontend/src/views/dashboard/DashboardView.vue index bd40f1f..2fe514f 100644 --- a/frontend/src/views/dashboard/DashboardView.vue +++ b/frontend/src/views/dashboard/DashboardView.vue @@ -25,6 +25,19 @@ interface DashboardTask { startedAt: string } +interface LoginDurationStat { + id: number + username: string + duration: number +} + +interface RecentLoginUser { + id: number + username: string + role: string + lastLogin: string +} + const router = useRouter() const period = ref('7d') @@ -185,6 +198,110 @@ const chartOption = computed(() => ({ ], })) +const operationChartOption = computed(() => ({ + animationDuration: 500, + tooltip: { trigger: 'item' }, + color: ['#4f46e5', '#10b981', '#f59e0b', '#3b82f6', '#ec4899'], + series: [ + { + name: '操作分类', + type: 'pie', + radius: ['40%', '64%'], + center: ['50%', '50%'], + avoidLabelOverlap: true, + itemStyle: { + borderRadius: 6, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: true, + position: 'outside', + formatter: '{b}', + color: '#475569', + fontSize: 11, + lineHeight: 16, + width: 70, + overflow: 'truncate', + }, + emphasis: { + label: { show: true, fontSize: 12, fontWeight: 'bold', color: '#1e293b' } + }, + labelLine: { + show: true, + length: 10, + length2: 8, + lineStyle: { color: '#94a3b8', width: 1 }, + }, + data: [ + { value: 1048, name: '模型训练' }, + { value: 735, name: '数据处理' }, + { value: 580, name: '模型评测' }, + { value: 484, name: '模型推理' }, + { value: 300, name: '系统设置' } + ] + } + ] +})) + +const loginDurationStats: LoginDurationStat[] = [ + { id: 1, username: 'admin', duration: 124 }, + { id: 2, username: 'zhangsan', duration: 86 }, + { id: 3, username: 'lisi', duration: 42 }, + { id: 4, username: 'wangwu', duration: 18 }, +] + +const loginDurationChartOption = computed(() => ({ + animationDuration: 500, + grid: { top: 8, right: 40, bottom: 6, left: 8, containLabel: true }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + valueFormatter: (value) => `${value} 小时`, + }, + xAxis: { + type: 'value', + max: Math.ceil(Math.max(...loginDurationStats.map((user) => user.duration)) / 10) * 10, + splitNumber: 4, + axisLabel: { color: '#94a3b8', fontSize: 11, formatter: '{value}h' }, + axisLine: { show: false }, + axisTick: { show: false }, + splitLine: { lineStyle: { color: '#eef2f7' } }, + }, + yAxis: { + type: 'category', + inverse: true, + data: loginDurationStats.map((user) => user.username), + axisLabel: { color: '#475569', fontSize: 12 }, + axisLine: { show: false }, + axisTick: { show: false }, + }, + series: [ + { + name: '登录时长', + type: 'bar', + data: loginDurationStats.map((user) => user.duration), + barMaxWidth: 18, + barCategoryGap: '34%', + itemStyle: { color: '#4f46e5', borderRadius: [0, 4, 4, 0] }, + label: { show: true, position: 'right', distance: 8, color: '#64748b', fontSize: 11, formatter: '{c} 小时' }, + }, + ], +})) + +const recentLoginUsers: RecentLoginUser[] = [ + { id: 1, username: 'admin', role: '超级管理员', lastLogin: '10 分钟前' }, + { id: 2, username: 'zhangsan', role: '操作员', lastLogin: '2 小时前' }, + { id: 5, username: 'zhaoliu', role: '观察员', lastLogin: '5 小时前' }, + { id: 3, username: 'lisi', role: '操作员', lastLogin: '昨天 15:30' }, +] + +const roleTagType: Record = { + '超级管理员': 'danger', + '操作员': 'primary', + '观察员': 'info', +} + function viewAllTasks() { router.push('/fine-tune') } @@ -262,6 +379,38 @@ function viewTask(task: DashboardTask) { +
+
+

用户操作分布

+
+ +
+
+ +
+

登录时长排行 (本月)

+ +
+ +
+

最近登录用户

+
+
+ + {{ user.role }} +
+
+
+
+

训练任务

@@ -319,22 +468,102 @@ function viewTask(task: DashboardTask) { .dashboard-view { display: flex; flex-direction: column; - height: 100%; - gap: 10px; + min-height: 100%; + gap: 16px; min-width: 0; - min-height: 0; color: #1e293b; } .overview-section, .chart-section, .service-section, +.stat-card, .tasks-section { border: 1px solid #e2e8f0; border-radius: 10px; background: #ffffff; } +.user-stats-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.stat-card { + display: flex; + flex-direction: column; + padding: 16px 18px; + min-height: 284px; + min-width: 0; +} + +.chart-container { + flex: 1 1 auto; + min-height: 224px; + margin-top: 10px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.pie-chart { + width: 100%; + height: 224px; +} + +.duration-chart { + width: 100%; + height: 224px; + margin-top: 10px; +} + +.list-container { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; +} + +.list-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; +} + +.user-avatar { + font-size: 14px; + font-weight: 600; +} + +.username { + font-size: 13px; + font-weight: 500; + color: #334155; +} + +.user-meta { + display: flex; + flex-direction: column; + gap: 2px; +} + +.login-time { + font-size: 11px; + color: #94a3b8; +} + +.recent-user-item { + padding: 2px 0; +} + .section-title { margin: 0; font-size: 15px; @@ -429,8 +658,8 @@ function viewTask(task: DashboardTask) { .dashboard-middle { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(300px, 0.82fr); - flex: 1 1 auto; - gap: 10px; + flex: 0 0 auto; + gap: 16px; min-height: 0; } @@ -438,8 +667,8 @@ function viewTask(task: DashboardTask) { .service-section { display: flex; flex-direction: column; - min-height: 280px; - padding: 12px 14px; + min-height: 360px; + padding: 16px 18px; } .section-heading-row { @@ -460,8 +689,8 @@ function viewTask(task: DashboardTask) { .training-chart { flex: 1 1 auto; width: 100%; - height: auto; - min-height: 230px; + height: 300px; + min-height: 300px; } .service-table { @@ -614,10 +843,6 @@ function viewTask(task: DashboardTask) { } @media (max-height: 900px) and (min-width: 1181px) { - .dashboard-view { - gap: 8px; - } - .section-title { font-size: 14px; } @@ -666,20 +891,6 @@ function viewTask(task: DashboardTask) { } } - .dashboard-middle { - gap: 8px; - } - - .chart-section, - .service-section { - min-height: 240px; - padding: 10px 12px; - } - - .training-chart { - min-height: 200px; - } - .service-table { grid-template-rows: 32px repeat(4, minmax(40px, 1fr)); margin-top: 8px; @@ -741,6 +952,17 @@ function viewTask(task: DashboardTask) { .dashboard-middle { grid-template-columns: 1fr; } + + .user-stats-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .user-stats-row { + grid-template-columns: 1fr; + } + } @media (prefers-reduced-motion: reduce) {