598 lines
23 KiB
Vue
598 lines
23 KiB
Vue
<template>
|
|
<div v-loading="loading" class="dashboard-page page-stack">
|
|
<section class="page-toolbar">
|
|
<div>
|
|
<span class="eyebrow">{{ pageConfig.group }}</span>
|
|
<h1>{{ pageConfig.title }}</h1>
|
|
<p>{{ pageConfig.description }}</p>
|
|
</div>
|
|
<div class="toolbar-actions">
|
|
<el-button :icon="Download">导出</el-button>
|
|
<el-button :icon="Refresh" :loading="loading" @click="loadOverview">刷新数据</el-button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="stats-grid dashboard-kpis">
|
|
<article v-for="item in pageKpis" :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
|
|
v-for="chart in pageCharts"
|
|
:key="chart.key"
|
|
:class="{ 'wide-chart': chart.wide, 'tall-chart': chart.tall }"
|
|
:title="chart.title"
|
|
:subtitle="chart.subtitle"
|
|
:option="chart.option"
|
|
/>
|
|
</section>
|
|
|
|
<section v-if="pageConfig.table === 'department'" class="data-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>{{ pageConfig.tableTitle }}</h2>
|
|
<p>{{ pageConfig.tableSubtitle }}</p>
|
|
</div>
|
|
</div>
|
|
<el-table :data="dashboard.departmentRows" empty-text="暂无科室数据" row-key="id">
|
|
<el-table-column prop="department" label="科室" min-width="130" />
|
|
<el-table-column prop="caseCount" label="病例数" width="100" />
|
|
<el-table-column prop="trainCount" label="训练次数" width="110" />
|
|
<el-table-column prop="effectiveTrain" label="有效训练" width="110" />
|
|
<el-table-column prop="activeUsers" label="活跃用户" width="110" />
|
|
<el-table-column prop="avgScore" label="平均分" width="100" />
|
|
</el-table>
|
|
</section>
|
|
|
|
<section v-else-if="pageConfig.table === 'case'" class="data-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>{{ pageConfig.tableTitle }}</h2>
|
|
<p>{{ pageConfig.tableSubtitle }}</p>
|
|
</div>
|
|
</div>
|
|
<el-table :data="caseRows" empty-text="暂无病例使用数据" row-key="id">
|
|
<el-table-column prop="title" label="病例名称" min-width="220" />
|
|
<el-table-column prop="count" label="训练次数" width="110" />
|
|
<el-table-column prop="total" label="完成训练" width="110" />
|
|
<el-table-column prop="passRate" label="通过率" width="110">
|
|
<template #default="{ row }">{{ percentText(row.passRate) }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import type { EChartsOption } from 'echarts'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Download, Refresh } from '@element-plus/icons-vue'
|
|
import ChartPanel from '@/components/ChartPanel.vue'
|
|
import { fetchHospitalOverview, type HospitalOverview } from '@/api/stats'
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
type StatTone = 'blue' | 'green' | 'purple' | 'orange'
|
|
type TableMode = 'department' | 'case' | ''
|
|
|
|
interface StatCard {
|
|
label: string
|
|
value: string
|
|
change: string
|
|
tone: StatTone
|
|
}
|
|
|
|
interface PageConfig {
|
|
title: string
|
|
group: string
|
|
description: string
|
|
table: TableMode
|
|
tableTitle: string
|
|
tableSubtitle: string
|
|
}
|
|
|
|
interface ChartSpec {
|
|
key: string
|
|
title: string
|
|
subtitle: string
|
|
option: EChartsOption
|
|
wide?: boolean
|
|
tall?: boolean
|
|
}
|
|
|
|
const axisLine = { lineStyle: { color: '#dce3eb' } }
|
|
const splitLine = { lineStyle: { color: '#eef2f7' } }
|
|
const route = useRoute()
|
|
const appStore = useAppStore()
|
|
const loading = ref(false)
|
|
const overview = ref<HospitalOverview | null>(null)
|
|
|
|
const pageConfigs: Record<string, PageConfig> = {
|
|
'hospital-data': {
|
|
title: '运营数据',
|
|
group: '运营数据',
|
|
description: '汇总本院训练、科室、病例和学习效果核心指标。',
|
|
table: 'department',
|
|
tableTitle: '科室运营明细',
|
|
tableSubtitle: '来自医院驾驶舱总览的科室聚合数据'
|
|
},
|
|
'case-usage': {
|
|
title: '病例使用',
|
|
group: '运营数据',
|
|
description: '查看本院病例资产、病例使用次数和训练通过率。',
|
|
table: 'case',
|
|
tableTitle: '病例使用明细',
|
|
tableSubtitle: '病例训练次数、完成训练和通过率'
|
|
},
|
|
'training-stats': {
|
|
title: '训练统计',
|
|
group: '运营数据',
|
|
description: '跟踪本院训练规模、完成率和科室训练投入。',
|
|
table: 'department',
|
|
tableTitle: '科室训练明细',
|
|
tableSubtitle: '训练总次数、有效训练和平均成绩'
|
|
},
|
|
'dept-analysis': {
|
|
title: '科室分析',
|
|
group: '运营数据',
|
|
description: '按科室分析病例数、训练次数、活跃用户和平均成绩。',
|
|
table: 'department',
|
|
tableTitle: '科室综合排行',
|
|
tableSubtitle: '病例、训练、有效训练、活跃用户和平均分'
|
|
},
|
|
'ability-trend': {
|
|
title: '能力趋势',
|
|
group: '能力提升',
|
|
description: '对比本院学员能力维度、平均分和平台平均水平。',
|
|
table: '',
|
|
tableTitle: '',
|
|
tableSubtitle: ''
|
|
},
|
|
'student-ranking': {
|
|
title: '学生排行',
|
|
group: '能力提升',
|
|
description: '按科室聚合查看本院学员训练表现和平均成绩。',
|
|
table: 'department',
|
|
tableTitle: '科室学生表现排行',
|
|
tableSubtitle: '以科室为单位聚合活跃用户、训练次数和平均分'
|
|
}
|
|
}
|
|
|
|
const currentPage = computed(() => String(route.params.page || route.path.split('/').pop() || 'hospital-data'))
|
|
const pageConfig = computed(() => pageConfigs[currentPage.value] || pageConfigs['hospital-data'])
|
|
|
|
const dashboard = computed(() => {
|
|
const data = overview.value
|
|
const summary = data?.summary || { train_months: [], train_monthly: [] }
|
|
const competency = data?.competency || { radar: [], radar_platform: [] }
|
|
const caseAsset = data?.case_asset || { top_used: [], pass_high: [], pass_low: [] }
|
|
const departments = data?.dept_rank || []
|
|
const trainMonthly = summary.train_monthly || []
|
|
const departmentRows = departments.map(item => ({
|
|
id: item.id,
|
|
department: item.department,
|
|
caseCount: item.case_count,
|
|
trainCount: item.train_count,
|
|
effectiveTrain: item.effective_train,
|
|
activeUsers: item.active_users,
|
|
avgScore: item.avg_score
|
|
}))
|
|
|
|
return {
|
|
summary,
|
|
caseAsset,
|
|
departments,
|
|
departmentRows,
|
|
months: summary.train_months || [],
|
|
trainingCounts: trainMonthly,
|
|
latestMonthTraining: trainMonthly[trainMonthly.length - 1] || 0,
|
|
growthRates: calcGrowth(trainMonthly),
|
|
scoreGauge: {
|
|
hospital: competency.student_avg || summary.avg_score || 0,
|
|
platform: competency.platform_avg || 0
|
|
},
|
|
departmentCases: departmentRows.map(item => ({ name: item.department, value: item.caseCount })),
|
|
departmentTraining: departmentRows.map(item => ({ name: item.department, total: item.trainCount, effective: item.effectiveTrain })),
|
|
departmentActiveUsers: departmentRows.map(item => ({ name: item.department, value: item.activeUsers })),
|
|
departmentScores: departmentRows.map(item => ({ name: item.department, value: item.avgScore })),
|
|
caseUsageTop: caseAsset.top_used.map(item => ({ id: item.case_id, name: item.title, value: item.count })),
|
|
passRateBest: caseAsset.pass_high.map(item => ({ id: item.case_id, name: item.title, rate: item.pass_rate, total: item.total })),
|
|
passRateWorst: caseAsset.pass_low.map(item => ({ id: item.case_id, name: item.title, rate: item.pass_rate, total: item.total })),
|
|
abilityCompare: mergeRadar(competency.radar, competency.radar_platform),
|
|
totals: {
|
|
caseCount: departmentRows.reduce((total, item) => total + item.caseCount, 0),
|
|
trainCount: departmentRows.reduce((total, item) => total + item.trainCount, 0),
|
|
effectiveTrain: departmentRows.reduce((total, item) => total + item.effectiveTrain, 0),
|
|
activeUsers: departmentRows.reduce((total, item) => total + item.activeUsers, 0)
|
|
}
|
|
}
|
|
})
|
|
|
|
const caseRows = computed(() => {
|
|
const rows = new Map<number, { id: number; title: string; count: number; total: number; passRate: number | null }>()
|
|
|
|
dashboard.value.caseUsageTop.forEach(item => {
|
|
rows.set(item.id, { id: item.id, title: item.name, count: item.value, total: 0, passRate: null })
|
|
})
|
|
;[...dashboard.value.passRateBest, ...dashboard.value.passRateWorst].forEach(item => {
|
|
const row = rows.get(item.id)
|
|
rows.set(item.id, {
|
|
id: item.id,
|
|
title: item.name,
|
|
count: row?.count || 0,
|
|
total: item.total,
|
|
passRate: item.rate
|
|
})
|
|
})
|
|
|
|
return Array.from(rows.values()).sort((a, b) => b.count - a.count)
|
|
})
|
|
|
|
const pageKpis = computed<StatCard[]>(() => {
|
|
const summary = dashboard.value.summary
|
|
const caseAsset = dashboard.value.caseAsset
|
|
const totals = dashboard.value.totals
|
|
const bestDept = [...dashboard.value.departmentRows].sort((a, b) => b.avgScore - a.avgScore)[0]
|
|
|
|
if (currentPage.value === 'case-usage') {
|
|
return [
|
|
stat('累计病例总数', numberText(caseAsset.total), '本院病例', 'blue'),
|
|
stat('当月新增病例', numberText(caseAsset.new_month), '本月新增', 'green'),
|
|
stat('已使用病例数', numberText(caseRows.value.length), '有训练记录', 'purple'),
|
|
stat('最高使用次数', numberText(dashboard.value.caseUsageTop[0]?.value), dashboard.value.caseUsageTop[0]?.name || '暂无数据', 'orange')
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'training-stats') {
|
|
return [
|
|
stat('累计训练次数', numberText(summary.train_total), '本院训练', 'blue'),
|
|
stat('本月训练次数', numberText(dashboard.value.latestMonthTraining), '最近月份', 'green'),
|
|
stat('训练完成率', percentText(summary.complete_rate), '完成训练占比', 'purple'),
|
|
stat('有效训练次数', numberText(totals.effectiveTrain), '科室合计', 'orange')
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'dept-analysis') {
|
|
return [
|
|
stat('本院科室数', numberText(summary.dept_count), '当前机构', 'blue'),
|
|
stat('科室病例数', numberText(totals.caseCount), '科室合计', 'green'),
|
|
stat('科室训练数', numberText(totals.trainCount), '科室合计', 'purple'),
|
|
stat('活跃用户数', numberText(totals.activeUsers), '科室合计', 'orange')
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'ability-trend') {
|
|
return [
|
|
stat('本院平均分', scoreText(dashboard.value.scoreGauge.hospital), '学生训练平均', 'blue'),
|
|
stat('平台平均分', scoreText(dashboard.value.scoreGauge.platform), '平台对比', 'green'),
|
|
stat('训练完成率', percentText(summary.complete_rate), '完成训练占比', 'purple'),
|
|
stat('能力维度数', numberText(dashboard.value.abilityCompare.length), '雷达维度', 'orange')
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'student-ranking') {
|
|
return [
|
|
stat('本院学员数', numberText(summary.student_count), '学生用户', 'blue'),
|
|
stat('平均训练得分', scoreText(summary.avg_score), '本院平均', 'green'),
|
|
stat('累计训练次数', numberText(summary.train_total), '本院训练', 'purple'),
|
|
stat('最高分科室', bestDept?.department || '-', bestDept ? scoreText(bestDept.avgScore) : '暂无数据', 'orange')
|
|
]
|
|
}
|
|
|
|
return [
|
|
stat('本院科室数', numberText(summary.dept_count), '当前机构', 'blue'),
|
|
stat('带教老师数', numberText(summary.doctor_count), '医生用户', 'green'),
|
|
stat('本院学员数', numberText(summary.student_count), '学生用户', 'purple'),
|
|
stat('累计训练次数', numberText(summary.train_total), '本院训练', 'blue'),
|
|
stat('训练完成率', percentText(summary.complete_rate), '完成训练占比', 'orange'),
|
|
stat('平均训练得分', scoreText(summary.avg_score), '归一百分制', 'green')
|
|
]
|
|
})
|
|
|
|
const pageCharts = computed<ChartSpec[]>(() => {
|
|
if (currentPage.value === 'case-usage') {
|
|
return [
|
|
chart('case-top', '使用病例最高次数 Top10', '按病例训练次数排序', caseUsageTopOption.value, true, true),
|
|
chart('case-pass', '病例通过率排行', '按完成训练通过率排序', casePassRateOption.value),
|
|
chart('case-total', '病例资产概览', '累计病例与当月新增病例', caseAssetOption.value)
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'training-stats') {
|
|
return [
|
|
chart('training-trend', '本院近6个月训练次数', '柱状图为训练次数,折线图为环比增长率', trainingTrendOption.value, true),
|
|
chart('dept-training', '各科室训练次数与有效训练次数', '训练总次数与完整完成次数对比', departmentTrainingOption.value, true),
|
|
chart('score-gauge', '本院训练完成率', '完成率与平均分概览', completionGaugeOption.value)
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'dept-analysis') {
|
|
return [
|
|
chart('dept-cases', '各科室病例数排行', '按科室统计当前病例数', departmentCasesOption.value),
|
|
chart('dept-training', '各科室训练次数与有效训练次数', '训练总次数与完整完成次数对比', departmentTrainingOption.value, true),
|
|
chart('dept-active', '各科室活跃用户排行', '产生训练行为的活跃用户数', departmentActiveOption.value),
|
|
chart('dept-score', '各科室平均成绩', '按科室计算训练平均成绩', departmentScoreOption.value)
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'ability-trend') {
|
|
return [
|
|
chart('ability-radar', '本院各维度得分率', '雷达图叠加平台平均线', abilityCompareOption.value, true),
|
|
chart('score-compare', '本院学员平均分与平台对比', '统计本院所有学生训练平均分', scoreGaugeOption.value),
|
|
chart('dept-score', '各科室平均成绩', '按科室计算训练平均成绩', departmentScoreOption.value)
|
|
]
|
|
}
|
|
|
|
if (currentPage.value === 'student-ranking') {
|
|
return [
|
|
chart('dept-score-rank', '科室学生平均分排行', '按科室聚合学生训练平均分', departmentScoreRankingOption.value, true),
|
|
chart('dept-active', '科室活跃学生排行', '按科室聚合活跃用户数', departmentActiveOption.value),
|
|
chart('dept-training', '科室训练投入排行', '按科室聚合训练次数', departmentTrainRankingOption.value)
|
|
]
|
|
}
|
|
|
|
return [
|
|
chart('training-trend', '本院近6个月训练次数', '柱状图为训练次数,折线图为环比增长率', trainingTrendOption.value, true),
|
|
chart('score-gauge', '本院学员平均分与平台对比', '统计本院所有学生训练平均分', scoreGaugeOption.value),
|
|
chart('dept-training', '各科室训练次数与有效训练次数', '训练总次数与完整完成次数对比', departmentTrainingOption.value, true),
|
|
chart('case-top', '使用病例最高次数 Top10', '按病例训练次数排序', caseUsageTopOption.value)
|
|
]
|
|
})
|
|
|
|
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: dashboard.value.months, axisLine },
|
|
yAxis: [
|
|
{ type: 'value', name: '训练次数', splitLine },
|
|
{ type: 'value', name: '环比', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } }
|
|
],
|
|
series: [
|
|
{ name: '训练次数', type: 'bar', barWidth: 22, data: dashboard.value.trainingCounts, itemStyle: { borderRadius: [5, 5, 0, 0] } },
|
|
{ name: '环比增长率', type: 'line', yAxisIndex: 1, smooth: true, data: dashboard.value.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: dashboard.value.scoreGauge.hospital, name: `平台 ${dashboard.value.scoreGauge.platform}` }]
|
|
}
|
|
]
|
|
}))
|
|
|
|
const completionGaugeOption = computed<EChartsOption>(() => ({
|
|
color: ['#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: 22, offsetCenter: [0, '62%'] },
|
|
data: [{ value: dashboard.value.summary.complete_rate || 0, name: `平均分 ${scoreText(dashboard.value.summary.avg_score)}` }]
|
|
}
|
|
]
|
|
}))
|
|
|
|
const departmentCasesOption = computed<EChartsOption>(() => horizontalBarOption(
|
|
dashboard.value.departmentCases.map(item => item.name),
|
|
dashboard.value.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: dashboard.value.departmentTraining.map(item => item.name), axisLine },
|
|
series: [
|
|
{ name: '训练总次数', type: 'bar', barWidth: 14, data: dashboard.value.departmentTraining.map(item => item.total) },
|
|
{ name: '有效训练次数', type: 'bar', barWidth: 14, data: dashboard.value.departmentTraining.map(item => item.effective) }
|
|
]
|
|
}))
|
|
|
|
const departmentActiveOption = computed<EChartsOption>(() => horizontalBarOption(
|
|
dashboard.value.departmentActiveUsers.map(item => item.name),
|
|
dashboard.value.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: dashboard.value.departmentScores.map(item => item.name), axisLine },
|
|
yAxis: { type: 'value', max: 100, splitLine },
|
|
series: [
|
|
{ name: '平均成绩', type: 'bar', barWidth: 22, data: dashboard.value.departmentScores.map(item => item.value), itemStyle: { borderRadius: [5, 5, 0, 0] } }
|
|
]
|
|
}))
|
|
|
|
const departmentScoreRankingOption = computed<EChartsOption>(() => {
|
|
const rows = [...dashboard.value.departmentScores].sort((a, b) => b.value - a.value)
|
|
return horizontalBarOption(
|
|
rows.map(item => item.name),
|
|
rows.map(item => item.value),
|
|
'平均分',
|
|
'#f59e0b',
|
|
100
|
|
)
|
|
})
|
|
|
|
const departmentTrainRankingOption = computed<EChartsOption>(() => {
|
|
const rows = [...dashboard.value.departmentTraining].sort((a, b) => b.total - a.total)
|
|
return horizontalBarOption(
|
|
rows.map(item => item.name),
|
|
rows.map(item => item.total),
|
|
'训练次数',
|
|
'#2563eb'
|
|
)
|
|
})
|
|
|
|
const caseUsageTopOption = computed<EChartsOption>(() => horizontalBarOption(
|
|
dashboard.value.caseUsageTop.map(item => item.name),
|
|
dashboard.value.caseUsageTop.map(item => item.value),
|
|
'训练次数',
|
|
'#2563eb'
|
|
))
|
|
|
|
const casePassRateOption = computed<EChartsOption>(() => {
|
|
const rows = [...caseRows.value].filter(item => item.passRate !== null).sort((a, b) => (b.passRate || 0) - (a.passRate || 0))
|
|
return horizontalBarOption(
|
|
rows.map(item => item.title),
|
|
rows.map(item => item.passRate || 0),
|
|
'通过率',
|
|
'#0f766e',
|
|
100
|
|
)
|
|
})
|
|
|
|
const caseAssetOption = computed<EChartsOption>(() => ({
|
|
color: ['#2563eb', '#f59e0b'],
|
|
tooltip: { trigger: 'axis' },
|
|
grid: { left: 42, right: 20, top: 28, bottom: 32 },
|
|
xAxis: { type: 'category', data: ['累计病例', '当月新增'], axisLine },
|
|
yAxis: { type: 'value', splitLine },
|
|
series: [
|
|
{
|
|
name: '病例数',
|
|
type: 'bar',
|
|
barWidth: 28,
|
|
data: [dashboard.value.caseAsset.total || 0, dashboard.value.caseAsset.new_month || 0],
|
|
itemStyle: { borderRadius: [5, 5, 0, 0] }
|
|
}
|
|
]
|
|
}))
|
|
|
|
const abilityCompareOption = computed<EChartsOption>(() => ({
|
|
color: ['#2563eb', '#f59e0b'],
|
|
legend: { top: 0 },
|
|
radar: {
|
|
radius: 96,
|
|
indicator: dashboard.value.abilityCompare.map(item => ({ name: item.name, max: 100 }))
|
|
},
|
|
series: [
|
|
{
|
|
type: 'radar',
|
|
data: [
|
|
{ name: '本院平均', value: dashboard.value.abilityCompare.map(item => item.hospital), areaStyle: { color: 'rgba(37, 99, 235, 0.14)' } },
|
|
{ name: '平台平均', value: dashboard.value.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: 100, right: 24, top: 18, bottom: 22 },
|
|
xAxis: { type: 'value', max, splitLine },
|
|
yAxis: { type: 'category', inverse: true, data: names, axisLabel: { width: 94, overflow: 'truncate' }, axisLine },
|
|
series: [
|
|
{ name: seriesName, type: 'bar', barWidth: 14, data: values, itemStyle: { borderRadius: [0, 5, 5, 0] } }
|
|
]
|
|
}
|
|
}
|
|
|
|
async function loadOverview() {
|
|
if (!appStore.token) {
|
|
ElMessage.warning('缺少登录信息,请重新登录')
|
|
return
|
|
}
|
|
if (loading.value) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
overview.value = await fetchHospitalOverview(appStore.token)
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '获取医院概览失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function stat(label: string, value: string, change: string, tone: StatTone): StatCard {
|
|
return { label, value, change, tone }
|
|
}
|
|
|
|
function chart(key: string, title: string, subtitle: string, option: EChartsOption, wide = false, tall = false): ChartSpec {
|
|
return { key, title, subtitle, option, wide, tall }
|
|
}
|
|
|
|
function numberText(value: number | null | undefined) {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value.toLocaleString('zh-CN') : '-'
|
|
}
|
|
|
|
function scoreText(value: number | null | undefined) {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(1) : '-'
|
|
}
|
|
|
|
function percentText(value: number | null | undefined) {
|
|
return typeof value === 'number' && Number.isFinite(value) ? `${value.toFixed(1)}%` : '-'
|
|
}
|
|
|
|
function calcGrowth(values: number[]) {
|
|
return values.map((value, index) => {
|
|
if (index === 0 || !values[index - 1]) {
|
|
return 0
|
|
}
|
|
return Number((((value - values[index - 1]) / values[index - 1]) * 100).toFixed(1))
|
|
})
|
|
}
|
|
|
|
function mergeRadar(
|
|
hospital: Array<{ dimension: string; score: number }>,
|
|
platform: Array<{ dimension: string; score: number }>
|
|
) {
|
|
const platformMap = new Map(platform.map(item => [item.dimension, item.score]))
|
|
const names = Array.from(new Set([...hospital.map(item => item.dimension), ...platform.map(item => item.dimension)]))
|
|
return names.map(name => ({
|
|
name,
|
|
hospital: hospital.find(item => item.dimension === name)?.score || 0,
|
|
platform: platformMap.get(name) || 0
|
|
}))
|
|
}
|
|
|
|
watch(
|
|
() => appStore.token,
|
|
token => {
|
|
if (token) {
|
|
loadOverview()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script>
|