feat(emr): 病历时效监控
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 }) }
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user