feat(emr): 病历时效监控

This commit is contained in:
2026-06-17 13:47:02 +08:00
parent 9673c0ed80
commit 09e43e4b8c
5 changed files with 399 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import java.util.List;
import java.util.Map;
public interface IEmrTimelinessAppService {
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
}

View File

@@ -0,0 +1,84 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import com.healthlink.his.emr.service.IEmrTimelinessService;
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class EmrTimelinessAppServiceImpl implements IEmrTimelinessAppService {
@Resource
private IEmrTimelinessService emrTimelinessService;
@Override
@Transactional(rollbackFor = Exception.class)
public EmrTimelinessStatisticsDto checkTimeliness(Long encounterId) {
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
if (encounterId != null) {
wrapper.eq(EmrTimeliness::getEncounterId, encounterId);
}
wrapper.eq(EmrTimeliness::getStatus, "PENDING");
List<EmrTimeliness> pendingList = emrTimelinessService.list(wrapper);
Date now = new Date();
int overdueCount = 0;
for (EmrTimeliness record : pendingList) {
if (record.getDeadlineTime() != null && now.after(record.getDeadlineTime())) {
record.setStatus("OVERDUE");
emrTimelinessService.updateById(record);
overdueCount++;
}
}
EmrTimelinessStatisticsDto stats = new EmrTimelinessStatisticsDto();
LambdaQueryWrapper<EmrTimeliness> countWrapper = new LambdaQueryWrapper<>();
if (encounterId != null) {
countWrapper.eq(EmrTimeliness::getEncounterId, encounterId);
}
long total = emrTimelinessService.count(countWrapper);
countWrapper.eq(EmrTimeliness::getStatus, "COMPLETED");
long completed = emrTimelinessService.count(countWrapper);
countWrapper.eq(EmrTimeliness::getStatus, "OVERDUE");
long overdue = emrTimelinessService.count(countWrapper);
long pending = total - completed - overdue;
double rate = total > 0 ? Math.round(completed * 10000.0 / total) / 100.0 : 0;
stats.setTotalCount(total);
stats.setCompletedCount(completed);
stats.setOverdueCount(overdue);
stats.setPendingCount(pending);
stats.setCompletionRate(rate);
return stats;
}
@Override
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(status), EmrTimeliness::getStatus, status);
wrapper.eq(StringUtils.hasText(emrType), EmrTimeliness::getEmrType, emrType);
wrapper.eq(StringUtils.hasText(departmentName), EmrTimeliness::getDepartmentName, departmentName);
wrapper.orderByAsc(EmrTimeliness::getDeadlineTime);
Page<EmrTimeliness> page = emrTimelinessService.page(new Page<>(pageNum, pageSize), wrapper);
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", page.getTotal());
result.put("rows", page.getRecords());
return result;
}
}

View File

@@ -0,0 +1,44 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/emr/timeliness")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历时限监控")
public class EmrTimelinessController {
private final IEmrTimelinessAppService emrTimelinessAppService;
@PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "执行病历时限检查")
public R<EmrTimelinessStatisticsDto> checkTimeliness(
@RequestParam(value = "encounterId", required = false) Long encounterId) {
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
}
@GetMapping("/alerts")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取病历时限提醒列表")
public R<Map<String, Object>> getTimelinessAlerts(
@RequestParam(value = "emrType", required = false) String emrType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "departmentName", required = false) String departmentName,
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
return R.ok(emrTimelinessAppService.getTimelinessAlerts(emrType, status, departmentName, pageNum, pageSize));
}
}

View File

@@ -16,3 +16,6 @@ export function compareEmrVersions(id1, id2) { return request({ url: "/emr/versi
export function checkCompleteness(emrId, encounterId) { return request({ url: "/emr/completeness/check", method: "post", params: { emrId, encounterId } }) }
export function getCompletenessResults(emrId) { return request({ url: "/emr/completeness/results/" + emrId, method: "get" }) }
export function checkTimeliness(encounterId) { return request({ url: "/emr/timeliness/check", method: "post", params: { encounterId } }) }
export function getTimelinessAlerts(params) { return request({ url: "/emr/timeliness/alerts", method: "get", params }) }

View File

@@ -0,0 +1,254 @@
<template>
<div class="emr-timeliness-monitor">
<el-row :gutter="16" class="stats-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value total">{{ stats.totalCount || 0 }}</div>
<div class="stat-label">病历总数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value completed">{{ stats.completedCount || 0 }}</div>
<div class="stat-label">已完成</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value pending">{{ stats.pendingCount || 0 }}</div>
<div class="stat-label">待完成</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value overdue">{{ stats.overdueCount || 0 }}</div>
<div class="stat-label">已超时</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" class="filter-card">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="病历类型">
<el-select v-model="queryParams.emrType" clearable placeholder="全部" style="width: 160px">
<el-option label="入院记录" value="ADMISSION" />
<el-option label="首次病程" value="FIRST_COURSE" />
<el-option label="日常病程" value="DAILY_COURSE" />
<el-option label="出院记录" value="DISCHARGE" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" clearable placeholder="全部" style="width: 140px">
<el-option label="待完成" value="PENDING" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已超时" value="OVERDUE" />
</el-select>
</el-form-item>
<el-form-item label="科室">
<el-input v-model="queryParams.departmentName" clearable placeholder="输入科室名" style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
搜索
</el-button>
<el-button @click="handleReset">
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="table-card">
<template #header>
<div class="card-header">
<span>时限提醒列表</span>
<el-button type="primary" size="small" @click="handleCheckAll">
执行检查
</el-button>
</div>
</template>
<el-table v-loading="loading" :data="alertList" stripe border size="default">
<el-table-column prop="emrType" label="病历类型" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getEmrTypeTag(row.emrType)">
{{ getEmrTypeLabel(row.emrType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="patientId" label="患者ID" width="100" align="center" />
<el-table-column prop="doctorName" label="主治医生" width="110" align="center" />
<el-table-column prop="departmentName" label="科室" width="130" align="center" />
<el-table-column prop="requiredHours" label="时限(小时)" width="100" align="center" />
<el-table-column prop="deadlineTime" label="截止时间" width="170" align="center" />
<el-table-column prop="actualCompleteTime" label="完成时间" width="170" align="center">
<template #default="{ row }">
{{ row.actualCompleteTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getStatusTag(row.status)">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="loadAlerts"
@current-change="loadAlerts"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { checkTimeliness, getTimelinessAlerts } from '@/api/emr'
const loading = ref(false)
const stats = ref({})
const alertList = ref([])
const total = ref(0)
const queryParams = reactive({
emrType: '',
status: '',
departmentName: '',
pageNum: 1,
pageSize: 20
})
const EMR_TYPE_MAP = {
ADMISSION: '入院记录',
FIRST_COURSE: '首次病程',
DAILY_COURSE: '日常病程',
DISCHARGE: '出院记录'
}
const EMR_TYPE_TAG = {
ADMISSION: 'primary',
FIRST_COURSE: 'success',
DAILY_COURSE: 'warning',
DISCHARGE: 'info'
}
const STATUS_MAP = {
PENDING: '待完成',
COMPLETED: '已完成',
OVERDUE: '已超时'
}
const STATUS_TAG = {
PENDING: 'warning',
COMPLETED: 'success',
OVERDUE: 'danger'
}
function getEmrTypeLabel(type) { return EMR_TYPE_MAP[type] || type }
function getEmrTypeTag(type) { return EMR_TYPE_TAG[type] || '' }
function getStatusLabel(s) { return STATUS_MAP[s] || s }
function getStatusTag(s) { return STATUS_TAG[s] || '' }
function loadStats() {
checkTimeliness(null).then(res => {
stats.value = res.data || {}
})
}
function loadAlerts() {
loading.value = true
const params = { ...queryParams }
if (!params.emrType) delete params.emrType
if (!params.status) delete params.status
if (!params.departmentName) delete params.departmentName
getTimelinessAlerts(params).then(res => {
alertList.value = res.data?.rows || []
total.value = res.data?.total || 0
}).catch(() => {
ElMessage.error('查询时限提醒失败')
}).finally(() => {
loading.value = false
})
}
function handleSearch() {
queryParams.pageNum = 1
loadAlerts()
}
function handleReset() {
queryParams.emrType = ''
queryParams.status = ''
queryParams.departmentName = ''
queryParams.pageNum = 1
loadAlerts()
}
function handleCheckAll() {
loading.value = true
checkTimeliness(null).then(res => {
stats.value = res.data || {}
ElMessage.success('时限检查完成')
loadAlerts()
}).catch(() => {
ElMessage.error('执行检查失败')
}).finally(() => {
loading.value = false
})
}
onMounted(() => {
loadStats()
loadAlerts()
})
</script>
<style scoped>
.emr-timeliness-monitor {
padding: 16px;
}
.stats-row {
margin-bottom: 16px;
}
.stat-card {
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1.4;
}
.stat-value.total { color: #409eff; }
.stat-value.completed { color: #67c23a; }
.stat-value.pending { color: #e6a23c; }
.stat-value.overdue { color: #f56c6c; }
.stat-label {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.filter-card {
margin-bottom: 16px;
}
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.table-card .el-pagination {
margin-top: 16px;
justify-content: flex-end;
}
</style>