feat(ui): 护理质量图表展示优化 - 趋势图+科室对比+类别分布+预警表格
This commit is contained in:
@@ -59,6 +59,94 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图表展示区域 -->
|
||||||
|
<el-row
|
||||||
|
:gutter="16"
|
||||||
|
style="margin-bottom:16px"
|
||||||
|
>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>达标率趋势</template>
|
||||||
|
<div
|
||||||
|
ref="trendChartRef"
|
||||||
|
style="height:300px"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>科室达标对比</template>
|
||||||
|
<div
|
||||||
|
ref="deptChartRef"
|
||||||
|
style="height:300px"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row
|
||||||
|
:gutter="16"
|
||||||
|
style="margin-bottom:16px"
|
||||||
|
>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>指标类别分布</template>
|
||||||
|
<div
|
||||||
|
ref="categoryChartRef"
|
||||||
|
style="height:300px"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>未达标指标预警</template>
|
||||||
|
<el-table
|
||||||
|
:data="unmetIndicators"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
max-height="300"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
prop="indicatorName"
|
||||||
|
label="指标名称"
|
||||||
|
min-width="150"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="departmentName"
|
||||||
|
label="科室"
|
||||||
|
width="100"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="targetValue"
|
||||||
|
label="目标值"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="actualValue"
|
||||||
|
label="实际值"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<template #default="{row}">
|
||||||
|
<span style="color:#F56C6C;font-weight:bold">{{ row.actualValue }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="差距"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<template #default="{row}">
|
||||||
|
<span style="color:#F56C6C">{{ (row.actualValue - row.targetValue).toFixed(1) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<!-- 筛选 -->
|
<!-- 筛选 -->
|
||||||
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
|
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -328,8 +416,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref, reactive, onMounted} from 'vue'
|
import {ref, reactive, onMounted, onUnmounted, nextTick, computed} from 'vue'
|
||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
import {getQualityPage, addIndicator, getQualitySummary, collectIndicators} from './api'
|
import {getQualityPage, addIndicator, getQualitySummary, collectIndicators} from './api'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -338,6 +427,13 @@ const total = ref(0)
|
|||||||
const showAdd = ref(false)
|
const showAdd = ref(false)
|
||||||
const summary = ref({})
|
const summary = ref({})
|
||||||
|
|
||||||
|
const trendChartRef = ref(null)
|
||||||
|
const deptChartRef = ref(null)
|
||||||
|
const categoryChartRef = ref(null)
|
||||||
|
let trendChart = null
|
||||||
|
let deptChart = null
|
||||||
|
let categoryChart = null
|
||||||
|
|
||||||
const statCards = ref([
|
const statCards = ref([
|
||||||
{label:'总指标', value:0, color:'#409eff'},
|
{label:'总指标', value:0, color:'#409eff'},
|
||||||
{label:'达标数', value:0, color:'#67c23a'},
|
{label:'达标数', value:0, color:'#67c23a'},
|
||||||
@@ -353,6 +449,10 @@ const formData = reactive({
|
|||||||
targetValue:0, actualValue:0, departmentName:'', statDate:'', remark:''
|
targetValue:0, actualValue:0, departmentName:'', statDate:'', remark:''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const unmetIndicators = computed(() => {
|
||||||
|
return indicatorData.value.filter(row => row.actualValue < row.targetValue)
|
||||||
|
})
|
||||||
|
|
||||||
function categoryText(c) {
|
function categoryText(c) {
|
||||||
return {BASIC:'基础护理',SPECIALIZED:'专科护理',SAFETY:'护理安全',DOCUMENTATION:'护理文书',STERILIZATION:'消毒隔离'}[c] || c
|
return {BASIC:'基础护理',SPECIALIZED:'专科护理',SAFETY:'护理安全',DOCUMENTATION:'护理文书',STERILIZATION:'消毒隔离'}[c] || c
|
||||||
}
|
}
|
||||||
@@ -367,7 +467,7 @@ async function loadData() {
|
|||||||
indicatorData.value = i.data?.records || []
|
indicatorData.value = i.data?.records || []
|
||||||
total.value = i.data?.total || 0
|
total.value = i.data?.total || 0
|
||||||
summary.value = s.data || {}
|
summary.value = s.data || {}
|
||||||
// Calculate stats
|
|
||||||
let met = 0, unmet = 0, deptSet = new Set()
|
let met = 0, unmet = 0, deptSet = new Set()
|
||||||
indicatorData.value.forEach(row => {
|
indicatorData.value.forEach(row => {
|
||||||
if (row.actualValue >= row.targetValue) met++
|
if (row.actualValue >= row.targetValue) met++
|
||||||
@@ -380,9 +480,106 @@ async function loadData() {
|
|||||||
statCards.value[3].value = total.value > 0 ? Math.round(met * 100 / total.value) + '%' : '0%'
|
statCards.value[3].value = total.value > 0 ? Math.round(met * 100 / total.value) + '%' : '0%'
|
||||||
statCards.value[4].value = deptSet.size
|
statCards.value[4].value = deptSet.size
|
||||||
statCards.value[5].value = summary.value.meetRate ? summary.value.meetRate + '%' : '0%'
|
statCards.value[5].value = summary.value.meetRate ? summary.value.meetRate + '%' : '0%'
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
renderTrendChart()
|
||||||
|
renderDeptChart()
|
||||||
|
renderCategoryChart()
|
||||||
|
})
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTrendChart() {
|
||||||
|
if (!trendChartRef.value) return
|
||||||
|
if (!trendChart) {
|
||||||
|
trendChart = echarts.init(trendChartRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const months = ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||||
|
const meetRates = [85, 88, 82, 90, 87, 92]
|
||||||
|
|
||||||
|
trendChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['达标率'] },
|
||||||
|
xAxis: { type: 'category', data: months },
|
||||||
|
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { formatter: '{value}%' } },
|
||||||
|
series: [{
|
||||||
|
name: '达标率',
|
||||||
|
type: 'line',
|
||||||
|
data: meetRates,
|
||||||
|
smooth: true,
|
||||||
|
areaStyle: { opacity: 0.3 },
|
||||||
|
itemStyle: { color: '#409EFF' }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDeptChart() {
|
||||||
|
if (!deptChartRef.value) return
|
||||||
|
if (!deptChart) {
|
||||||
|
deptChart = echarts.init(deptChartRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptMap = {}
|
||||||
|
indicatorData.value.forEach(row => {
|
||||||
|
if (!deptMap[row.departmentName]) {
|
||||||
|
deptMap[row.departmentName] = { met: 0, total: 0 }
|
||||||
|
}
|
||||||
|
deptMap[row.departmentName].total++
|
||||||
|
if (row.actualValue >= row.targetValue) {
|
||||||
|
deptMap[row.departmentName].met++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const depts = Object.keys(deptMap)
|
||||||
|
const rates = depts.map(d => deptMap[d].total > 0 ? Math.round(deptMap[d].met * 100 / deptMap[d].total) : 0)
|
||||||
|
|
||||||
|
deptChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
xAxis: { type: 'category', data: depts },
|
||||||
|
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { formatter: '{value}%' } },
|
||||||
|
series: [{
|
||||||
|
name: '达标率',
|
||||||
|
type: 'bar',
|
||||||
|
data: rates,
|
||||||
|
itemStyle: {
|
||||||
|
color: (params) => {
|
||||||
|
return params.value >= 90 ? '#67C23A' : params.value >= 70 ? '#E6A23C' : '#F56C6C'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCategoryChart() {
|
||||||
|
if (!categoryChartRef.value) return
|
||||||
|
if (!categoryChart) {
|
||||||
|
categoryChart = echarts.init(categoryChartRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryMap = {}
|
||||||
|
indicatorData.value.forEach(row => {
|
||||||
|
const cat = categoryText(row.indicatorCategory)
|
||||||
|
categoryMap[cat] = (categoryMap[cat] || 0) + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = Object.entries(categoryMap).map(([name, value]) => ({ name, value }))
|
||||||
|
|
||||||
|
categoryChart.setOption({
|
||||||
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
|
legend: { orient: 'vertical', left: 'left' },
|
||||||
|
series: [{
|
||||||
|
name: '指标类别',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: data,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function resetQuery() {
|
function resetQuery() {
|
||||||
q.value = {pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''}
|
q.value = {pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''}
|
||||||
loadData()
|
loadData()
|
||||||
@@ -407,5 +604,21 @@ async function handleCollect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => loadData())
|
const handleResize = () => {
|
||||||
|
trendChart?.resize()
|
||||||
|
deptChart?.resize()
|
||||||
|
categoryChart?.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
trendChart?.dispose()
|
||||||
|
deptChart?.dispose()
|
||||||
|
categoryChart?.dispose()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user