Files
YG_FT/frontend/src/views/dashboard/DashboardView.vue
caoxiaozhu 687831870c feat: 看板细节优化与设计走查补充
DashboardView 调整窄屏布局与卡片展示,回归脚本补充断言,新增窄屏看板设计截图并更新视觉走查记录。
2026-07-12 15:40:16 +08:00

754 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { EChartsOption } from 'echarts'
type ServiceState = 'normal' | 'busy' | 'error'
type TaskState = 'running' | 'pending' | 'completed' | 'failed'
interface ServiceStatus {
name: string
icon: string
state: ServiceState
instances: string
}
interface DashboardTask {
id: number
name: string
state: TaskState
trainType: string
trainMethod: string
baseModel: string
progress: number
accuracy: number | null
startedAt: string
}
const router = useRouter()
const period = ref('7d')
const serviceStatuses: ServiceStatus[] = [
{ name: '模型推理', icon: 'fa-cube', state: 'normal', instances: '6 / 6' },
{ name: '模型微调', icon: 'fa-sliders', state: 'busy', instances: '4 / 6' },
{ name: '模型评测', icon: 'fa-bar-chart', state: 'normal', instances: '3 / 3' },
{ name: '数据处理', icon: 'fa-filter', state: 'error', instances: '1 / 3' },
]
const trainingTasks: DashboardTask[] = [
{
id: 103942,
name: 'finance-sft-003',
state: 'running',
trainType: 'SFT',
trainMethod: 'LoRA',
baseModel: 'Qwen2.5-7B-Instruct',
progress: 68,
accuracy: 89.2,
startedAt: '今天 09:18',
},
{
id: 593021,
name: 'legal-eval-008',
state: 'pending',
trainType: 'DPO',
trainMethod: 'LoRA',
baseModel: 'Qwen2.5-7B-Instruct',
progress: 0,
accuracy: null,
startedAt: '今天 08:55',
},
{
id: 849301,
name: 'medical-cpt-002',
state: 'completed',
trainType: 'CPT',
trainMethod: 'Full',
baseModel: 'Qwen2.5-14B-Instruct',
progress: 100,
accuracy: 91.6,
startedAt: '07/10 16:20',
},
{
id: 201948,
name: 'finance-sft-002',
state: 'failed',
trainType: 'SFT',
trainMethod: 'LoRA',
baseModel: 'Qwen2.5-7B-Instruct',
progress: 42,
accuracy: null,
startedAt: '07/10 11:08',
},
]
const serviceStateMeta: Record<ServiceState, { label: string; className: string }> = {
normal: { label: '正常', className: 'is-normal' },
busy: { label: '繁忙', className: 'is-busy' },
error: { label: '异常', className: 'is-error' },
}
const taskStateMeta: Record<TaskState, { label: string; className: string }> = {
running: { label: '训练中', className: 'is-running' },
pending: { label: '等待中', className: 'is-pending' },
completed: { label: '已完成', className: 'is-completed' },
failed: { label: '训练失败', className: 'is-failed' },
}
const progressColor: Record<TaskState, string> = {
running: '#4f46e5',
pending: '#cbd5e1',
completed: '#10b981',
failed: '#ef4444',
}
const chartOption = computed<EChartsOption>(() => ({
animationDuration: 500,
color: ['#4f46e5', '#10b981', '#f59e0b'],
grid: { top: 50, right: 50, bottom: 38, left: 46, containLabel: false },
legend: {
top: 4,
left: 0,
itemWidth: 12,
itemHeight: 12,
itemGap: 22,
textStyle: { color: '#64748b', fontSize: 11 },
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(15, 23, 42, 0.92)',
borderWidth: 0,
padding: [10, 12],
textStyle: { color: '#ffffff', fontSize: 12 },
valueFormatter: (value) => `${value}`,
},
xAxis: {
type: 'category',
data: ['07/05', '07/06', '07/07', '07/08', '07/09', '07/10', '07/11\n今天'],
axisLine: { lineStyle: { color: '#e2e8f0' } },
axisTick: { show: false },
axisLabel: { color: '#64748b', fontSize: 11, lineHeight: 16, margin: 12 },
},
yAxis: [
{
type: 'value',
name: '次数 / GPU 数',
min: 0,
max: 20,
interval: 5,
nameTextStyle: { color: '#64748b', fontSize: 11, align: 'left' },
axisLabel: { color: '#64748b', fontSize: 11 },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#eef2f7' } },
},
{
type: 'value',
name: '准确率',
min: 0,
max: 100,
interval: 25,
nameTextStyle: { color: '#64748b', fontSize: 11, align: 'right' },
axisLabel: { color: '#64748b', fontSize: 11, formatter: '{value}%' },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
],
series: [
{
name: '训练次数(次)',
type: 'bar',
data: [8, 12, 10, 15, 13, 18, 11],
barMaxWidth: 16,
itemStyle: { borderRadius: [3, 3, 0, 0] },
label: { show: true, position: 'top', color: '#64748b', fontSize: 10 },
},
{
name: 'GPU 使用数(个)',
type: 'bar',
data: [3, 4, 4, 6, 5, 7, 5],
barMaxWidth: 16,
itemStyle: { borderRadius: [3, 3, 0, 0] },
label: { show: true, position: 'top', color: '#64748b', fontSize: 10 },
},
{
name: '平均准确率(%',
type: 'bar',
yAxisIndex: 1,
data: [82, 85, 84, 88, 87, 91, 89],
barMaxWidth: 16,
itemStyle: { borderRadius: [3, 3, 0, 0] },
label: { show: true, position: 'top', color: '#d97706', fontSize: 10 },
},
],
}))
function viewAllTasks() {
router.push('/fine-tune')
}
function viewTask(task: DashboardTask) {
router.push(`/training-log/${task.id}`)
}
</script>
<template>
<div class="dashboard-view">
<section class="overview-section" aria-labelledby="overview-title">
<h2 id="overview-title" class="section-title">平台运行状态</h2>
<div class="overview-content">
<div class="health-summary">
<div class="health-icon" aria-hidden="true"><i class="fa fa-check" /></div>
<div>
<strong>整体运行正常</strong>
<p>平台运行良好各项服务稳定</p>
</div>
</div>
<div class="overview-metrics">
<div class="overview-metric">
<span>在线服务</span>
<strong>12</strong>
<small>全部在线</small>
</div>
<div class="overview-metric">
<span>运行中任务</span>
<strong>5</strong>
<small>较昨日 +1</small>
</div>
<div class="overview-metric is-alert">
<span>待处理告警</span>
<strong>2</strong>
<small>较昨日 -1</small>
</div>
</div>
</div>
</section>
<div class="dashboard-middle">
<section class="chart-section" aria-labelledby="chart-title">
<div class="section-heading-row">
<h2 id="chart-title" class="section-title"> 7 天训练统计</h2>
<el-select v-model="period" class="period-select" aria-label="统计周期">
<el-option label="近7天" value="7d" />
</el-select>
</div>
<VChart class="training-chart" :option="chartOption" autoresize />
</section>
<section class="service-section" aria-labelledby="service-title">
<h2 id="service-title" class="section-title">服务状态</h2>
<div class="service-table" role="table" aria-label="服务状态">
<div class="service-row service-header" role="row">
<span role="columnheader">服务名称</span>
<span role="columnheader">状态</span>
<span role="columnheader">实例数</span>
</div>
<div v-for="service in serviceStatuses" :key="service.name" class="service-row" role="row">
<span class="service-name" role="cell">
<i class="fa" :class="service.icon" aria-hidden="true" />
{{ service.name }}
</span>
<span role="cell">
<span class="state-pill" :class="serviceStateMeta[service.state].className">
{{ serviceStateMeta[service.state].label }}
</span>
</span>
<span class="instance-count" role="cell">{{ service.instances }}</span>
</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>
<el-button link type="primary" @click="viewAllTasks">查看全部任务</el-button>
</div>
<div class="tasks-table-wrap">
<table class="tasks-table">
<thead>
<tr>
<th>任务名称</th>
<th>任务状态</th>
<th>训练方式</th>
<th>基座模型</th>
<th>训练进度</th>
<th>准确率</th>
<th>开始时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="task in trainingTasks" :key="task.id">
<td class="task-name">{{ task.name }}</td>
<td>
<span class="task-state" :class="taskStateMeta[task.state].className">
{{ taskStateMeta[task.state].label }}
</span>
</td>
<td>{{ task.trainType }} / {{ task.trainMethod }}</td>
<td>{{ task.baseModel }}</td>
<td>
<div class="progress-cell">
<el-progress
:percentage="task.progress"
:stroke-width="6"
:show-text="false"
:color="progressColor[task.state]"
/>
<span>{{ task.progress }}%</span>
</div>
</td>
<td class="numeric-value">{{ task.accuracy == null ? '--' : `${task.accuracy}%` }}</td>
<td>{{ task.startedAt }}</td>
<td>
<el-button link type="primary" @click="viewTask(task)">查看详情</el-button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</template>
<style scoped lang="scss">
.dashboard-view {
display: flex;
flex-direction: column;
height: 100%;
gap: 10px;
min-width: 0;
min-height: 0;
color: #1e293b;
}
.overview-section,
.chart-section,
.service-section,
.tasks-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #ffffff;
}
.section-title {
margin: 0;
font-size: 15px;
line-height: 1.4;
font-weight: 600;
color: #1e293b;
}
.overview-section {
padding: 12px 14px 13px;
}
.overview-content {
display: grid;
grid-template-columns: minmax(300px, 1.05fr) minmax(540px, 1.75fr);
align-items: stretch;
margin-top: 10px;
}
.health-summary {
display: flex;
align-items: center;
gap: 14px;
min-height: 86px;
padding: 6px 18px 6px 12px;
border-right: 1px solid #e2e8f0;
strong {
display: block;
color: #10b981;
font-size: 20px;
line-height: 1.3;
font-weight: 700;
}
p {
margin: 5px 0 0;
color: #64748b;
font-size: 13px;
}
}
.health-icon {
display: grid;
place-items: center;
width: 56px;
height: 56px;
flex: 0 0 56px;
border-radius: 50%;
background: #10b981;
color: #ffffff;
font-size: 26px;
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.14);
}
.overview-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.overview-metric {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 0;
border-right: 1px solid #e2e8f0;
&:last-child {
border-right: none;
}
span,
small {
color: #64748b;
font-size: 12px;
}
strong {
font-size: 26px;
line-height: 1;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
&.is-alert strong {
color: #ef4444;
}
}
.dashboard-middle {
display: grid;
grid-template-columns: minmax(0, 1.9fr) minmax(300px, 0.82fr);
flex: 1 1 auto;
gap: 10px;
min-height: 0;
}
.chart-section,
.service-section {
display: flex;
flex-direction: column;
min-height: 280px;
padding: 12px 14px;
}
.section-heading-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.period-select {
width: 94px;
:deep(.el-select__wrapper) {
min-height: 30px;
}
}
.training-chart {
flex: 1 1 auto;
width: 100%;
height: auto;
min-height: 230px;
}
.service-table {
display: grid;
grid-template-rows: 36px repeat(4, minmax(48px, 1fr));
flex: 1 1 auto;
margin-top: 12px;
min-height: 0;
}
.service-row {
display: grid;
grid-template-columns: minmax(130px, 1.35fr) minmax(72px, 0.7fr) 72px;
align-items: center;
min-height: 48px;
border-top: 1px solid #eef2f7;
color: #475569;
font-size: 13px;
}
.service-header {
min-height: 36px;
border-top: none;
color: #64748b;
font-size: 12px;
font-weight: 600;
}
.service-name {
display: flex;
align-items: center;
gap: 10px;
color: #334155;
font-weight: 500;
.fa {
width: 18px;
color: #64748b;
text-align: center;
}
}
.state-pill,
.task-state {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 0 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.is-normal,
.is-completed {
color: #059669;
background: #ecfdf5;
}
.is-busy,
.is-pending {
color: #d97706;
background: #fffbeb;
}
.is-error,
.is-failed {
color: #dc2626;
background: #fef2f2;
}
.is-running {
color: #4f46e5;
background: #eef2ff;
}
.instance-count,
.numeric-value {
font-variant-numeric: tabular-nums;
}
.tasks-section {
padding: 12px 0 0;
overflow: hidden;
}
.tasks-heading {
min-height: 32px;
padding: 0 14px 10px;
}
.tasks-table-wrap {
overflow-x: auto;
}
.tasks-table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
table-layout: fixed;
color: #475569;
font-size: 12px;
th,
td {
height: 42px;
padding: 6px 10px;
border-top: 1px solid #eef2f7;
text-align: left;
vertical-align: middle;
white-space: nowrap;
}
th {
height: 36px;
background: #f8fafc;
color: #475569;
font-weight: 600;
}
th:nth-child(1) { width: 14%; }
th:nth-child(2) { width: 10%; }
th:nth-child(3) { width: 11%; }
th:nth-child(4) { width: 17%; }
th:nth-child(5) { width: 17%; }
th:nth-child(6) { width: 9%; }
th:nth-child(7) { width: 12%; }
th:nth-child(8) { width: 10%; }
}
.task-name {
color: #334155;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.progress-cell {
display: grid;
grid-template-columns: minmax(72px, 1fr) 38px;
align-items: center;
gap: 8px;
font-variant-numeric: tabular-nums;
:deep(.el-progress-bar__outer) {
background: #e2e8f0;
}
}
@media (max-height: 900px) and (min-width: 1181px) {
.dashboard-view {
gap: 8px;
}
.section-title {
font-size: 14px;
}
.overview-section {
padding: 8px 12px 9px;
}
.overview-content {
margin-top: 6px;
}
.health-summary {
gap: 10px;
min-height: 64px;
padding: 4px 14px 4px 8px;
strong {
font-size: 18px;
}
p {
margin-top: 3px;
font-size: 12px;
}
}
.health-icon {
width: 44px;
height: 44px;
flex-basis: 44px;
font-size: 20px;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.12);
}
.overview-metric {
gap: 2px;
span,
small {
font-size: 11px;
}
strong {
font-size: 23px;
}
}
.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;
}
.service-row {
min-height: 40px;
font-size: 12px;
}
.service-header {
min-height: 32px;
font-size: 11px;
}
.state-pill,
.task-state {
min-height: 20px;
padding-inline: 7px;
font-size: 11px;
}
.tasks-section {
padding-top: 8px;
}
.tasks-heading {
min-height: 28px;
padding: 0 12px 8px;
}
.tasks-table {
th,
td {
height: 36px;
padding: 4px 8px;
}
th {
height: 32px;
}
}
}
@media (max-width: 1180px) {
.overview-content {
grid-template-columns: 1fr;
}
.health-summary {
border-right: none;
border-bottom: 1px solid #e2e8f0;
}
.overview-metrics {
min-height: 94px;
}
.dashboard-middle {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
.dashboard-view * {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
</style>