feat(infection): 院感监测细化 — 科室感染率+感染趋势

- 新增 IInfectionDetailAppService + InfectionDetailAppServiceImpl
- 新增 InfectionDetailController (GET /rate-by-dept, GET /trend)
- 新增 V74 迁移脚本: hir_infection_case 加 department_id
- 前端 InfectionDetailStats.vue 统计面板+趋势表格
This commit is contained in:
2026-06-18 17:24:56 +08:00
parent 2702258e34
commit 0752f53966
2 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getInfectionRateByDept(params) {
return request({ url: '/infection-detail/rate-by-dept', method: 'get', params })
}
export function getInfectionTrend(params) {
return request({ url: '/infection-detail/trend', method: 'get', params })
}

View File

@@ -0,0 +1,152 @@
<template>
<div class="infection-detail-container">
<div class="page-header">
<span class="tab-title">院感监测统计</span>
</div>
<el-card shadow="never" style="margin-bottom: 16px">
<template #header>
<span>查询条件</span>
</template>
<el-form :model="queryParams" inline>
<el-form-item label="科室">
<el-input v-model="queryParams.deptId" placeholder="科室ID" clearable style="width: 160px" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="queryParams.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="queryParams.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData" :loading="loading">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #409eff">{{ rateData.totalCases || 0 }}</div>
<div class="stat-label">总病例数</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #e6a23c">{{ rateData.reportedCases || 0 }}</div>
<div class="stat-label">已上报</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #67c23a">{{ rateData.confirmedCases || 0 }}</div>
<div class="stat-label">已确认</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #f56c6c">{{ rateData.infectionRate || 0 }}%</div>
<div class="stat-label">感染率</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :span="12">
<el-card shadow="never">
<template #header><span>按感染类型分布</span></template>
<el-table :data="typeList" border stripe size="small">
<el-table-column prop="type" label="感染类型" />
<el-table-column prop="count" label="病例数" width="100" />
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span>按感染部位分布</span></template>
<el-table :data="siteList" border stripe size="small">
<el-table-column prop="site" label="感染部位" />
<el-table-column prop="count" label="病例数" width="100" />
</el-table>
</el-card>
</el-col>
</el-row>
<el-card shadow="never">
<template #header><span>感染趋势</span></template>
<el-table :data="trendData" v-loading="loading" border stripe style="width: 100%">
<el-table-column prop="date" label="日期" width="120" />
<el-table-column prop="total" label="总计" width="80" />
<el-table-column prop="REPORTED" label="已上报" width="80" />
<el-table-column prop="CONFIRMED" label="已确认" width="80" />
<el-table-column prop="REJECTED" label="已驳回" width="80" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getInfectionRateByDept, getInfectionTrend } from '@/api/infection/detail'
const loading = ref(false)
const queryParams = reactive({
deptId: '',
startDate: '',
endDate: ''
})
const rateData = ref({})
const trendData = ref([])
const typeList = computed(() => {
const byType = rateData.value.byType || {}
return Object.entries(byType).map(([type, count]) => ({ type, count }))
})
const siteList = computed(() => {
const bySite = rateData.value.bySite || {}
return Object.entries(bySite).map(([site, count]) => ({ site, count }))
})
const loadData = async () => {
loading.value = true
try {
const params = {}
if (queryParams.deptId) params.deptId = queryParams.deptId
if (queryParams.startDate) params.startDate = queryParams.startDate
if (queryParams.endDate) params.endDate = queryParams.endDate
const [rateRes, trendRes] = await Promise.all([
getInfectionRateByDept(params),
getInfectionTrend({ startDate: queryParams.startDate, endDate: queryParams.endDate })
])
rateData.value = rateRes.data || {}
trendData.value = trendRes.data || []
} catch (e) {
ElMessage.error('加载失败: ' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
onMounted(() => loadData())
</script>
<style scoped>
.infection-detail-container { padding: 16px; }
.page-header { margin-bottom: 16px; }
.tab-title { font-size: 18px; font-weight: bold; }
.stat-card { text-align: center; padding: 12px 0; }
.stat-value { font-size: 28px; font-weight: bold; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
</style>