feat(V36): Infection Module — 完整实现院感管理8大子模块

后端修复:
- InfectionController: 修复HandHygiene/EnvironmentalMonitor字段引用错误
- InfectionAppServiceImpl: delFlag→deleteFlag迁移至HisBaseEntity
- HirInfectionCase: 移除冗余delFlag,使用HisBaseEntity.deleteFlag
- HirOccupationalExposure: 添加@TableField注解,修复hiv_test_3month列名
- TargetedSurveillance: surveillanceType Integer→String(匹配DB)

数据库修复:
- 8张表统一delete_flag/create_by/create_time/update_by/update_time/tenant_id
- 移除所有多余del_flag列
- 放宽NOT NULL约束(encounter_id/patient_id/staff_id等)

前端: 8个完整页面(case/hygiene/environment/antibiotic-usage/resistant/exposure/warning/surveillance)

测试: 19/19 API接口全部通过(增删改查+统计)
This commit is contained in:
2026-06-07 12:21:10 +08:00
parent 21dd790dd9
commit bfa33f6efe
20 changed files with 632 additions and 75 deletions

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/antibiotic-usage/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/antibiotic-usage/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/antibiotic-usage/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/case/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/case/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/case/delete/'+id,method:'delete'})}

View File

@@ -1,30 +1,42 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="院感病例" :value="stats.totalCases || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="待审核" :value="stats.reportedCases || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已审核" :value="stats.reviewedCases || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="抗菌使用" :value="stats.antibioticUsages || 0" /></el-card></el-col>
</el-row>
<el-form :model="q" :inline="true"><el-form-item label="状态">
<el-select v-model="q.status" clearable><el-option label="全部" value="" /><el-option label="待审核" value="REPORTED" /><el-option label="已审核" value="REVIEWED" /></el-select>
</el-form-item><el-form-item><el-button type="primary" @click="getList">搜索</el-button></el-form-item></el-form>
<el-table v-loading="loading" :data="list">
<el-table-column label="患者" prop="patientName" width="100" />
<el-table-column label="感染类型" prop="infectionType" width="120" />
<el-table-column label="感染部位" prop="infectionSite" width="120" />
<el-table-column label="病原体" prop="pathogen" width="120" />
<el-table-column label="报告时间" prop="reportTime" width="170" />
<el-table-column label="状态" prop="status" width="100">
<template #default="s"><el-tag :type="s.row.status==='REPORTED'?'warning':'success'">{{ s.row.status==='REPORTED'?'待审核':'已审核' }}</el-tag></template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">院感病例监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:130px"/>
<el-select v-model="q.infectionType" placeholder="感染类型" clearable style="width:110px">
<el-option label="医院感染" value="NOSOCOMIAL"/><el-option label="社区感染" value="COMMUNITY"/>
</el-select>
<el-select v-model="q.status" placeholder="状态" clearable style="width:100px">
<el-option label="已上报" value="REPORTED"/><el-option label="已确认" value="CONFIRMED"/><el-option label="已拒绝" value="REJECTED"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="infectionType" label="类型" width="90" align="center">
<template #default="{row}"><el-tag :type="row.infectionType==='NOSOCOMIAL'?'danger':''" size="small">{{ {NOSOCOMIAL:'医院感染',COMMUNITY:'社区感染'}[row.infectionType]||row.infectionType }}</el-tag></template>
</el-table-column>
<el-table-column prop="infectionSite" label="感染部位" width="110"/>
<el-table-column prop="pathogen" label="病原体" width="120"/>
<el-table-column prop="reporterName" label="上报人" width="90"/>
<el-table-column prop="reportTime" label="上报时间" width="160"/>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{row}">
<el-tag :type="row.status==='CONFIRMED'?'success':row.status==='REJECTED'?'danger':'warning'" size="small">
{{ {REPORTED:'已上报',CONFIRMED:'已确认',REJECTED:'已拒绝'}[row.status]||row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewResult" label="审核意见" width="100"/>
</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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'; import { getCaseList, getStatistics } from '@/api/infection'
const loading = ref(false); const list = ref([]); const stats = ref({}); const q = reactive({ status: '' })
const getList = async () => { loading.value = true; const r = await getCaseList({ status: q.status || undefined }); list.value = r.data || []; loading.value = false }
const loadStats = async () => { const r = await getStatistics(); stats.value = r.data || {} }
onMounted(() => { getList(); loadStats() })
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,patientName:'',infectionType:'',status:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/environment/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/environment/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/environment/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,32 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">环境监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.sampleType" placeholder="采样类型" clearable style="width:110px">
<el-option label="空气" value="AIR"/><el-option label="物表" value="SURFACE"/><el-option label="手采样" value="HAND"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="sampleType" label="类型" width="80" align="center">
<template #default="{row}"><el-tag size="small">{{ {AIR:'空气',SURFACE:'物表',HAND:'手采样'}[row.sampleType]||row.sampleType }}</el-tag></template>
</el-table-column>
<el-table-column prop="sampleLocation" label="采样地点" min-width="150"/>
<el-table-column prop="sampleDate" label="采样日期" width="110"/>
<el-table-column prop="monitorResult" label="结果" width="80" align="center">
<template #default="{row}"><el-tag :type="row.monitorResult==='QUALIFIED'?'success':'danger'" size="small">{{ {QUALIFIED:'合格',UNQUALIFIED:'不合格'}[row.monitorResult]||row.monitorResult }}</el-tag></template>
</el-table-column>
<el-table-column prop="bacterialCount" label="菌落数" width="80" align="center"/>
<el-table-column prop="standardLimit" label="标准限值" width="80" align="center"/>
</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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,sampleType:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/exposure/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/exposure/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/exposure/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,27 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">职业暴露管理</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.staffName" placeholder="工作人员" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="staffName" label="工作人员" width="100"/>
<el-table-column prop="department" label="科室" width="100"/>
<el-table-column prop="exposureType" label="暴露类型" width="100"/>
<el-table-column prop="exposureSource" label="暴露源" width="120"/>
<el-table-column prop="exposureDate" label="暴露日期" width="110"/>
<el-table-column prop="immediateProcessing" label="即刻处理" min-width="150" show-overflow-tooltip/>
<el-table-column prop="followUpPlan" 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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,staffName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/hygiene/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/hygiene/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/hygiene/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,31 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">手卫生监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.deptName" placeholder="科室" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="deptName" label="科室" width="120"/>
<el-table-column prop="observerName" label="观察人" width="90"/>
<el-table-column prop="observationDate" label="观察日期" width="110"/>
<el-table-column prop="totalOpportunities" label="手卫生时机" width="100" align="center"/>
<el-table-column prop="actualCompliance" label="实际执行" width="80" align="center"/>
<el-table-column prop="complianceRate" label="依从率" width="90" align="center">
<template #default="{row}"><el-progress :percentage="row.complianceRate||0" :stroke-width="14" :color="row.complianceRate>=90?'#67C23A':'#F56C6C'"/></template>
</el-table-column>
<el-table-column prop="complianceStatus" label="达标" width="70" align="center">
<template #default="{row}"><el-tag :type="row.complianceStatus==='COMPLIANT'?'success':'danger'" size="small">{{ {COMPLIANT:'达标',NON_COMPLIANT:'未达标'}[row.complianceStatus]||row.complianceStatus }}</el-tag></template>
</el-table-column>
</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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,deptName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/resistant/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/resistant/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/resistant/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,28 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">多重耐药菌管理</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="pathogenName" label="耐药菌" width="130"/>
<el-table-column prop="resistantType" label="耐药类型" width="120"/>
<el-table-column prop="sampleSource" label="标本来源" width="110"/>
<el-table-column prop="isolationStatus" label="隔离" width="80" align="center">
<template #default="{row}"><el-tag :type="row.isolationStatus==='ISOLATED'?'danger':'info'" size="small">{{ {ISOLATED:'已隔离',NOT_ISOLATED:'未隔离',RELEASED:'已解除'}[row.isolationStatus]||row.isolationStatus }}</el-tag></template>
</el-table-column>
<el-table-column prop="reportTime" label="上报时间" width="160"/>
</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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,patientName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/surveillance/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/surveillance/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/surveillance/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,33 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">目标性监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.surveillanceType" placeholder="监测类型" clearable style="width:120px">
<el-option label="ICU感染" value="ICU"/><el-option label="手术部位" value="SSI"/><el-option label="导管相关" value="CLABSI"/>
<el-option label="呼吸机相关" value="VAP"/><el-option label="导尿管相关" value="CAUTI"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="surveillanceType" label="类型" width="100" align="center">
<template #default="{row}"><el-tag size="small">{{ {ICU:'ICU感染',SSI:'手术部位',CLABSI:'导管相关',VAP:'呼吸机相关',CAUTI:'导尿管相关'}[row.surveillanceType]||row.surveillanceType }}</el-tag></template>
</el-table-column>
<el-table-column prop="totalPatients" label="监测人数" width="90" align="center"/>
<el-table-column prop="infectionCount" label="感染数" width="70" align="center"/>
<el-table-column prop="infectionRate" label="感染率" width="80" align="center">
<template #default="{row}"><span :style="{color:row.infectionRate>5?'#F56C6C':'#67C23A',fontWeight:'bold'}">{{ row.infectionRate }}%</span></template>
</el-table-column>
<el-table-column prop="surveillancePeriod" label="监测周期" width="150"/>
<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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,surveillanceType:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/warning/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/warning/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/warning/delete/'+id,method:'delete'})}

View File

@@ -0,0 +1,34 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">疫情预警</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.warningLevel" placeholder="预警级别" clearable style="width:100px">
<el-option label="一级" value="LEVEL1"/><el-option label="二级" value="LEVEL2"/><el-option label="三级" value="LEVEL3"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="diseaseName" label="疾病" width="130"/>
<el-table-column prop="warningLevel" label="级别" width="70" align="center">
<template #default="{row}">
<el-tag :type="row.warningLevel==='LEVEL1'?'danger':row.warningLevel==='LEVEL2'?'warning':'info'" size="small" effect="dark">
{{ {LEVEL1:'一级',LEVEL2:'二级',LEVEL3:'三级'}[row.warningLevel]||row.warningLevel }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="caseCount" label="病例数" width="70" align="center"/>
<el-table-column prop="thresholdCount" label="阈值" width="60" align="center"/>
<el-table-column prop="affectedArea" label="影响区域" min-width="150" show-overflow-tooltip/>
<el-table-column prop="createTime" label="预警时间" width="160"/>
</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,prev,pager,next" @current-change="loadData"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,warningLevel:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
</script>