dev #2
@@ -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('服务看板回归检查通过')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<EChartsOption>(() => ({
|
||||
],
|
||||
}))
|
||||
|
||||
const operationChartOption = computed<EChartsOption>(() => ({
|
||||
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<EChartsOption>(() => ({
|
||||
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<string, 'danger' | 'primary' | 'info'> = {
|
||||
'超级管理员': 'danger',
|
||||
'操作员': 'primary',
|
||||
'观察员': 'info',
|
||||
}
|
||||
|
||||
function viewAllTasks() {
|
||||
router.push('/fine-tune')
|
||||
}
|
||||
@@ -262,6 +379,38 @@ function viewTask(task: DashboardTask) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="user-stats-row">
|
||||
<section class="stat-card" aria-labelledby="op-dist-title">
|
||||
<h2 id="op-dist-title" class="section-title">用户操作分布</h2>
|
||||
<div class="chart-container">
|
||||
<VChart class="pie-chart" :option="operationChartOption" autoresize />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stat-card" aria-labelledby="login-dur-title">
|
||||
<h2 id="login-dur-title" class="section-title">登录时长排行 (本月)</h2>
|
||||
<VChart class="duration-chart" :option="loginDurationChartOption" autoresize />
|
||||
</section>
|
||||
|
||||
<section class="stat-card" aria-labelledby="recent-login-title">
|
||||
<h2 id="recent-login-title" class="section-title">最近登录用户</h2>
|
||||
<div class="list-container">
|
||||
<div v-for="user in recentLoginUsers" :key="user.id" class="list-item recent-user-item">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" class="user-avatar" :style="{ backgroundColor: '#e2e8f0', color: '#475569' }">
|
||||
{{ user.username.slice(0, 1).toUpperCase() }}
|
||||
</el-avatar>
|
||||
<div class="user-meta">
|
||||
<span class="username">{{ user.username }}</span>
|
||||
<span class="login-time">{{ user.lastLogin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag :type="roleTagType[user.role]" size="small">{{ user.role }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="tasks-section" aria-labelledby="tasks-title">
|
||||
<div class="section-heading-row tasks-heading">
|
||||
<h2 id="tasks-title" class="section-title">训练任务</h2>
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user