feat(emr): 优化病历检索页面

- 添加患者基本信息:性别、年龄、电话、身份证号
- 添加就诊号字段
- 重写前端页面,参考行业通用设计
- 支持点击查看病历详情
- 同步时自动填充患者和医生信息
This commit is contained in:
2026-06-21 14:47:36 +08:00
parent 8b77710c19
commit 88b35c13f8
4 changed files with 345 additions and 112 deletions

View File

@@ -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++;

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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>