feat: 图表绘制
This commit is contained in:
@@ -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>
|
||||
+199
-61
@@ -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">
|
||||
<div>
|
||||
<h2>最新动态</h2>
|
||||
<p>平台关键事件流</p>
|
||||
</div>
|
||||
<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>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<em>{{ item.change }}</em>
|
||||
</div>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="item in activityTimeline" :key="item" timestamp="今天" placement="top">
|
||||
{{ item }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user