refactor: Dashboard 看板布局与图表优化
DashboardView 调整看板卡片与栅格展示,ECharts 补充图表类型,主布局 dashboard 区域由内嵌滚动改为自适应可滚动以避免内容被裁剪,回归脚本适配。
This commit is contained in:
@@ -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