feat: 图表绘制

This commit is contained in:
王天骄
2026-06-12 17:01:12 +08:00
parent 3c8db9f503
commit 9fddb42ebe
7 changed files with 1220 additions and 61 deletions
+138
View File
@@ -466,6 +466,10 @@ p {
gap: 16px;
}
.dashboard-kpis {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.stat-card {
display: flex;
align-items: center;
@@ -533,12 +537,37 @@ p {
gap: 18px;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.wide-chart {
grid-column: span 2;
}
.tall-chart {
.chart-canvas {
height: 380px;
}
}
.compact-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.55fr);
gap: 18px;
}
.hospital-rank-grid,
.content-warning-grid {
grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr);
}
.chart-panel,
.data-section {
padding: 18px;
@@ -581,6 +610,115 @@ p {
height: 300px;
}
.rank-list,
.quality-list {
display: grid;
gap: 12px;
}
.rank-item {
display: grid;
grid-template-columns: 28px minmax(120px, 0.8fr) minmax(160px, 1fr) 58px;
align-items: center;
gap: 12px;
min-height: 36px;
> span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 8px;
color: #fff;
font-weight: 700;
background: var(--primary);
}
strong {
min-width: 0;
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
color: var(--green);
font-size: 12px;
font-style: normal;
text-align: right;
}
}
.pass-rate-columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
h3 {
margin: 0 0 12px;
font-size: 15px;
}
}
.pass-rate-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 38px;
margin-bottom: 10px;
padding: 10px 12px;
border-radius: 8px;
background: var(--panel-soft);
strong {
min-width: 0;
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
span {
color: var(--green);
font-weight: 700;
}
&.danger span {
color: var(--danger);
}
}
.quality-item {
display: grid;
gap: 8px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-soft);
strong,
span {
display: block;
}
strong {
font-size: 14px;
}
span {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
}
.compact-search {
width: 220px;
}
.page-toolbar {
display: flex;
align-items: center;
+310
View File
@@ -41,3 +41,313 @@ export const activityTimeline = [
'方正中心医院导入 86 名学生账号',
'内容团队发布卒中识别专题训练包'
]
export const platformDashboard = {
kpis: [
{ label: '入驻医院/机构数', value: '86', change: '+5 本月', tone: 'blue' },
{ label: '月活跃机构数', value: '73', change: '84.9% 活跃', tone: 'green' },
{ label: '平台总用户数', value: '48,260', change: '+1,246', tone: 'purple' },
{ label: '月活跃用户数', value: '18,936', change: '+12.4%', tone: 'orange' },
{ label: '本月新增训练', value: '28,642', change: '+18.6% 环比', tone: 'blue' },
{ label: '累计训练次数', value: '426,180', change: '+42,908', tone: 'green' },
{ label: '训练完成率', value: '88.7%', change: '+2.3%', tone: 'purple' },
{ label: '平均训练得分', value: '83.6', change: '+1.8 分', tone: 'orange' }
],
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
trainingTrend: [18620, 21580, 24890, 26930, 30120, 34480],
activeUserTrend: [6420, 7310, 8160, 8940, 10320, 11860],
userComposition: [
{ name: '学生', value: 38260 },
{ name: '带教老师', value: 7240 },
{ name: '医院管理员', value: 2760 }
],
hourlyTrainingAverage: {
hours: ['00', '03', '06', '09', '12', '15', '18', '21'],
values: [22, 18, 42, 186, 218, 246, 312, 204]
},
institutionUserDistribution: [
{ name: '方正中心医院', users: 4260, activeUsers: 3120 },
{ name: '华东医学院', users: 3920, activeUsers: 2860 },
{ name: '首都医科附院', users: 3180, activeUsers: 2210 },
{ name: '南城社区医院', users: 1260, activeUsers: 760 },
{ name: '西湖教学医院', users: 1080, activeUsers: 690 }
],
hospitalTrainingRanking: [
{ name: '方正中心医院', value: 6420 },
{ name: '华东医学院', value: 5880 },
{ name: '首都医科附院', value: 5240 },
{ name: '西湖教学医院', value: 4120 },
{ name: '南城社区医院', value: 3860 },
{ name: '浦江人民医院', value: 3420 },
{ name: '仁济临床学院', value: 3210 },
{ name: '东城中心医院', value: 2860 },
{ name: '松江教学医院', value: 2460 },
{ name: '宁海县医院', value: 2180 }
],
hospitalScoreRanking: [
{ name: '华东医学院', value: 89.4 },
{ name: '方正中心医院', value: 88.1 },
{ name: '仁济临床学院', value: 86.7 },
{ name: '首都医科附院', value: 85.9 },
{ name: '西湖教学医院', value: 84.2 },
{ name: '南城社区医院', value: 82.7 }
],
hospitalActivityRanking: [
{ name: '方正中心医院', value: 91, change: '+4.8%' },
{ name: '华东医学院', value: 87, change: '+3.5%' },
{ name: '首都医科附院', value: 84, change: '+2.6%' },
{ name: '西湖教学医院', value: 79, change: '+1.8%' },
{ name: '南城社区医院', value: 73, change: '+5.1%' }
],
caseKpis: [
{ label: '累计病例总数', value: '1,286', change: '+38 本月', tone: 'green' },
{ label: '本月新增病例数', value: '96', change: '+21.5% 环比', tone: 'blue' }
],
caseTypeDistribution: [
{ name: '脚本病例', value: 526 },
{ name: '教学病例', value: 418 },
{ name: '传统病例', value: 342 }
],
caseTypeUsageRate: [
{ name: '脚本病例', value: 82 },
{ name: '教学病例', value: 76 },
{ name: '传统病例', value: 64 }
],
caseTypeTrainingTrend: {
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
script: [3100, 3480, 4020, 4360, 4880, 5420],
teaching: [2280, 2540, 2880, 3150, 3610, 4080],
traditional: [1620, 1860, 2040, 2290, 2480, 2760],
growth: [5.8, 8.1, 10.2, 7.6, 11.4, 12.8]
},
topCaseUsage: [
{ name: '急性胸痛鉴别诊断', value: 1842 },
{ name: '卒中早期识别训练', value: 1626 },
{ name: '儿童发热问诊流程', value: 1520 },
{ name: '糖尿病慢病随访', value: 1416 },
{ name: '腹痛急诊处理', value: 1288 },
{ name: '慢阻肺急性加重', value: 1190 },
{ name: '产后出血识别', value: 1042 },
{ name: '肾绞痛处置', value: 968 },
{ name: '甲亢危象判断', value: 884 },
{ name: '儿童哮喘发作', value: 820 }
],
lowCasePassRates: [
{ name: '产后出血识别', value: 58 },
{ name: '肾绞痛处置', value: 61 },
{ name: '甲亢危象判断', value: 63 },
{ name: '慢阻肺急性加重', value: 66 },
{ name: '复杂腹痛鉴别', value: 68 }
],
opsTrend: {
days: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
calls: [14200, 16800, 15900, 18200, 21400, 19600, 23100],
responseTime: [420, 398, 416, 384, 372, 390, 365]
}
}
export const hospitalDashboard = {
profile: {
name: '方正中心医院',
level: '三甲综合医院',
cooperationDays: 428
},
kpis: [
{ label: '本院科室数', value: '18', change: '+2 较上月', tone: 'blue' },
{ label: '带教老师数', value: '86', change: '+6', tone: 'green' },
{ label: '本院学员数', value: '1,260', change: '+84', tone: 'purple' },
{ label: '训练完成率', value: '89.6%', change: '+3.2%', tone: 'orange' },
{ label: '平均训练得分', value: '84.8', change: '+1.6 分', tone: 'green' },
{ label: '累计病例总数', value: '526', change: '+21 本月', tone: 'blue' },
{ label: '当月新增病例', value: '42', change: '+16.7%', tone: 'purple' }
],
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
trainingCounts: [4260, 4820, 5160, 5880, 6420, 7240],
growthRates: [4.2, 6.6, 7.1, 13.9, 9.2, 12.8],
departmentCases: [
{ name: '心内科', value: 86 },
{ name: '儿科', value: 72 },
{ name: '神经内科', value: 64 },
{ name: '急诊科', value: 58 },
{ name: '内分泌科', value: 46 },
{ name: '呼吸科', value: 42 }
],
departmentTraining: [
{ name: '心内科', total: 1260, effective: 1120 },
{ name: '儿科', total: 1080, effective: 940 },
{ name: '急诊科', total: 980, effective: 846 },
{ name: '神经内科', total: 920, effective: 790 },
{ name: '内分泌科', total: 760, effective: 690 }
],
departmentActiveUsers: [
{ name: '心内科', value: 286 },
{ name: '儿科', value: 242 },
{ name: '急诊科', value: 218 },
{ name: '神经内科', value: 204 },
{ name: '呼吸科', value: 166 }
],
departmentScores: [
{ name: '心内科', value: 88.4 },
{ name: '儿科', value: 86.2 },
{ name: '神经内科', value: 84.6 },
{ name: '急诊科', value: 82.9 },
{ name: '内分泌科', value: 81.8 }
],
weeklyTrainingRanking: [
{ period: '06/01-06/07', name: '刘一鸣', department: '心内科', count: 34 },
{ period: '06/01-06/07', name: '赵晴', department: '儿科', count: 31 },
{ period: '06/01-06/07', name: '周子涵', department: '急诊科', count: 29 },
{ period: '06/01-06/07', name: '陈思远', department: '神经内科', count: 27 },
{ period: '06/01-06/07', name: '王悦', department: '内分泌科', count: 24 }
],
studentScoreRanking: [
{ name: '赵晴', value: 94.2 },
{ name: '刘一鸣', value: 92.8 },
{ name: '陈思远', value: 91.1 },
{ name: '周子涵', value: 89.6 },
{ name: '王悦', value: 87.4 }
],
caseUsageTop: [
{ name: '急性胸痛鉴别诊断', value: 842 },
{ name: '儿童发热问诊流程', value: 720 },
{ name: '卒中早期识别训练', value: 686 },
{ name: '糖尿病慢病随访', value: 602 },
{ name: '腹痛急诊处理', value: 548 },
{ name: '慢阻肺急性加重', value: 502 },
{ name: '产后出血识别', value: 466 },
{ name: '肾绞痛处置', value: 420 },
{ name: '甲亢危象判断', value: 386 },
{ name: '儿童哮喘发作', value: 342 }
],
passRateBest: [
{ name: '儿童发热问诊流程', rate: 94 },
{ name: '糖尿病慢病随访', rate: 92 },
{ name: '急性胸痛鉴别诊断', rate: 89 },
{ name: '卒中早期识别训练', rate: 87 },
{ name: '儿童哮喘发作', rate: 86 }
],
passRateWorst: [
{ name: '产后出血识别', rate: 58 },
{ name: '甲亢危象判断', rate: 62 },
{ name: '复杂腹痛鉴别', rate: 65 },
{ name: '肾绞痛处置', rate: 67 },
{ name: '慢阻肺急性加重', rate: 69 }
],
abilityCompare: [
{ name: '问诊', hospital: 86, platform: 82 },
{ name: '诊断', hospital: 84, platform: 80 },
{ name: '治疗', hospital: 81, platform: 78 },
{ name: '沟通', hospital: 88, platform: 83 },
{ name: '检查', hospital: 83, platform: 79 }
],
scoreGauge: {
hospital: 84.8,
platform: 82.1
}
}
export const contentDashboard = {
kpis: [
{ label: '病例总数', value: '526', change: '+21 本月', tone: 'green' },
{ label: '知识文档数', value: '1,842', change: '+86', tone: 'blue' },
{ label: '待发布病例数', value: '38', change: '12 待审核', tone: 'orange' },
{ label: '本月新增病例', value: '42', change: '+16.7%', tone: 'purple' },
{ label: '病例使用率', value: '76.4%', change: '+5.2%', tone: 'green' },
{ label: '累计训练总次数', value: '68,420', change: '+9.8%', tone: 'blue' }
],
caseTypeDistribution: [
{ name: '传统病例', value: 166 },
{ name: '教学病例', value: 214 },
{ name: '脚本病例', value: 146 }
],
caseTypeTraining: [
{ name: '传统病例', value: 9820 },
{ name: '教学病例', value: 18260 },
{ name: '脚本病例', value: 14280 }
],
departmentCases: [
{ name: '心内科', value: 86 },
{ name: '儿科', value: 72 },
{ name: '神经内科', value: 64 },
{ name: '急诊科', value: 58 },
{ name: '内分泌科', value: 46 },
{ name: '呼吸科', value: 42 }
],
departmentUsage: [
{ name: '心内科', usedCases: 72, trainingTimes: 12600 },
{ name: '儿科', usedCases: 58, trainingTimes: 10820 },
{ name: '神经内科', usedCases: 52, trainingTimes: 9360 },
{ name: '急诊科', usedCases: 46, trainingTimes: 8680 },
{ name: '内分泌科', usedCases: 36, trainingTimes: 6420 }
],
difficultyUsage: [
{ name: '初级', cases: 182, usedCases: 148, trainingTimes: 18620 },
{ name: '中级', cases: 238, usedCases: 176, trainingTimes: 28480 },
{ name: '高级', cases: 106, usedCases: 78, trainingTimes: 21320 }
],
lowPassWarnings: [
{ name: '产后出血识别', department: '妇产科', type: '脚本病例', rate: 58, trainings: 466 },
{ name: '甲亢危象判断', department: '内分泌科', type: '教学病例', rate: 62, trainings: 386 },
{ name: '复杂腹痛鉴别', department: '急诊科', type: '传统病例', rate: 65, trainings: 328 },
{ name: '肾绞痛处置', department: '泌尿外科', type: '脚本病例', rate: 67, trainings: 420 },
{ name: '慢阻肺急性加重', department: '呼吸科', type: '教学病例', rate: 69, trainings: 502 }
],
hotCases: [
{ name: '急性胸痛鉴别诊断', value: 842 },
{ name: '儿童发热问诊流程', value: 720 },
{ name: '卒中早期识别训练', value: 686 },
{ name: '糖尿病慢病随访', value: 602 },
{ name: '腹痛急诊处理', value: 548 }
],
caseManagementRows: [
{ name: '急性胸痛鉴别诊断', department: '心内科', author: '王内容', difficulty: '高', type: '脚本病例', scoring: '已上传', project: '检查+检验', status: '启用', audit: '已通过' },
{ name: '儿童发热问诊流程', department: '儿科', author: '李编辑', difficulty: '中', type: '教学病例', scoring: '已上传', project: '检验', status: '启用', audit: '待审核' },
{ name: '产后出血识别', department: '妇产科', author: '王内容', difficulty: '高', type: '脚本病例', scoring: '待完善', project: '检查', status: '启用', audit: '需优化' },
{ name: '糖尿病慢病随访', department: '内分泌科', author: '赵编辑', difficulty: '低', type: '传统病例', scoring: '已上传', project: '检验', status: '启用', audit: '已通过' },
{ name: '肾绞痛处置', department: '泌尿外科', author: '李编辑', difficulty: '中', type: '脚本病例', scoring: '已上传', project: '检查', status: '禁用', audit: '复核中' }
]
}
export const teacherDashboard = {
kpis: [
{ label: '我的学生数', value: '46', change: '+4 本月', tone: 'blue' },
{ label: '学生整体平均分', value: '84.2', change: '医院 82.8', tone: 'green' },
{ label: '进行中任务数', value: '6', change: '2 个今日截止', tone: 'orange' },
{ label: '任务完成率', value: '78.5%', change: '+6.1%', tone: 'purple' }
],
students: [
{ id: 'S-10021', name: '刘一鸣', department: '心内科', trainings: 156, completion: '92%', avgScore: 92.8, weakDimension: '治疗', favoriteType: '脚本病例', lastTraining: '2026-06-11 19:24', pendingTasks: 1 },
{ id: 'S-10032', name: '赵晴', department: '儿科', trainings: 142, completion: '88%', avgScore: 94.2, weakDimension: '检查', favoriteType: '教学病例', lastTraining: '2026-06-12 08:16', pendingTasks: 0 },
{ id: 'S-10048', name: '陈思远', department: '神经内科', trainings: 128, completion: '84%', avgScore: 91.1, weakDimension: '沟通', favoriteType: '脚本病例', lastTraining: '2026-06-10 21:03', pendingTasks: 2 },
{ id: 'S-10053', name: '周子涵', department: '急诊科', trainings: 121, completion: '81%', avgScore: 89.6, weakDimension: '诊断', favoriteType: '传统病例', lastTraining: '2026-06-11 14:42', pendingTasks: 2 },
{ id: 'S-10067', name: '王悦', department: '内分泌科', trainings: 108, completion: '76%', avgScore: 87.4, weakDimension: '问诊', favoriteType: '教学病例', lastTraining: '2026-06-09 18:30', pendingTasks: 3 }
],
weakRadar: [
{ name: '问诊', value: 82 },
{ name: '诊断', value: 78 },
{ name: '治疗', value: 74 },
{ name: '沟通', value: 86 },
{ name: '检查', value: 76 }
],
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
trainingTrend: [620, 760, 840, 920, 1080, 1260],
taskCompletion: [
{ name: '胸痛鉴别', done: 38, undone: 8 },
{ name: '发热问诊', done: 34, undone: 12 },
{ name: '卒中识别', done: 29, undone: 17 },
{ name: '慢病随访', done: 41, undone: 5 }
],
scoreDistribution: [
{ name: '90分以上', value: 18 },
{ name: '80-89分', value: 21 },
{ name: '70-79分', value: 9 },
{ name: '60-69分', value: 4 },
{ name: '60分以下', value: 2 }
],
taskRows: [
{ id: 'T-202606-001', name: '急性胸痛鉴别诊断', type: '临床思维模拟练习', expected: 46, finished: 38, startedAt: '2026-06-03', deadline: '2026-06-12', avgScore: 86.4, highestScore: 98 },
{ id: 'T-202606-002', name: '儿童发热问诊流程', type: '客观题训练', expected: 46, finished: 34, startedAt: '2026-06-05', deadline: '2026-06-14', avgScore: 88.1, highestScore: 96 },
{ id: 'T-202606-003', name: '卒中早期识别训练', type: '临床思维模拟练习', expected: 46, finished: 29, startedAt: '2026-06-07', deadline: '2026-06-16', avgScore: 82.8, highestScore: 94 },
{ id: 'T-202606-004', name: '糖尿病慢病随访', type: '客观题训练', expected: 46, finished: 41, startedAt: '2026-06-09', deadline: '2026-06-18', avgScore: 90.2, highestScore: 99 }
]
}
+3
View File
@@ -25,6 +25,9 @@ const routes: RouteRecordRaw[] = [
{ path: 'my-students', name: 'MyStudents', component: () => import('@/views/MyStudentsView.vue'), meta: { title: '我的学生' } },
{ path: 'teacher-student-relations', name: 'TeacherStudentRelations', component: () => import('@/views/TeacherStudentRelationsView.vue'), meta: { title: '师生关系管理' } },
{ path: 'settings', name: 'Settings', component: () => import('@/views/SettingsView.vue'), meta: { title: '系统配置' } },
{ path: 'module/hospital-dashboard', name: 'HospitalDashboard', component: () => import('@/views/HospitalDashboardView.vue'), meta: { title: '医院驾驶舱' } },
{ path: 'module/content-dashboard', name: 'ContentDashboard', component: () => import('@/views/ContentDashboardView.vue'), meta: { title: '内容概览' } },
{ path: 'module/teacher-dashboard', name: 'TeacherDashboard', component: () => import('@/views/TeacherDashboardView.vue'), meta: { title: '教学概览' } },
{ path: 'module/content-admin-list', redirect: '/users/content-admins' },
{ path: 'module/department-list', redirect: '/departments' },
{ path: 'module/doctor-list', redirect: '/users/doctors' },
+199
View File
@@ -0,0 +1,199 @@
<template>
<div class="dashboard-page page-stack">
<section class="hero-strip dashboard-hero">
<div>
<span class="eyebrow">内容概览大屏</span>
<h1>内容录入员工作台</h1>
<p>围绕病例总量状态使用率低通过率预警和内容管理效率监控内容资产质量</p>
</div>
<div class="hero-actions">
<el-button :icon="Upload">导入知识库</el-button>
<el-button :icon="Plus" type="primary">新增病例</el-button>
</div>
</section>
<section class="stats-grid dashboard-kpis">
<article v-for="item in contentDashboard.kpis" :key="item.label" class="stat-card">
<div :class="['stat-mark', item.tone]">{{ item.label.slice(0, 1) }}</div>
<div>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.change }}</em>
</div>
</article>
</section>
<section class="overview-grid">
<ChartPanel title="不同病例类型分布" subtitle="传统、教学、脚本病例数量" :option="caseTypeDistributionOption" />
<ChartPanel title="不同类型病例训练次数" subtitle="按类型汇总训练总次数" :option="caseTypeTrainingOption" />
<ChartPanel class="wide-chart" title="不同科室病例分布" subtitle="按科室统计病例数" :option="departmentCasesOption" />
<ChartPanel class="wide-chart" title="不同科室病例使用与训练次数" subtitle="使用数为去重病例数,训练次数为累计次数" :option="departmentUsageOption" />
<ChartPanel class="wide-chart" title="不同病例难度分布与使用次数" subtitle="病例数、病例使用数、病例训练次数对比" :option="difficultyUsageOption" />
<ChartPanel title="病例使用热度 Top5" subtitle="按训练次数排序" :option="hotCasesOption" />
</section>
<section class="content-grid content-warning-grid">
<div class="data-section">
<div class="section-header">
<div>
<h2>低通过率病例预警</h2>
<p>按通过率升序展示前五个病例</p>
</div>
</div>
<el-table :data="contentDashboard.lowPassWarnings" height="300">
<el-table-column prop="name" label="病例名称" min-width="170" />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="rate" label="通过率" width="100">
<template #default="{ row }">
<el-tag type="danger">{{ row.rate }}%</el-tag>
</template>
</el-table-column>
<el-table-column prop="trainings" label="训练次数" width="110" />
</el-table>
</div>
<div class="data-section">
<div class="section-header">
<div>
<h2>内容质量处理队列</h2>
<p>可直接用于后续接入病例列表接口</p>
</div>
</div>
<div class="quality-list">
<article v-for="item in contentDashboard.lowPassWarnings" :key="item.name" class="quality-item">
<div>
<strong>{{ item.name }}</strong>
<span>{{ item.department }} | {{ item.type }}</span>
</div>
<el-progress :percentage="item.rate" status="exception" :stroke-width="8" />
</article>
</div>
</div>
</section>
<section class="data-section">
<div class="section-header">
<div>
<h2>病例管理</h2>
<p>病例名称科室录入人员难度类型评分规则关联项目状态与审核状态</p>
</div>
<div class="toolbar-actions">
<el-button :icon="Search">筛选</el-button>
<el-button :icon="Plus" type="primary">新增病例</el-button>
</div>
</div>
<el-table :data="contentDashboard.caseManagementRows" row-key="name">
<el-table-column prop="name" label="病例名称" min-width="180" />
<el-table-column prop="department" label="科室" width="110" />
<el-table-column prop="author" label="录入人员" width="110" />
<el-table-column prop="difficulty" label="难度" width="80">
<template #default="{ row }">
<el-tag :type="row.difficulty === '高' ? 'danger' : row.difficulty === '中' ? 'warning' : 'success'">{{ row.difficulty }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="110" />
<el-table-column prop="scoring" label="评分规则" width="100" />
<el-table-column prop="project" label="关联项目" width="120" />
<el-table-column prop="status" label="状态" width="80" />
<el-table-column prop="audit" label="审核状态" width="100" />
<el-table-column label="操作" width="160" fixed="right">
<template #default>
<el-button link type="primary">修改</el-button>
<el-button link type="primary">详情</el-button>
<el-button link type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</section>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Plus, Search, Upload } from '@element-plus/icons-vue'
import ChartPanel from '@/components/ChartPanel.vue'
import { contentDashboard } from '@/mock/dashboard'
const axisLine = { lineStyle: { color: '#dce3eb' } }
const splitLine = { lineStyle: { color: '#eef2f7' } }
const caseTypeDistributionOption = computed<EChartsOption>(() => horizontalBarOption(
contentDashboard.caseTypeDistribution.map(item => item.name),
contentDashboard.caseTypeDistribution.map(item => item.value),
'病例数',
'#2563eb'
))
const caseTypeTrainingOption = computed<EChartsOption>(() => ({
color: ['#0f766e'],
tooltip: { trigger: 'axis' },
grid: { left: 42, right: 20, top: 28, bottom: 32 },
xAxis: { type: 'category', data: contentDashboard.caseTypeTraining.map(item => item.name), axisLine },
yAxis: { type: 'value', splitLine },
series: [
{ name: '训练次数', type: 'bar', barWidth: 24, data: contentDashboard.caseTypeTraining.map(item => item.value), itemStyle: { borderRadius: [5, 5, 0, 0] } }
]
}))
const departmentCasesOption = computed<EChartsOption>(() => horizontalBarOption(
contentDashboard.departmentCases.map(item => item.name),
contentDashboard.departmentCases.map(item => item.value),
'病例数',
'#7c3aed'
))
const departmentUsageOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 70, right: 48, top: 42, bottom: 28 },
xAxis: [
{ type: 'value', name: '使用数', splitLine },
{ type: 'value', name: '训练次数', splitLine: { show: false } }
],
yAxis: { type: 'category', inverse: true, data: contentDashboard.departmentUsage.map(item => item.name), axisLine },
series: [
{ name: '病例使用数', type: 'bar', barWidth: 14, data: contentDashboard.departmentUsage.map(item => item.usedCases) },
{ name: '病例训练次数', type: 'bar', barWidth: 14, xAxisIndex: 1, data: contentDashboard.departmentUsage.map(item => item.trainingTimes) }
]
}))
const difficultyUsageOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 48, right: 36, top: 42, bottom: 32 },
xAxis: { type: 'category', data: contentDashboard.difficultyUsage.map(item => item.name), axisLine },
yAxis: [
{ type: 'value', name: '病例数', splitLine },
{ type: 'value', name: '训练次数', splitLine: { show: false } }
],
series: [
{ name: '病例数', type: 'bar', data: contentDashboard.difficultyUsage.map(item => item.cases) },
{ name: '病例使用数', type: 'bar', data: contentDashboard.difficultyUsage.map(item => item.usedCases) },
{ name: '病例训练次数', type: 'line', yAxisIndex: 1, smooth: true, data: contentDashboard.difficultyUsage.map(item => item.trainingTimes) }
]
}))
const hotCasesOption = computed<EChartsOption>(() => horizontalBarOption(
contentDashboard.hotCases.map(item => item.name),
contentDashboard.hotCases.map(item => item.value),
'训练次数',
'#dc2626'
))
function horizontalBarOption(names: string[], values: number[], seriesName: string, color: string): EChartsOption {
return {
color: [color],
tooltip: { trigger: 'axis' },
grid: { left: 98, right: 24, top: 18, bottom: 22 },
xAxis: { type: 'value', splitLine },
yAxis: { type: 'category', inverse: true, data: names, axisLabel: { width: 92, overflow: 'truncate' }, axisLine },
series: [
{ name: seriesName, type: 'bar', barWidth: 14, data: values, itemStyle: { borderRadius: [0, 5, 5, 0] } }
]
}
}
</script>
+198 -60
View File
@@ -1,19 +1,19 @@
<template>
<div class="dashboard-page page-stack">
<section class="hero-strip">
<section class="hero-strip dashboard-hero">
<div>
<span class="eyebrow">MediAI Command Center</span>
<h1>数据驾驶舱</h1>
<p>集中查看病例训练机构活跃AI评估质量和教学任务进展</p>
<span class="eyebrow">平台总览大屏</span>
<h1>超级管理员数据驾驶舱</h1>
<p>从机构用户活跃训练效果病例资产和 AI 服务稳定性监控平台整体运营状况</p>
</div>
<div class="hero-actions">
<el-button :icon="Download">导出报表</el-button>
<el-button :icon="Plus" type="primary">新增训练任务</el-button>
<el-button :icon="Refresh">刷新数据</el-button>
</div>
</section>
<section class="stats-grid">
<article v-for="item in stats" :key="item.label" class="stat-card">
<section class="stats-grid dashboard-kpis">
<article v-for="item in platformDashboard.kpis" :key="item.label" class="stat-card">
<div :class="['stat-mark', item.tone]">{{ item.label.slice(0, 1) }}</div>
<div>
<span>{{ item.label }}</span>
@@ -23,87 +23,225 @@
</article>
</section>
<section class="dashboard-grid">
<ChartPanel title="训练趋势" subtitle="近 7 日训练完成量与通过率" :option="trendOption">
<template #actions>
<el-segmented v-model="range" :options="['周', '月', '季']" />
</template>
</ChartPanel>
<ChartPanel title="能力画像" subtitle="按最新训练评估汇总" :option="radarOption" />
<section class="overview-grid">
<ChartPanel class="wide-chart" title="近6个月训练次数与活跃用户" subtitle="柱状图为训练次数,折线图为月活跃用户数" :option="trainingActiveOption" />
<ChartPanel title="用户构成" subtitle="学生、带教老师、医院管理员分布" :option="userCompositionOption" />
<ChartPanel title="近7天平均训练量" subtitle="按小时聚合的平均训练量" :option="hourlyAverageOption" />
<ChartPanel class="wide-chart" title="各机构用户人数与活跃人数" subtitle="累计注册用户与近30天活跃用户对比" :option="institutionUsersOption" />
</section>
<section class="content-grid">
<section class="overview-grid">
<ChartPanel class="wide-chart tall-chart" title="医院训练次数排行 Top10" subtitle="按医院训练次数求和排序" :option="hospitalTrainingRankingOption" />
<ChartPanel title="医院平均分排名" subtitle="按当月训练平均分排序" :option="hospitalScoreRankingOption" />
<div class="data-section">
<div class="section-header">
<div>
<h2>重点病例</h2>
<p>高频训练和待审核内容</p>
<h2>各医院月活跃度排名</h2>
<p>月活跃用户数 / 注册用户数含同比变化</p>
</div>
<el-button link type="primary">查看全部</el-button>
</div>
<el-table :data="caseRows" height="300">
<el-table-column prop="name" label="病例名称" min-width="170" />
<el-table-column prop="department" label="科室" width="110" />
<el-table-column prop="difficulty" label="难度" width="80">
<template #default="{ row }">
<el-tag :type="row.difficulty === '高' ? 'danger' : row.difficulty === '中' ? 'warning' : 'success'">{{ row.difficulty }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="usage" label="训练次数" width="110" />
<el-table-column prop="status" label="状态" width="100" />
</el-table>
<div class="rank-list">
<article v-for="(item, index) in platformDashboard.hospitalActivityRanking" :key="item.name" class="rank-item">
<span>{{ index + 1 }}</span>
<strong>{{ item.name }}</strong>
<el-progress :percentage="item.value" :stroke-width="8" />
<em>{{ item.change }}</em>
</article>
</div>
</div>
</section>
<div class="data-section">
<div class="section-header">
<section class="stats-grid compact-kpis">
<article v-for="item in platformDashboard.caseKpis" :key="item.label" class="stat-card">
<div :class="['stat-mark', item.tone]">{{ item.label.slice(0, 1) }}</div>
<div>
<h2>最新动态</h2>
<p>平台关键事件流</p>
</div>
</div>
<el-timeline>
<el-timeline-item v-for="item in activityTimeline" :key="item" timestamp="今天" placement="top">
{{ item }}
</el-timeline-item>
</el-timeline>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.change }}</em>
</div>
</article>
</section>
<section class="overview-grid">
<ChartPanel title="病例类型分布" subtitle="脚本、教学、传统病例占比" :option="caseTypeDistributionOption" />
<ChartPanel title="各类型病例使用率" subtitle="被使用病例数 / 类型病例总数" :option="caseTypeUsageOption" />
<ChartPanel class="wide-chart" title="各类型病例月训练次数变化" subtitle="柱状图为训练次数,折线图为环比增长率" :option="caseTypeTrainingTrendOption" />
<ChartPanel class="wide-chart tall-chart" title="病例使用排行 Top10" subtitle="按病例训练次数求和排序" :option="topCaseUsageOption" />
<ChartPanel title="低通过率病例预警" subtitle="得分不低于60分的训练占比" :option="lowCasePassOption" />
<ChartPanel class="wide-chart" title="AI服务调用与响应时长" subtitle="近一周平均调用量与响应时长" :option="opsTrendOption" />
</section>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Download, Plus } from '@element-plus/icons-vue'
import { Download, Refresh } from '@element-plus/icons-vue'
import ChartPanel from '@/components/ChartPanel.vue'
import { abilityRadar, activityTimeline, caseRows, stats } from '@/mock/dashboard'
import { platformDashboard } from '@/mock/dashboard'
const range = ref('周')
const palette = ['#2563eb', '#0f766e', '#f59e0b', '#7c3aed', '#dc2626', '#16a34a']
const axisLine = { lineStyle: { color: '#dce3eb' } }
const splitLine = { lineStyle: { color: '#eef2f7' } }
const trendOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#16a34a'],
const trainingActiveOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e'],
tooltip: { trigger: 'axis' },
grid: { left: 36, right: 20, top: 34, bottom: 28 },
xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] },
yAxis: { type: 'value', splitLine: { lineStyle: { color: '#eef2f7' } } },
legend: { top: 0 },
grid: { left: 54, right: 46, top: 42, bottom: 32 },
xAxis: { type: 'category', data: platformDashboard.months, axisLine },
yAxis: [
{ type: 'value', name: '训练次数', splitLine },
{ type: 'value', name: '活跃用户', splitLine: { show: false } }
],
series: [
{ name: '完成量', type: 'bar', barWidth: 18, data: [320, 438, 386, 520, 628, 446, 580], itemStyle: { borderRadius: [5, 5, 0, 0] } },
{ name: '通过率', type: 'line', smooth: true, data: [82, 86, 84, 88, 91, 87, 92] }
{ name: '训练次数', type: 'bar', barWidth: 22, data: platformDashboard.trainingTrend, itemStyle: { borderRadius: [5, 5, 0, 0] } },
{ name: '活跃用户', type: 'line', yAxisIndex: 1, smooth: true, data: platformDashboard.activeUserTrend }
]
}))
const radarOption = computed<EChartsOption>(() => ({
color: ['#0f766e'],
radar: {
radius: 94,
indicator: abilityRadar.map(item => ({ name: item.name, max: 100 }))
},
const userCompositionOption = computed<EChartsOption>(() => ({
color: palette,
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'radar',
areaStyle: { color: 'rgba(15, 118, 110, 0.16)' },
data: [{ value: abilityRadar.map(item => item.value), name: '综合能力' }]
name: '用户构成',
type: 'pie',
radius: ['42%', '68%'],
center: ['50%', '46%'],
data: platformDashboard.userComposition,
label: { formatter: '{b}\n{d}%' }
}
]
}))
const hourlyAverageOption = computed<EChartsOption>(() => ({
color: ['#16a34a'],
tooltip: { trigger: 'axis' },
grid: { left: 42, right: 18, top: 28, bottom: 30 },
xAxis: { type: 'category', data: platformDashboard.hourlyTrainingAverage.hours, axisLine },
yAxis: { type: 'value', splitLine },
series: [
{ name: '平均训练量', type: 'line', smooth: true, areaStyle: { color: 'rgba(22, 163, 74, 0.12)' }, data: platformDashboard.hourlyTrainingAverage.values }
]
}))
const institutionUsersOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 46, right: 20, top: 42, bottom: 34 },
xAxis: { type: 'category', data: platformDashboard.institutionUserDistribution.map(item => item.name), axisLabel: { interval: 0, rotate: 18 }, axisLine },
yAxis: { type: 'value', splitLine },
series: [
{ name: '累计用户', type: 'bar', barWidth: 18, data: platformDashboard.institutionUserDistribution.map(item => item.users) },
{ name: '活跃用户', type: 'bar', barWidth: 18, data: platformDashboard.institutionUserDistribution.map(item => item.activeUsers) }
]
}))
const hospitalTrainingRankingOption = computed<EChartsOption>(() => horizontalBarOption(
platformDashboard.hospitalTrainingRanking.map(item => item.name),
platformDashboard.hospitalTrainingRanking.map(item => item.value),
'训练次数',
'#2563eb'
))
const hospitalScoreRankingOption = computed<EChartsOption>(() => horizontalBarOption(
platformDashboard.hospitalScoreRanking.map(item => item.name),
platformDashboard.hospitalScoreRanking.map(item => item.value),
'平均分',
'#0f766e',
100
))
const caseTypeDistributionOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e', '#f59e0b'],
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
name: '病例类型',
type: 'pie',
radius: ['48%', '72%'],
center: ['50%', '45%'],
data: platformDashboard.caseTypeDistribution,
label: { formatter: '{b}\n{d}%' }
}
]
}))
const caseTypeUsageOption = computed<EChartsOption>(() => ({
color: ['#0f766e'],
tooltip: { trigger: 'axis', valueFormatter: value => `${value}%` },
grid: { left: 42, right: 20, top: 28, bottom: 32 },
xAxis: { type: 'category', data: platformDashboard.caseTypeUsageRate.map(item => item.name), axisLine },
yAxis: { type: 'value', max: 100, splitLine },
series: [
{ name: '使用率', type: 'bar', barWidth: 24, data: platformDashboard.caseTypeUsageRate.map(item => item.value), itemStyle: { borderRadius: [5, 5, 0, 0] } }
]
}))
const caseTypeTrainingTrendOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e', '#f59e0b', '#dc2626'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 50, right: 48, top: 44, bottom: 32 },
xAxis: { type: 'category', data: platformDashboard.caseTypeTrainingTrend.months, axisLine },
yAxis: [
{ type: 'value', name: '训练次数', splitLine },
{ type: 'value', name: '环比', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } }
],
series: [
{ name: '脚本病例', type: 'bar', stack: 'case', data: platformDashboard.caseTypeTrainingTrend.script },
{ name: '教学病例', type: 'bar', stack: 'case', data: platformDashboard.caseTypeTrainingTrend.teaching },
{ name: '传统病例', type: 'bar', stack: 'case', data: platformDashboard.caseTypeTrainingTrend.traditional },
{ name: '环比增长率', type: 'line', yAxisIndex: 1, smooth: true, data: platformDashboard.caseTypeTrainingTrend.growth }
]
}))
const topCaseUsageOption = computed<EChartsOption>(() => horizontalBarOption(
platformDashboard.topCaseUsage.map(item => item.name),
platformDashboard.topCaseUsage.map(item => item.value),
'训练次数',
'#7c3aed'
))
const lowCasePassOption = computed<EChartsOption>(() => horizontalBarOption(
platformDashboard.lowCasePassRates.map(item => item.name),
platformDashboard.lowCasePassRates.map(item => item.value),
'通过率',
'#dc2626',
100,
'{value}%'
))
const opsTrendOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 54, right: 52, top: 42, bottom: 32 },
xAxis: { type: 'category', data: platformDashboard.opsTrend.days, axisLine },
yAxis: [
{ type: 'value', name: '调用量', splitLine },
{ type: 'value', name: '响应ms', splitLine: { show: false } }
],
series: [
{ name: 'AI平均调用量', type: 'bar', barWidth: 22, data: platformDashboard.opsTrend.calls, itemStyle: { borderRadius: [5, 5, 0, 0] } },
{ name: '平均响应时长', type: 'line', yAxisIndex: 1, smooth: true, data: platformDashboard.opsTrend.responseTime }
]
}))
function horizontalBarOption(names: string[], values: number[], seriesName: string, color: string, max?: number, formatter?: string): EChartsOption {
return {
color: [color],
tooltip: { trigger: 'axis' },
grid: { left: 116, right: 28, top: 18, bottom: 22 },
xAxis: { type: 'value', max, axisLabel: formatter ? { formatter } : undefined, splitLine },
yAxis: { type: 'category', inverse: true, data: names, axisLabel: { width: 104, overflow: 'truncate' }, axisLine },
series: [
{ name: seriesName, type: 'bar', barWidth: 14, data: values, itemStyle: { borderRadius: [0, 5, 5, 0] } }
]
}
}
</script>
+211
View File
@@ -0,0 +1,211 @@
<template>
<div class="dashboard-page page-stack">
<section class="hero-strip dashboard-hero">
<div>
<span class="eyebrow">医院驾驶舱大屏</span>
<h1>{{ hospitalDashboard.profile.name }}</h1>
<p>{{ hospitalDashboard.profile.level }} | 合作 {{ hospitalDashboard.profile.cooperationDays }} 聚焦本院资源师生训练与效果</p>
</div>
<div class="hero-actions">
<el-button :icon="Download">导出院内报表</el-button>
<el-button :icon="Refresh">每日更新</el-button>
</div>
</section>
<section class="stats-grid dashboard-kpis">
<article v-for="item in hospitalDashboard.kpis" :key="item.label" class="stat-card">
<div :class="['stat-mark', item.tone]">{{ item.label.slice(0, 1) }}</div>
<div>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.change }}</em>
</div>
</article>
</section>
<section class="overview-grid">
<ChartPanel class="wide-chart" title="本院近6个月训练次数" subtitle="柱状图为累计训练次数,折线图为环比增长率" :option="trainingTrendOption" />
<ChartPanel title="本院学员平均分与平台对比" subtitle="统计本院所有学生训练平均分" :option="scoreGaugeOption" />
<ChartPanel title="各科室病例数排行" subtitle="按科室统计当前病例数" :option="departmentCasesOption" />
<ChartPanel class="wide-chart" title="各科室训练次数与有效训练次数" subtitle="训练总次数与完整完成次数对比" :option="departmentTrainingOption" />
<ChartPanel title="各科室活跃用户排行" subtitle="上月产生训练行为的去重学员数" :option="departmentActiveOption" />
<ChartPanel title="各科室平均成绩" subtitle="按科室计算训练平均成绩" :option="departmentScoreOption" />
</section>
<section class="content-grid hospital-rank-grid">
<div class="data-section">
<div class="section-header">
<div>
<h2>每周训练次数排行</h2>
<p>按学生每周训练总次数排序</p>
</div>
</div>
<el-table :data="hospitalDashboard.weeklyTrainingRanking" height="300">
<el-table-column prop="period" label="时间段" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="department" label="科室" width="110" />
<el-table-column prop="count" label="训练次数" />
</el-table>
</div>
<ChartPanel title="学员每周训练平均分排行" subtitle="按每周训练平均分排序" :option="studentScoreRankingOption" />
</section>
<section class="overview-grid">
<ChartPanel class="wide-chart tall-chart" title="使用病例最高次数 Top10" subtitle="按病例训练次数排序" :option="caseUsageTopOption" />
<div class="data-section pass-rate-board">
<div class="section-header">
<div>
<h2>病例训练通过率排行</h2>
<p>通过率 = 及格训练次数 / 完成训练次数</p>
</div>
</div>
<div class="pass-rate-columns">
<section>
<h3>最高通过率</h3>
<article v-for="item in hospitalDashboard.passRateBest" :key="item.name" class="pass-rate-item">
<strong>{{ item.name }}</strong>
<span>{{ item.rate }}%</span>
</article>
</section>
<section>
<h3>最低通过率</h3>
<article v-for="item in hospitalDashboard.passRateWorst" :key="item.name" class="pass-rate-item danger">
<strong>{{ item.name }}</strong>
<span>{{ item.rate }}%</span>
</article>
</section>
</div>
</div>
<ChartPanel class="wide-chart" title="本院各维度得分率" subtitle="雷达图叠加平台平均线" :option="abilityCompareOption" />
</section>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Download, Refresh } from '@element-plus/icons-vue'
import ChartPanel from '@/components/ChartPanel.vue'
import { hospitalDashboard } from '@/mock/dashboard'
const axisLine = { lineStyle: { color: '#dce3eb' } }
const splitLine = { lineStyle: { color: '#eef2f7' } }
const trainingTrendOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 48, right: 48, top: 42, bottom: 32 },
xAxis: { type: 'category', data: hospitalDashboard.months, axisLine },
yAxis: [
{ type: 'value', name: '训练次数', splitLine },
{ type: 'value', name: '环比', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } }
],
series: [
{ name: '训练次数', type: 'bar', barWidth: 22, data: hospitalDashboard.trainingCounts, itemStyle: { borderRadius: [5, 5, 0, 0] } },
{ name: '环比增长率', type: 'line', yAxisIndex: 1, smooth: true, data: hospitalDashboard.growthRates }
]
}))
const scoreGaugeOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e'],
series: [
{
name: '本院平均分',
type: 'gauge',
radius: '82%',
min: 0,
max: 100,
progress: { show: true, roundCap: true, width: 12 },
axisLine: { lineStyle: { width: 12 } },
pointer: { width: 4 },
detail: { formatter: '{value}', fontSize: 24, offsetCenter: [0, '62%'] },
data: [{ value: hospitalDashboard.scoreGauge.hospital, name: `平台 ${hospitalDashboard.scoreGauge.platform}` }]
}
]
}))
const departmentCasesOption = computed<EChartsOption>(() => horizontalBarOption(
hospitalDashboard.departmentCases.map(item => item.name),
hospitalDashboard.departmentCases.map(item => item.value),
'病例数',
'#2563eb'
))
const departmentTrainingOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#0f766e'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 70, right: 24, top: 42, bottom: 28 },
xAxis: { type: 'value', splitLine },
yAxis: { type: 'category', inverse: true, data: hospitalDashboard.departmentTraining.map(item => item.name), axisLine },
series: [
{ name: '训练总次数', type: 'bar', barWidth: 14, data: hospitalDashboard.departmentTraining.map(item => item.total) },
{ name: '有效训练次数', type: 'bar', barWidth: 14, data: hospitalDashboard.departmentTraining.map(item => item.effective) }
]
}))
const departmentActiveOption = computed<EChartsOption>(() => horizontalBarOption(
hospitalDashboard.departmentActiveUsers.map(item => item.name),
hospitalDashboard.departmentActiveUsers.map(item => item.value),
'活跃用户',
'#0f766e'
))
const departmentScoreOption = computed<EChartsOption>(() => ({
color: ['#7c3aed'],
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 20, top: 28, bottom: 32 },
xAxis: { type: 'category', data: hospitalDashboard.departmentScores.map(item => item.name), axisLine },
yAxis: { type: 'value', max: 100, splitLine },
series: [
{ name: '平均成绩', type: 'bar', barWidth: 22, data: hospitalDashboard.departmentScores.map(item => item.value), itemStyle: { borderRadius: [5, 5, 0, 0] } }
]
}))
const studentScoreRankingOption = computed<EChartsOption>(() => horizontalBarOption(
hospitalDashboard.studentScoreRanking.map(item => item.name),
hospitalDashboard.studentScoreRanking.map(item => item.value),
'平均分',
'#f59e0b',
100
))
const caseUsageTopOption = computed<EChartsOption>(() => horizontalBarOption(
hospitalDashboard.caseUsageTop.map(item => item.name),
hospitalDashboard.caseUsageTop.map(item => item.value),
'训练次数',
'#2563eb'
))
const abilityCompareOption = computed<EChartsOption>(() => ({
color: ['#2563eb', '#f59e0b'],
legend: { top: 0 },
radar: {
radius: 96,
indicator: hospitalDashboard.abilityCompare.map(item => ({ name: item.name, max: 100 }))
},
series: [
{
type: 'radar',
data: [
{ name: '本院平均', value: hospitalDashboard.abilityCompare.map(item => item.hospital), areaStyle: { color: 'rgba(37, 99, 235, 0.14)' } },
{ name: '平台平均', value: hospitalDashboard.abilityCompare.map(item => item.platform), areaStyle: { color: 'rgba(245, 158, 11, 0.1)' } }
]
}
]
}))
function horizontalBarOption(names: string[], values: number[], seriesName: string, color: string, max?: number): EChartsOption {
return {
color: [color],
tooltip: { trigger: 'axis' },
grid: { left: 88, right: 24, top: 18, bottom: 22 },
xAxis: { type: 'value', max, splitLine },
yAxis: { type: 'category', inverse: true, data: names, axisLabel: { width: 82, overflow: 'truncate' }, axisLine },
series: [
{ name: seriesName, type: 'bar', barWidth: 14, data: values, itemStyle: { borderRadius: [0, 5, 5, 0] } }
]
}
}
</script>
+160
View File
@@ -0,0 +1,160 @@
<template>
<div class="dashboard-page page-stack">
<section class="hero-strip dashboard-hero">
<div>
<span class="eyebrow">教学数据中心</span>
<h1>带教医生教学概览</h1>
<p>实时查看所带学生训练画像任务完成分数分布和已下发任务情况</p>
</div>
<div class="hero-actions">
<el-button :icon="Download">导出教学报告</el-button>
<el-button :icon="Plus" type="primary">新建任务</el-button>
</div>
</section>
<section class="stats-grid">
<article v-for="item in teacherDashboard.kpis" :key="item.label" class="stat-card">
<div :class="['stat-mark', item.tone]">{{ item.label.slice(0, 1) }}</div>
<div>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.change }}</em>
</div>
</article>
</section>
<section class="data-section">
<div class="section-header">
<div>
<h2>我的学生列表</h2>
<p>可替换为学生训练情况表学习画像表和师生关系表聚合数据</p>
</div>
<div class="toolbar-actions">
<el-input class="compact-search" :prefix-icon="Search" placeholder="搜索学生" />
<el-button :icon="Search">筛选</el-button>
</div>
</div>
<el-table :data="teacherDashboard.students" row-key="id">
<el-table-column prop="id" label="学生ID" width="110" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="department" label="科室" width="110" />
<el-table-column prop="trainings" label="累计训练" width="100" />
<el-table-column prop="completion" label="完成率" width="90" />
<el-table-column prop="avgScore" label="平均分" width="90" />
<el-table-column prop="weakDimension" label="薄弱维度" width="100">
<template #default="{ row }">
<el-tag type="warning">{{ row.weakDimension }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="favoriteType" label="最常训练类型" width="130" />
<el-table-column prop="lastTraining" label="最近一次训练时间" min-width="160" />
<el-table-column prop="pendingTasks" label="待完成任务" width="110">
<template #default="{ row }">
<el-tag :type="row.pendingTasks > 0 ? 'danger' : 'success'">{{ row.pendingTasks }}</el-tag>
</template>
</el-table-column>
</el-table>
</section>
<section class="overview-grid">
<ChartPanel title="整体薄弱维度" subtitle="问诊、诊断、治疗、沟通、检查平均得分率" :option="weakRadarOption" />
<ChartPanel class="wide-chart" title="整体训练次数趋势" subtitle="近6个月所带学生训练总次数" :option="trainingTrendOption" />
<ChartPanel class="wide-chart" title="任务完成情况" subtitle="一周内下发任务的已完成与未完成人数" :option="taskCompletionOption" />
<ChartPanel title="当前任务得分分布" subtitle="已完成学生的分数分布" :option="scoreDistributionOption" />
</section>
<section class="data-section">
<div class="section-header">
<div>
<h2>任务列表</h2>
<p>展示已下发任务情况后续可接老师创建任务后的统计接口</p>
</div>
<el-button :icon="Plus" type="primary">新建任务</el-button>
</div>
<el-table :data="teacherDashboard.taskRows" row-key="id">
<el-table-column prop="id" label="任务ID" width="130" />
<el-table-column prop="name" label="任务名称" min-width="180" />
<el-table-column prop="type" label="类型" min-width="150" />
<el-table-column prop="expected" label="应完成人数" width="110" />
<el-table-column prop="finished" label="实际完成" width="100" />
<el-table-column prop="startedAt" label="开始时间" width="120" />
<el-table-column prop="deadline" label="截止时间" width="120" />
<el-table-column prop="avgScore" label="平均分" width="90" />
<el-table-column prop="highestScore" label="最高成绩" width="100" />
<el-table-column label="操作" width="160" fixed="right">
<template #default>
<el-button link type="primary">修改</el-button>
<el-button link type="primary">详情</el-button>
<el-button link type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</section>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Download, Plus, Search } from '@element-plus/icons-vue'
import ChartPanel from '@/components/ChartPanel.vue'
import { teacherDashboard } from '@/mock/dashboard'
const axisLine = { lineStyle: { color: '#dce3eb' } }
const splitLine = { lineStyle: { color: '#eef2f7' } }
const weakRadarOption = computed<EChartsOption>(() => ({
color: ['#dc2626'],
radar: {
radius: 94,
indicator: teacherDashboard.weakRadar.map(item => ({ name: item.name, max: 100 }))
},
series: [
{
type: 'radar',
areaStyle: { color: 'rgba(220, 38, 38, 0.12)' },
data: [{ value: teacherDashboard.weakRadar.map(item => item.value), name: '平均得分率' }]
}
]
}))
const trainingTrendOption = computed<EChartsOption>(() => ({
color: ['#2563eb'],
tooltip: { trigger: 'axis' },
grid: { left: 42, right: 20, top: 28, bottom: 32 },
xAxis: { type: 'category', data: teacherDashboard.months, axisLine },
yAxis: { type: 'value', splitLine },
series: [
{ name: '训练次数', type: 'line', smooth: true, areaStyle: { color: 'rgba(37, 99, 235, 0.12)' }, data: teacherDashboard.trainingTrend }
]
}))
const taskCompletionOption = computed<EChartsOption>(() => ({
color: ['#0f766e', '#f59e0b'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 42, right: 20, top: 42, bottom: 40 },
xAxis: { type: 'category', data: teacherDashboard.taskCompletion.map(item => item.name), axisLine },
yAxis: { type: 'value', splitLine },
series: [
{ name: '已完成', type: 'bar', barWidth: 20, data: teacherDashboard.taskCompletion.map(item => item.done), itemStyle: { borderRadius: [5, 5, 0, 0] } },
{ name: '未完成', type: 'bar', barWidth: 20, data: teacherDashboard.taskCompletion.map(item => item.undone), itemStyle: { borderRadius: [5, 5, 0, 0] } }
]
}))
const scoreDistributionOption = computed<EChartsOption>(() => ({
color: ['#16a34a', '#2563eb', '#f59e0b', '#7c3aed', '#dc2626'],
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
name: '得分分布',
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '45%'],
data: teacherDashboard.scoreDistribution,
label: { formatter: '{b}\n{d}%' }
}
]
}))
</script>