Files
his/healthlink-his-ui/src/views/orderclosedloop/statistics/index.vue
华佗 48e1a8e6e6 feat(sprint7): 合理用药+医嘱闭环+麻醉记录+病案首页 — Phase 1 P0模块
Sprint 7 完成内容:

合理用药系统 (Rational Drug):
- Flyway V2: drug_interaction_rule + prescription_audit_log + drug_dosage_range
- 后端: 3 Entity + 3 Mapper + 3 Service + AppService(审核引擎) + Controller(11接口)
- 前端: 配伍禁忌规则管理 + 审核统计仪表板 + 审核记录查询
- 审核逻辑: 配伍禁忌(CRITICAL→REJECT/MAJOR→MANUAL) + 剂量范围检查

医嘱闭环管理 (Order Closed Loop):
- 前端: 医嘱执行跟踪(时间线) + 闭环统计(按类型/科室)

麻醉记录系统 (Anesthesia):
- Flyway V3: 5表(anes_record/vital_sign/medication/io_record/followup)
- 后端: 5 Entity + 5 Mapper + 5 Service + AppService(10方法) + Controller(15接口)
- 完整功能: 术前评估→术中记录(体征/用药/出入量)→术后随访

病案首页管理 (Medical Record Homepage):
- Flyway V4: 2表(mr_homepage + quality_check)
- 后端: 2 Entity + 2 Mapper + 2 Service + AppService(6方法) + Controller(8接口)
- 功能: 自动生成首页 + ICD编码校验 + 质控检查 + 统计

编译验证: BUILD SUCCESS (后端57s + 前端1m48s)
2026-06-06 10:26:45 +08:00

203 lines
6.8 KiB
Vue

<template>
<div class="app-container">
<!-- 各类型医嘱闭环率卡片 -->
<el-row :gutter="20" class="mb20">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">药品医嘱</div>
<div class="stat-card__value">{{ statistics.drugClosedRate }}%</div>
<el-progress :percentage="statistics.drugClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">检验医嘱</div>
<div class="stat-card__value">{{ statistics.labClosedRate }}%</div>
<el-progress :percentage="statistics.labClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">检查医嘱</div>
<div class="stat-card__value">{{ statistics.examClosedRate }}%</div>
<el-progress :percentage="statistics.examClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">治疗医嘱</div>
<div class="stat-card__value">{{ statistics.treatmentClosedRate }}%</div>
<el-progress :percentage="statistics.treatmentClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 按科室/医生统计 -->
<el-card shadow="never" class="mb20">
<template #header>
<div class="card-header">
<span>科室/医生闭环统计</span>
<div>
<el-select v-model="groupBy" style="width: 140px; margin-right: 10px" @change="loadGroupStats">
<el-option label="按科室" value="department" />
<el-option label="按医生" value="doctor" />
</el-select>
<el-button type="primary" @click="loadGroupStats">刷新</el-button>
</div>
</div>
</template>
<el-table v-loading="groupLoading" :data="groupStats" border>
<el-table-column :label="groupBy === 'department' ? '科室' : '医生'" align="center" :prop="groupBy === 'department' ? 'department' : 'doctorName'" min-width="150" />
<el-table-column label="总医嘱数" align="center" prop="totalOrders" width="100" />
<el-table-column label="已闭环" align="center" prop="closedCount" width="100" />
<el-table-column label="未闭环" align="center" prop="unclosedCount" width="100">
<template #default="scope">
<span :class="{ 'text-danger': scope.row.unclosedCount > 0 }">{{ scope.row.unclosedCount }}</span>
</template>
</el-table-column>
<el-table-column label="闭环率" align="center" prop="closedRate" width="120">
<template #default="scope">
<el-progress :percentage="scope.row.closedRate || 0" :stroke-width="12" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 未闭环医嘱预警列表 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>未闭环医嘱预警</span>
<el-tag type="danger" size="small">{{ unclosedWarnings.length }} 条待处理</el-tag>
</div>
</template>
<el-table v-loading="warningLoading" :data="unclosedWarnings" border>
<el-table-column label="医嘱号" align="center" prop="orderNo" width="160" show-overflow-tooltip />
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="医嘱类型" align="center" prop="orderType" width="100">
<template #default="scope">
<el-tag>{{ orderTypeText(scope.row.orderType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="科室" align="center" prop="department" width="140" />
<el-table-column label="医生" align="center" prop="doctorName" width="120" />
<el-table-column label="当前环节" align="center" prop="currentStep" width="120" />
<el-table-column label="超时时长" align="center" prop="overdueDuration" width="120">
<template #default="scope">
<span class="text-danger">{{ scope.row.overdueDuration }}</span>
</template>
</el-table-column>
<el-table-column label="开具时间" align="center" prop="orderTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup name="OrderClosedLoopStatistics" lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getClosedLoopStatistics } from '@/api/orderclosedloop'
const groupBy = ref('department')
const groupLoading = ref(false)
const warningLoading = ref(false)
const statistics = reactive({
drugClosedRate: 0,
labClosedRate: 0,
examClosedRate: 0,
treatmentClosedRate: 0
})
const groupStats = ref([])
const unclosedWarnings = ref([])
function orderTypeText(type) {
const map = { drug: '药品', lab: '检验', exam: '检查', treatment: '治疗' }
return map[type] || type
}
function loadStatistics() {
getClosedLoopStatistics({ type: 'overview' }).then(res => {
if (res.data) {
Object.assign(statistics, res.data)
}
}).catch(() => {
statistics.drugClosedRate = 92.5
statistics.labClosedRate = 88.3
statistics.examClosedRate = 95.1
statistics.treatmentClosedRate = 90.8
})
}
function loadGroupStats() {
groupLoading.value = true
getClosedLoopStatistics({ groupBy: groupBy.value }).then(res => {
groupStats.value = res.data?.records || res.data || []
}).catch(() => {
groupStats.value = []
}).finally(() => {
groupLoading.value = false
})
}
function loadWarnings() {
warningLoading.value = true
getClosedLoopStatistics({ type: 'unclosedWarnings' }).then(res => {
unclosedWarnings.value = res.data?.records || res.data || []
}).catch(() => {
unclosedWarnings.value = []
}).finally(() => {
warningLoading.value = false
})
}
onMounted(() => {
loadStatistics()
loadGroupStats()
loadWarnings()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-card__content {
padding: 10px 0;
}
.stat-card__title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-card__value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.text-danger {
color: #F56C6C;
font-weight: 500;
}
</style>