feat(emr): 优化病历检索页面
- 添加患者基本信息:性别、年龄、电话、身份证号 - 添加就诊号字段 - 重写前端页面,参考行业通用设计 - 支持点击查看病历详情 - 同步时自动填充患者和医生信息
This commit is contained in:
@@ -2,6 +2,12 @@ package com.healthlink.his.web.emr.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.core.domain.entity.SysUser;
|
||||
import com.core.system.mapper.SysUserMapper;
|
||||
import com.healthlink.his.administration.domain.Encounter;
|
||||
import com.healthlink.his.administration.domain.Patient;
|
||||
import com.healthlink.his.administration.mapper.EncounterMapper;
|
||||
import com.healthlink.his.administration.mapper.PatientMapper;
|
||||
import com.healthlink.his.document.domain.Emr;
|
||||
import com.healthlink.his.document.service.IEmrService;
|
||||
import com.healthlink.his.emr.domain.EmrRevision;
|
||||
@@ -31,6 +37,9 @@ public class EmrSyncController {
|
||||
private final IEmrRevisionService emrRevisionService;
|
||||
private final IEmrSearchIndexService emrSearchIndexService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PatientMapper patientMapper;
|
||||
private final EncounterMapper encounterMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
/**
|
||||
* 同步EMR数据
|
||||
@@ -97,15 +106,61 @@ public class EmrSyncController {
|
||||
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
|
||||
String diagnosis = contentMap.getOrDefault("diagnosis", "");
|
||||
|
||||
// 获取患者详细信息
|
||||
Patient patient = patientMapper.selectById(emr.getPatientId());
|
||||
String patientName = patient != null ? patient.getName() : "未知";
|
||||
String patientGender = "";
|
||||
String patientAge = "";
|
||||
String patientPhone = "";
|
||||
String patientIdCard = "";
|
||||
if (patient != null) {
|
||||
// 性别
|
||||
if (patient.getGenderEnum() != null) {
|
||||
patientGender = patient.getGenderEnum() == 1 ? "男" : "女";
|
||||
}
|
||||
// 年龄
|
||||
if (patient.getBirthDate() != null) {
|
||||
int age = java.time.Period.between(
|
||||
patient.getBirthDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(),
|
||||
java.time.LocalDate.now()
|
||||
).getYears();
|
||||
patientAge = String.valueOf(age);
|
||||
}
|
||||
patientPhone = patient.getPhone();
|
||||
patientIdCard = patient.getIdCard();
|
||||
}
|
||||
|
||||
// 获取就诊信息
|
||||
String encounterNo = "";
|
||||
if (emr.getEncounterId() != null) {
|
||||
var encounter = encounterMapper.selectById(emr.getEncounterId());
|
||||
if (encounter != null) {
|
||||
encounterNo = encounter.getBusNo();
|
||||
}
|
||||
}
|
||||
|
||||
EmrSearchIndex index = new EmrSearchIndex();
|
||||
index.setEmrId(emr.getId());
|
||||
index.setEncounterId(emr.getEncounterId());
|
||||
index.setPatientId(emr.getPatientId());
|
||||
index.setPatientName("患者" + emr.getPatientId());
|
||||
index.setPatientName(patientName);
|
||||
index.setPatientGender(patientGender);
|
||||
index.setPatientAge(patientAge);
|
||||
index.setPatientPhone(patientPhone);
|
||||
index.setPatientIdCard(patientIdCard);
|
||||
index.setEncounterNo(encounterNo);
|
||||
index.setEmrType(emr.getClassEnum() == 1 ? "OUTPATIENT" : "INPATIENT");
|
||||
index.setEmrTitle(chiefComplaint.isEmpty() ? "未命名病历" : chiefComplaint);
|
||||
index.setDiagnosisText(diagnosis);
|
||||
index.setDoctorName("医生" + emr.getRecordId());
|
||||
// 获取医生姓名
|
||||
String doctorName = "未知医生";
|
||||
if (emr.getRecordId() != null) {
|
||||
var doctor = sysUserMapper.selectById(emr.getRecordId());
|
||||
if (doctor != null) {
|
||||
doctorName = doctor.getNickName();
|
||||
}
|
||||
}
|
||||
index.setDoctorName(doctorName);
|
||||
index.setCreateTime(emr.getCreateTime());
|
||||
emrSearchIndexService.save(index);
|
||||
searchIndexCount++;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- V103__add_patient_info_to_emr_search_index.sql
|
||||
-- 为病历检索索引添加患者基本信息
|
||||
|
||||
-- 添加患者信息字段
|
||||
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
|
||||
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
|
||||
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
|
||||
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
|
||||
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX IF NOT EXISTS idx_emr_search_patient_name ON emr_search_index(patient_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_emr_search_encounter_no ON emr_search_index(encounter_no);
|
||||
@@ -5,6 +5,8 @@ import com.core.common.core.domain.HisBaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("emr_search_index")
|
||||
@@ -19,6 +21,14 @@ public class EmrSearchIndex extends HisBaseEntity {
|
||||
private Long patientId;
|
||||
@TableField("patient_name")
|
||||
private String patientName;
|
||||
@TableField("patient_gender")
|
||||
private String patientGender;
|
||||
@TableField("patient_age")
|
||||
private String patientAge;
|
||||
@TableField("patient_phone")
|
||||
private String patientPhone;
|
||||
@TableField("patient_id_card")
|
||||
private String patientIdCard;
|
||||
@TableField("emr_type")
|
||||
private String emrType;
|
||||
@TableField("emr_title")
|
||||
@@ -29,4 +39,6 @@ public class EmrSearchIndex extends HisBaseEntity {
|
||||
private String doctorName;
|
||||
@TableField("department_name")
|
||||
private String departmentName;
|
||||
@TableField("encounter_no")
|
||||
private String encounterNo;
|
||||
}
|
||||
|
||||
@@ -1,143 +1,296 @@
|
||||
<template>
|
||||
<div style="padding:16px">
|
||||
<div style="margin-bottom:16px">
|
||||
<span style="font-size:18px;font-weight:bold">病历检索</span>
|
||||
</div>
|
||||
<el-card
|
||||
shadow="never"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<el-form
|
||||
:model="queryParams"
|
||||
inline
|
||||
>
|
||||
<div class="emr-search-container">
|
||||
<el-card shadow="never" class="search-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>病历检索</span>
|
||||
<el-tag type="info" size="small">共 {{ total }} 条记录</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="queryParams" inline class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="诊断/标题/患者"
|
||||
placeholder="病历号/患者/诊断"
|
||||
clearable
|
||||
style="width:200px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="患者">
|
||||
<el-form-item label="患者姓名">
|
||||
<el-input
|
||||
v-model="queryParams.patientName"
|
||||
placeholder="患者姓名"
|
||||
clearable
|
||||
style="width:120px"
|
||||
style="width:140px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select
|
||||
v-model="queryParams.emrType"
|
||||
clearable
|
||||
style="width:120px"
|
||||
>
|
||||
<el-option
|
||||
v-for="t in [{l:'入院记录',v:'admission'},{l:'日常病程',v:'daily'},{l:'出院记录',v:'discharge'},{l:'手术记录',v:'surgery'},{l:'会诊记录',v:'consultation'}]"
|
||||
:key="t.v"
|
||||
:label="t.l"
|
||||
:value="t.v"
|
||||
/>
|
||||
<el-form-item label="病历类型">
|
||||
<el-select v-model="queryParams.emrType" placeholder="全部" clearable style="width:130px">
|
||||
<el-option v-for="t in emrTypeOptions" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="医生">
|
||||
<el-input
|
||||
v-model="queryParams.doctorName"
|
||||
clearable
|
||||
style="width:100px"
|
||||
/>
|
||||
<el-input v-model="queryParams.doctorName" placeholder="医生姓名" clearable style="width:120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室">
|
||||
<el-input v-model="queryParams.departmentName" placeholder="科室名称" clearable style="width:120px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
检索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="primary" icon="Search" @click="handleSearch">检索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="searchData"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column
|
||||
prop="patientName"
|
||||
label="患者"
|
||||
width="90"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="emrType"
|
||||
label="类型"
|
||||
width="100"
|
||||
|
||||
<el-card shadow="never" class="result-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="searchData"
|
||||
border
|
||||
stripe
|
||||
highlight-current-row
|
||||
@row-click="handleRowClick"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ {admission:'入院记录',daily:'日常病程',discharge:'出院记录',surgery:'手术记录',consultation:'会诊记录'}[row.emrType]||row.emrType }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="emrTitle"
|
||||
label="标题"
|
||||
min-width="180"
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="encounterNo" label="病历号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="patientName" label="患者姓名" width="100">
|
||||
<template #default="{row}">
|
||||
<span class="patient-name">{{ row.patientName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="patientGender" label="性别" width="60" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.patientGender === '男' ? '' : 'danger'" size="small">{{ row.patientGender }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="patientAge" label="年龄" width="60" align="center">
|
||||
<template #default="{row}">
|
||||
{{ row.patientAge ? row.patientAge + '岁' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="patientPhone" label="联系电话" width="120">
|
||||
<template #default="{row}">
|
||||
{{ row.patientPhone || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="emrType" label="病历类型" width="100">
|
||||
<template #default="{row}">
|
||||
<el-tag size="small" :type="getEmrTypeTag(row.emrType)">{{ getEmrTypeLabel(row.emrType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="emrTitle" label="病历标题" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="diagnosisText" label="诊断" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{row}">
|
||||
<span class="diagnosis-text">{{ row.diagnosisText || '暂无诊断' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doctorName" label="主治医生" width="100" />
|
||||
<el-table-column prop="departmentName" label="科室" width="100" />
|
||||
<el-table-column prop="createTime" label="就诊时间" width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button type="primary" link size="small" @click.stop="handleViewDetail(row)">
|
||||
<el-icon><View /></el-icon> 详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@size-change="handleSearch"
|
||||
@current-change="handleSearch"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="diagnosisText"
|
||||
label="诊断"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="doctorName"
|
||||
label="医生"
|
||||
width="90"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="departmentName"
|
||||
label="科室"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
width="170"
|
||||
/>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
layout="total,prev,pager,next"
|
||||
style="margin-top:16px;text-align:right"
|
||||
@current-change="handleSearch"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 病历详情弹窗 -->
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
title="病历详情"
|
||||
size="60%"
|
||||
direction="rtl"
|
||||
>
|
||||
<template v-if="currentRow">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="病历号">{{ currentRow.encounterNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="病历类型">
|
||||
<el-tag>{{ getEmrTypeLabel(currentRow.emrType) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="患者姓名">{{ currentRow.patientName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ currentRow.patientGender }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年龄">{{ currentRow.patientAge ? currentRow.patientAge + '岁' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ currentRow.patientPhone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号" :span="2">{{ currentRow.patientIdCard || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="主治医生">{{ currentRow.doctorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="科室">{{ currentRow.departmentName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="就诊时间" :span="2">{{ currentRow.createTime }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">诊断信息</el-divider>
|
||||
<el-input
|
||||
v-model="currentRow.diagnosisText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
readonly
|
||||
placeholder="暂无诊断信息"
|
||||
/>
|
||||
|
||||
<el-divider content-position="left">病历标题</el-divider>
|
||||
<el-input
|
||||
v-model="currentRow.emrTitle"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
readonly
|
||||
placeholder="暂无标题"
|
||||
/>
|
||||
|
||||
<el-divider />
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="handleViewFullEmr">查看完整病历</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref,reactive,onMounted} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {searchEmr} from './api'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { searchEmr } from './api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
|
||||
const route=useRoute()
|
||||
const loading=ref(false)
|
||||
const searchData=ref([])
|
||||
const total=ref(0)
|
||||
const queryParams=reactive({keyword:'',patientName:'',emrType:'',doctorName:'',departmentName:'',pageNo:1,pageSize:20})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const searchData = ref([])
|
||||
const total = ref(0)
|
||||
const detailVisible = ref(false)
|
||||
const currentRow = ref(null)
|
||||
|
||||
const handleSearch=async()=>{
|
||||
loading.value=true
|
||||
try{const r=await searchEmr(queryParams);searchData.value=r.data?.records||[];total.value=r.data?.total||0}finally{loading.value=false}
|
||||
const queryParams = reactive({
|
||||
keyword: '',
|
||||
patientName: '',
|
||||
emrType: '',
|
||||
doctorName: '',
|
||||
departmentName: '',
|
||||
pageNo: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
const emrTypeOptions = [
|
||||
{ label: '入院记录', value: 'OUTPATIENT' },
|
||||
{ label: '日常病程', value: 'DAILY' },
|
||||
{ label: '出院记录', value: 'DISCHARGE' },
|
||||
{ label: '手术记录', value: 'SURGERY' },
|
||||
{ label: '会诊记录', value: 'CONSULTATION' }
|
||||
]
|
||||
|
||||
const emrTypeMap = {
|
||||
OUTPATIENT: { label: '入院记录', type: '' },
|
||||
INPATIENT: { label: '住院病历', type: 'success' },
|
||||
DAILY: { label: '日常病程', type: 'warning' },
|
||||
DISCHARGE: { label: '出院记录', type: 'info' },
|
||||
SURGERY: { label: '手术记录', type: 'danger' },
|
||||
CONSULTATION: { label: '会诊记录', type: '' }
|
||||
}
|
||||
const resetQuery=()=>{Object.assign(queryParams,{keyword:'',patientName:'',emrType:'',doctorName:'',pageNo:1});handleSearch()}
|
||||
onMounted(()=>{
|
||||
if(route.query.patientName){queryParams.patientName=route.query.patientName}
|
||||
if(route.query.emrType){queryParams.emrType=route.query.emrType}
|
||||
if(route.query.keyword){queryParams.keyword=route.query.keyword}
|
||||
|
||||
const getEmrTypeLabel = (type) => emrTypeMap[type]?.label || type
|
||||
const getEmrTypeTag = (type) => emrTypeMap[type]?.type || 'info'
|
||||
|
||||
const handleSearch = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
// 清理空参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null) delete params[key]
|
||||
})
|
||||
const res = await searchEmr(params)
|
||||
searchData.value = res.data?.records || res.data || []
|
||||
total.value = res.data?.total || searchData.value.length
|
||||
} catch (e) {
|
||||
console.error('检索失败:', e)
|
||||
ElMessage.error('检索失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
Object.assign(queryParams, {
|
||||
keyword: '',
|
||||
patientName: '',
|
||||
emrType: '',
|
||||
doctorName: '',
|
||||
departmentName: '',
|
||||
pageNo: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleRowClick = (row) => {
|
||||
handleViewDetail(row)
|
||||
}
|
||||
|
||||
const handleViewDetail = (row) => {
|
||||
currentRow.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleViewFullEmr = () => {
|
||||
if (currentRow.value?.encounterId) {
|
||||
router.push({
|
||||
path: '/doctorstation',
|
||||
query: { encounterId: currentRow.value.encounterId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.patientName) queryParams.patientName = route.query.patientName
|
||||
if (route.query.emrType) queryParams.emrType = route.query.emrType
|
||||
if (route.query.keyword) queryParams.keyword = route.query.keyword
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.emr-search-container {
|
||||
padding: 16px;
|
||||
}
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.patient-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
.diagnosis-text {
|
||||
color: #606266;
|
||||
}
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user