976 lines
22 KiB
Vue
976 lines
22 KiB
Vue
<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
|
||
}
|
||
|
||
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')
|
||
|
||
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 },
|
||
},
|
||
],
|
||
}))
|
||
|
||
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')
|
||
}
|
||
|
||
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>
|
||
|
||
<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>
|
||
<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;
|
||
min-height: 100%;
|
||
gap: 16px;
|
||
min-width: 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;
|
||
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: 0 0 auto;
|
||
gap: 16px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.chart-section,
|
||
.service-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 360px;
|
||
padding: 16px 18px;
|
||
}
|
||
|
||
.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: 300px;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.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) {
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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) {
|
||
.dashboard-view * {
|
||
scroll-behavior: auto !important;
|
||
transition-duration: 0.01ms !important;
|
||
animation-duration: 0.01ms !important;
|
||
}
|
||
}
|
||
</style>
|