feat: 护理质量指标 + 病历质量统计页面增强

- 护理质量指标管理页面增强(统计卡片/新增/筛选/达标状态)
- 病历质量统计页面增强(运行质控/终末质控/缺陷记录/统计卡片)
- 质量API文件更新(添加getQualityStatistics接口)
This commit is contained in:
2026-06-07 19:54:56 +08:00
parent bd90c40c49
commit 8b099d94df
3 changed files with 346 additions and 47 deletions

View File

@@ -1,7 +1,8 @@
import request from '@/utils/request'
export function runtimeCheck(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/runtime-check/' + encounterId, method: 'post' }) }
export function terminalCheck(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/terminal-check/' + encounterId, method: 'post' }) }
export function getScores(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/score/' + encounterId, method: 'get' }) }
export function getDefects(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/defect/' + encounterId, method: 'get' }) }
export function getDefectStatistics() { return request({ url: '/healthlink-his/api/v1/emr-quality/defect-statistics', method: 'get' }) }
export function getCompletionRate() { return request({ url: '/healthlink-his/api/v1/emr-quality/completion-rate', method: 'get' }) }
export function runtimeCheck(encounterId) { return request({ url: '/api/v1/emr-quality/runtime-check/' + encounterId, method: 'post' }) }
export function terminalCheck(encounterId) { return request({ url: '/api/v1/emr-quality/terminal-check/' + encounterId, method: 'post' }) }
export function getScores(encounterId) { return request({ url: '/api/v1/emr-quality/score/' + encounterId, method: 'get' }) }
export function getDefects(encounterId) { return request({ url: '/api/v1/emr-quality/defect/' + encounterId, method: 'get' }) }
export function getDefectStatistics() { return request({ url: '/api/v1/emr-quality/defect-statistics', method: 'get' }) }
export function getCompletionRate() { return request({ url: '/api/v1/emr-quality/completion-rate', method: 'get' }) }
export function getQualityStatistics(params) { return request({ url: '/api/v1/emr-quality/defect-statistics', method: 'get', params }) }

View File

@@ -1,30 +1,175 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">护理质量指标</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="display:flex;gap:30px;text-align:center">
<div><div style="font-size:24px;font-weight:bold;color:#409eff">{{ summary.total||0 }}</div><div>总指标</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#67c23a">{{ summary.meetTarget||0 }}</div><div></div></div>
<div><div style="font-size:24px;font-weight:bold;color:#e6a23c">{{ summary.meetRate||0 }}%</div><div>达标率</div></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">护理质量指标管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增指</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</el-card>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增指标</el-button></div>
<el-table :data="indicatorData" border stripe>
<el-table-column prop="indicatorCode" label="编码" width="100"/>
<el-table-column prop="indicatorName" label="指标名称" min-width="180"/>
<el-table-column prop="indicatorCategory" label="类别" width="100"/>
<el-table-column prop="targetValue" label="目标" width="70" align="center"/>
<el-table-column prop="actualValue" label="实际" width="70" align="center"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.indicatorCategory" placeholder="指标类别" clearable style="width:140px">
<el-option label="基础护理" value="BASIC"/>
<el-option label="专科护理" value="SPECIALIZED"/>
<el-option label="护理安全" value="SAFETY"/>
<el-option label="护理文书" value="DOCUMENTATION"/>
<el-option label="消毒隔离" value="STERILIZATION"/>
</el-select>
<el-select v-model="q.departmentName" placeholder="科室" clearable style="width:140px">
<el-option label="内科" value="内科"/>
<el-option label="外科" value="外科"/>
<el-option label="妇产科" value="妇产科"/>
<el-option label="儿科" value="儿科"/>
<el-option label="ICU" value="ICU"/>
<el-option label="急诊科" value="急诊科"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="indicatorData" border stripe v-loading="loading">
<el-table-column prop="indicatorCode" label="指标编码" width="120"/>
<el-table-column prop="indicatorName" label="指标名称" min-width="180" show-overflow-tooltip/>
<el-table-column prop="indicatorCategory" label="类别" width="100" align="center">
<template #default="{row}">
<el-tag size="small">{{ categoryText(row.indicatorCategory) }}</el-tag>
</template>
</el-table-column>
<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: row.actualValue >= row.targetValue ? '#67C23A' : '#F56C6C', fontWeight:'bold'}">
{{ row.actualValue }}
</span>
</template>
</el-table-column>
<el-table-column label="达标状态" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.actualValue >= row.targetValue ? 'success' : 'danger'" size="small">
{{ row.actualValue >= row.targetValue ? '达标' : '未达标' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="departmentName" label="科室" width="100"/>
<el-table-column prop="statDate" label="统计日期" width="120"/>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增护理质量指标" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="指标编码" required><el-input v-model="formData.indicatorCode" placeholder="如: NQ001"/></el-form-item>
<el-form-item label="指标名称" required><el-input v-model="formData.indicatorName" placeholder="请输入指标名称"/></el-form-item>
<el-form-item label="指标类别" required>
<el-select v-model="formData.indicatorCategory" placeholder="请选择">
<el-option label="基础护理" value="BASIC"/>
<el-option label="专科护理" value="SPECIALIZED"/>
<el-option label="护理安全" value="SAFETY"/>
<el-option label="护理文书" value="DOCUMENTATION"/>
<el-option label="消毒隔离" value="STERILIZATION"/>
</el-select>
</el-form-item>
<el-form-item label="目标值"><el-input-number v-model="formData.targetValue" :min="0"/></el-form-item>
<el-form-item label="实际值"><el-input-number v-model="formData.actualValue" :min="0"/></el-form-item>
<el-form-item label="科室"><el-input v-model="formData.departmentName" placeholder="请输入科室"/></el-form-item>
<el-form-item label="统计日期"><el-date-picker v-model="formData.statDate" type="date" placeholder="选择日期"/></el-form-item>
<el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getQualityPage,addIndicator,getQualitySummary} from './api'
const indicatorData=ref([]),summary=ref({})
const showAdd=ref(false)
const loadData=async()=>{const [i,s]=await Promise.all([getQualityPage({pageNo:1,pageSize:50}),getQualitySummary()]);indicatorData.value=i.data?.records||[];summary.value=s.data||{}}
onMounted(()=>loadData())
import {getQualityPage, addIndicator, getQualitySummary} from './api'
const loading = ref(false)
const indicatorData = ref([])
const total = ref(0)
const showAdd = ref(false)
const summary = ref({})
const statCards = ref([
{label:'总指标', value:0, color:'#409eff'},
{label:'达标数', value:0, color:'#67c23a'},
{label:'未达标数', value:0, color:'#f56c6c'},
{label:'达标率', value:'0%', color:'#e6a23c'},
{label:'科室数', value:0, color:'#909399'},
{label:'平均达标率', value:'0%', color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''})
const formData = reactive({
indicatorCode:'', indicatorName:'', indicatorCategory:'BASIC',
targetValue:0, actualValue:0, departmentName:'', statDate:'', remark:''
})
function categoryText(c) {
return {BASIC:'基础护理',SPECIALIZED:'专科护理',SAFETY:'护理安全',DOCUMENTATION:'护理文书',STERILIZATION:'消毒隔离'}[c] || c
}
async function loadData() {
loading.value = true
try {
const [i, s] = await Promise.all([
getQualityPage(q.value),
getQualitySummary()
])
indicatorData.value = i.data?.records || []
total.value = i.data?.total || 0
summary.value = s.data || {}
// Calculate stats
let met = 0, unmet = 0, deptSet = new Set()
indicatorData.value.forEach(row => {
if (row.actualValue >= row.targetValue) met++
else unmet++
deptSet.add(row.departmentName)
})
statCards.value[0].value = total.value
statCards.value[1].value = met
statCards.value[2].value = unmet
statCards.value[3].value = total.value > 0 ? Math.round(met * 100 / total.value) + '%' : '0%'
statCards.value[4].value = deptSet.size
statCards.value[5].value = summary.value.meetRate ? summary.value.meetRate + '%' : '0%'
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''}
loadData()
}
async function submitForm() {
await addIndicator(formData)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -1,28 +1,181 @@
<template>
<div class="app-container">
<el-form :model="q" :inline="true"><el-form-item label="就诊号"><el-input v-model="q.encounterId" clearable /></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item></el-form>
<el-row :gutter="20" class="mb8">
<el-col :span="12"><el-card shadow="hover"><el-statistic title="运行质控结果" :value="runtimeResult.status || '-'" /></el-card></el-col>
<el-col :span="12"><el-card shadow="hover"><el-statistic title="终末质控评分" :value="terminalResult.score || 0" suffix="分" /></el-card></el-col>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">病历质量统计</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="mt8"><template #header>缺陷记录</template>
<el-table :data="defects" size="small">
<el-table-column label="缺陷类型" prop="defectType" width="120" />
<el-table-column label="缺陷项" prop="defectItem" width="150" />
<el-table-column label="严重程度" prop="severity" width="100">
<template #default="s"><el-tag :type="s.row.severity==='CRITICAL'?'danger':s.row.severity==='MAJOR'?'warning':'info'">{{ s.row.severity }}</el-tag></template>
</el-table-column>
<el-table-column label="整改状态" prop="rectifyStatus" width="100" />
</el-table></el-card>
<!-- 查询表单 -->
<el-form :model="q" :inline="true" style="margin-bottom:16px">
<el-form-item label="就诊号"><el-input v-model="q.encounterId" clearable placeholder="请输入就诊号" style="width:180px"/></el-form-item>
<el-form-item label="科室">
<el-select v-model="q.departmentName" clearable placeholder="请选择科室" style="width:140px">
<el-option label="内科" value="内科"/>
<el-option label="外科" value="外科"/>
<el-option label="妇产科" value="妇产科"/>
<el-option label="儿科" value="儿科"/>
<el-option label="ICU" value="ICU"/>
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="q.dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" style="width:240px"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 质控结果 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="12">
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between;align-items:center"><span>运行质控结果</span><el-tag :type="runtimeResult.status==='PASS'?'success':'danger'" size="small">{{ runtimeResult.status || '待检测' }}</el-tag></div></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="质控时间">{{ runtimeResult.checkTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="缺陷数">{{ runtimeResult.defectCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="严重缺陷">{{ runtimeResult.criticalDefects || 0 }}</el-descriptions-item>
<el-descriptions-item label="一般缺陷">{{ runtimeResult.normalDefects || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between;align-items:center"><span>终末质控评分</span><el-tag :type="terminalResult.score>=80?'success':terminalResult.score>=60?'warning':'danger'" size="large">{{ terminalResult.score || 0 }}</el-tag></div></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="评分时间">{{ terminalResult.checkTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="评分等级">{{ scoreLevel(terminalResult.score) }}</el-descriptions-item>
<el-descriptions-item label="甲级病历">{{ terminalResult.gradeA || 0 }}</el-descriptions-item>
<el-descriptions-item label="乙级病历">{{ terminalResult.gradeB || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<!-- 缺陷记录 -->
<el-card shadow="never">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<span>缺陷记录 ({{ defects.length }})</span>
<el-select v-model="defectFilter" clearable placeholder="筛选缺陷类型" style="width:140px" @change="filterDefects">
<el-option label="全部" value=""/>
<el-option label="严重缺陷" value="CRITICAL"/>
<el-option label="主要缺陷" value="MAJOR"/>
<el-option label="一般缺陷" value="MINOR"/>
</el-select>
</div>
</template>
<el-table :data="filteredDefects" border stripe>
<el-table-column label="缺陷类型" prop="defectType" width="140"/>
<el-table-column label="缺陷项" prop="defectItem" min-width="180" show-overflow-tooltip/>
<el-table-column label="严重程度" prop="severity" width="110" align="center">
<template #default="s">
<el-tag :type="s.row.severity==='CRITICAL'?'danger':s.row.severity==='MAJOR'?'warning':'info'" size="small">
{{ s.row.severity==='CRITICAL'?'严重缺陷':s.row.severity==='MAJOR'?'主要缺陷':'一般缺陷' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="整改状态" prop="rectifyStatus" width="100" align="center">
<template #default="s">
<el-tag :type="s.row.rectifyStatus==='RECTIFIED'?'success':s.row.rectifyStatus==='RECTIFYING'?'warning':'info'" size="small">
{{ s.row.rectifyStatus==='RECTIFIED'?'已整改':s.row.rectifyStatus==='RECTIFYING'?'整改中':'待整改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="发现时间" prop="discoverTime" width="160"/>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'; import { runtimeCheck, terminalCheck, getDefects } from '@/api/quality'
const q = reactive({ encounterId: '' }); const runtimeResult = ref({}); const terminalResult = ref({}); const defects = ref([])
const loadData = async () => {
if (!q.encounterId) return
const [r1, r2, r3] = await Promise.all([runtimeCheck(q.encounterId), terminalCheck(q.encounterId), getDefects(q.encounterId)])
runtimeResult.value = r1.data || {}; terminalResult.value = r2.data || {}; defects.value = r3.data || []
import {ref, reactive, computed, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {runtimeCheck, terminalCheck, getDefects, getQualityStatistics} from '@/api/quality'
const q = reactive({encounterId:'', departmentName:'', dateRange:null})
const runtimeResult = ref({})
const terminalResult = ref({})
const defects = ref([])
const defectFilter = ref('')
const qualityStats = ref({})
const statCards = ref([
{label:'总病历数', value:0, color:'#409eff'},
{label:'甲级病历', value:0, color:'#67c23a'},
{label:'乙级病历', value:0, color:'#e6a23c'},
{label:'丙级病历', value:0, color:'#f56c6c'},
{label:'缺陷总数', value:0, color:'#909399'},
{label:'甲级率', value:'0%', color:'#409eff'}
])
const filteredDefects = computed(() => {
if (!defectFilter.value) return defects.value
return defects.value.filter(d => d.severity === defectFilter.value)
})
function scoreLevel(score) {
if (!score) return '-'
if (score >= 90) return '甲级'
if (score >= 75) return '乙级'
if (score >= 60) return '丙级'
return '不合格'
}
async function loadData() {
try {
if (q.encounterId) {
const [r1, r2, r3] = await Promise.all([
runtimeCheck(q.encounterId),
terminalCheck(q.encounterId),
getDefects(q.encounterId)
])
runtimeResult.value = r1.data || {}
terminalResult.value = r2.data || {}
defects.value = r3.data || []
}
// Load overall statistics
try {
const r4 = await getQualityStatistics(q)
qualityStats.value = r4.data || {}
statCards.value[0].value = qualityStats.value.totalRecords || 0
statCards.value[1].value = qualityStats.value.gradeA || 0
statCards.value[2].value = qualityStats.value.gradeB || 0
statCards.value[3].value = qualityStats.value.gradeC || 0
statCards.value[4].value = qualityStats.value.totalDefects || 0
statCards.value[5].value = (qualityStats.value.gradeARate || 0) + '%'
} catch(e) {}
} catch(e) { ElMessage.error('查询失败') }
}
function resetQuery() {
q.encounterId = ''
q.departmentName = ''
q.dateRange = null
runtimeResult.value = {}
terminalResult.value = {}
defects.value = []
loadData()
}
function filterDefects() {}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>