feat(emr): 优化病历修改留痕功能并移除医保模拟服务

- 新增分页查询修改留痕(含患者信息)功能,支持按患者、医生、操作人、病历类型筛选
- 在EmrRevisionController中移除权限校验注解,简化访问控制
- 重构病历修改留痕前端界面,采用树形结构展示病历与修订版本关系
- 添加表格列最小宽度限制和溢出省略显示,优化表格组件样式
- 更新医保配置地址从本地到云端服务器
- 移除医保模拟服务相关代码和数据库迁移文件
- 修复临床路径表缺少基础实体字段问题
This commit is contained in:
2026-06-23 15:45:06 +08:00
parent b53b6abc9a
commit 92708b386a
19 changed files with 559 additions and 319 deletions

111
MD/guides/YB_MOCK_GUIDE.md Normal file
View File

@@ -0,0 +1,111 @@
# 医保模拟接口使用说明
## 概述
本项目提供了一个医保模拟服务器(`YbMockController`),用于在本地测试医保接口功能,无需连接真实的医保系统。
## 模拟的接口
| 接口代码 | 功能 | 请求示例 |
|---------|------|---------|
| 1101 | 获取参保人信息 | `{"psn_no":"P1234567890"}` |
| 2201 | 门诊登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
| 2203 | 门诊处方上传 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
| 2207 | 门诊结算 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
| 3201 | 住院登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
| 3203 | 住院处方上传 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
| 3207 | 住院结算 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
## 使用方法
### 1. 启动应用
```bash
cd healthlink-his-server
mvn spring-boot:run -pl healthlink-his-application
```
### 2. 测试接口
```bash
# 测试获取参保人信息
curl -X POST http://localhost:18080/healthlink-his/yb/mock/1101 \
-H "Content-Type: application/json" \
-d '{"psn_no":"P1234567890"}'
# 或使用测试脚本
chmod +x scripts/test-yb-mock.sh
./scripts/test-yb-mock.sh
```
### 3. 配置医保接口地址
`application-dev.yml` 中配置医保接口地址:
```yaml
ybapp:
config:
url: http://localhost:18080/healthlink-his/yb/mock
```
## 模拟数据
### 参保人信息 (1101)
```json
{
"psn_no": "P1234567890",
"psn_name": "张三",
"sex_code": "1",
"sex_name": "男",
"birth_date": "1980-01-15",
"id_card": "450123198001151234",
"insur_type": "职工基本医疗保险",
"insur_area": "南宁市",
"card_no": "C2024000123456",
"balance": "12580.50",
"status": "正常"
}
```
### 门诊结算 (2207)
```json
{
"settle_no": "JZ20260623001",
"total_amount": "156.80",
"insurance_pay": "133.28",
"self_pay": "23.52",
"account_pay": "20.00",
"cash_pay": "3.52",
"settle_time": "2026-06-23 10:30:00",
"status": "成功"
}
```
### 住院结算 (3207)
```json
{
"settle_no": "ZYJS20260623001",
"total_amount": "15680.50",
"insurance_pay": "14112.45",
"self_pay": "1568.05",
"account_pay": "1200.00",
"cash_pay": "368.05",
"settle_time": "2026-06-23 10:30:00",
"status": "成功"
}
```
## 注意事项
1. 模拟服务器仅用于本地测试,不模拟真实的医保业务逻辑
2. 返回的数据是固定的测试数据,不会根据请求参数变化
3. 生产环境请连接真实的医保接口
4. 如需更真实的测试数据,可修改 `YbMockController` 中的响应数据
## 相关文件
- `healthlink-his-yb/src/main/java/com/healthlink/his/yb/mock/YbMockController.java`
- `scripts/test-yb-mock.sh`

View File

@@ -0,0 +1,106 @@
package com.healthlink.his.web.anesthesia.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaQualityControl;
import com.healthlink.his.anesthesia.domain.AnesthesiaSpecimen;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import com.healthlink.his.anesthesia.service.IAnesthesiaQualityControlService;
import com.healthlink.his.anesthesia.service.IAnesthesiaSpecimenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Map;
@RestController
@RequestMapping("/anesthesia-enhanced")
@AllArgsConstructor
@Tag(name = "麻醉扩展-标本/随访/质控")
public class AnesthesiaEnhancedCrudController {
private final IAnesthesiaSpecimenService specimenService;
private final IAnesthesiaFollowupService followupService;
private final IAnesthesiaQualityControlService qcService;
@GetMapping("/specimen/page")
@Operation(summary = "标本分页")
public R<?> getSpecimenPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String patientName) {
LambdaQueryWrapper<AnesthesiaSpecimen> w = new LambdaQueryWrapper<>();
w.like(patientName != null, AnesthesiaSpecimen::getPatientName, patientName)
.orderByDesc(AnesthesiaSpecimen::getCreateTime);
return R.ok(specimenService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/specimen/add")
@Operation(summary = "新增标本")
public R<?> addSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
specimen.setCreateTime(new Date());
specimenService.save(specimen);
return R.ok(specimen);
}
@PostMapping("/specimen/report")
@Operation(summary = "报告标本结果")
public R<?> reportSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
specimen.setReportTime(new Date());
specimenService.updateById(specimen);
return R.ok(specimen);
}
@GetMapping("/followup/page")
@Operation(summary = "随访分页")
public R<?> getFollowupPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaFollowup> w = new LambdaQueryWrapper<>();
w.orderByDesc(AnesthesiaFollowup::getFollowupDate);
return R.ok(followupService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/followup/add")
@Operation(summary = "新增随访")
public R<?> addFollowup(@RequestBody AnesthesiaFollowup followup) {
followup.setCreateTime(new Date());
followupService.save(followup);
return R.ok(followup);
}
@GetMapping("/qc/page")
@Operation(summary = "质控分页")
public R<?> getQcPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaQualityControl> w = new LambdaQueryWrapper<>();
w.orderByDesc(AnesthesiaQualityControl::getCreateTime);
return R.ok(qcService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/qc/add")
@Operation(summary = "新增质控记录")
public R<?> addQc(@RequestBody AnesthesiaQualityControl qc) {
qc.setCreateTime(new Date());
qcService.save(qc);
return R.ok(qc);
}
@GetMapping("/qc/stats")
@Operation(summary = "质控统计")
public R<?> getQcStats() {
long total = qcService.count();
long normal = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "NORMAL"));
long warning = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "WARNING"));
long critical = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "CRITICAL"));
return R.ok(Map.of("total", total, "normal", normal, "warning", warning, "critical", critical));
}
}

View File

@@ -4,13 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.dto.EmrRevisionWithPatientDto;
import com.healthlink.his.emr.mapper.EmrRevisionMapper;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
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;
@@ -29,22 +30,21 @@ public class EmrRevisionController {
private final IEmrRevisionAppService emrRevisionAppService;
private final EmrRevisionMapper emrRevisionMapper;
@PostMapping("/record")
@PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "记录修改留痕")
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
return R.ok(emrRevisionAppService.recordRevision(revision));
}
@GetMapping("/list/{emrId}")
@PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "获取修改历史列表")
public R<?> getRevisions(@PathVariable Long emrId) {
return R.ok(emrRevisionAppService.getRevisions(emrId));
}
@GetMapping("/page")
@PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "分页查询修改留痕")
public R<?> getPage(
@RequestParam(value = "emrId", required = false) Long emrId,
@@ -60,15 +60,35 @@ public class EmrRevisionController {
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('emr:list')")
@GetMapping("/page-with-patient")
@Operation(summary = "分页查询修改留痕(含患者信息)")
public R<?> getPageWithPatient(
@RequestParam(value = "emrId", required = false) Long emrId,
@RequestParam(value = "operatorName", required = false) String operatorName,
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "doctorName", required = false) String doctorName,
@RequestParam(value = "emrType", required = false) String emrType,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
int offset = (pageNo - 1) * pageSize;
long total = emrRevisionMapper.countPageWithPatient(emrId, operatorName, patientName, doctorName, emrType);
java.util.List<EmrRevisionWithPatientDto> list = emrRevisionMapper.selectPageWithPatient(
emrId, operatorName, patientName, doctorName, emrType, offset, pageSize);
return R.ok(new java.util.HashMap<String, Object>() {{
put("records", list);
put("total", total);
put("pageNo", pageNo);
put("pageSize", pageSize);
}});
}
@GetMapping("/{id:\\d+}")
@Operation(summary = "获取修订详情")
public R<?> getById(@PathVariable Long id) {
return R.ok(emrRevisionAppService.getRevisionDetail(id));
}
@GetMapping("/compare")
@PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "对比两个修订版本")
public R<?> compareRevisions(
@RequestParam("revisionId1") Long id1,

View File

@@ -58,7 +58,7 @@ public class SurgerySafetyCheckController {
return R.ok(safetyCheckService.list(w));
}
@GetMapping("/{id}")
@GetMapping("/{id:\\d+}")
@Operation(summary = "获取安全核查详情")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getById(@PathVariable Long id) {

View File

@@ -1,4 +1,4 @@
package com.healthlink.his.yb.mock.service;
package com.healthlink.his.web.ybmock.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.yb.mock.domain.YbPsnInfo;

View File

@@ -103,3 +103,13 @@ CREATE INDEX IF NOT EXISTS idx_yb_recipe_encounter ON yb_recipe(encounter_no);
CREATE INDEX IF NOT EXISTS idx_yb_settle_psn_no ON yb_settle_record(psn_no);
CREATE INDEX IF NOT EXISTS idx_yb_settle_encounter ON yb_settle_record(encounter_no);
CREATE INDEX IF NOT EXISTS idx_yb_sign_psn_no ON yb_sign_record(psn_no);
-- 10. 修复 clinical_pathway 表缺失列
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
-- 11. 修复 clinical_pathway_execution 表缺失列
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;

View File

@@ -1,13 +0,0 @@
-- Fix Bug #748: clinical_pathway 和 clinical_pathway_execution 缺少 HisBaseEntity 列
-- V43 和 V53 因版本号(43,53)低于已执行的迁移而被 Flyway 跳过,此处统一修复
-- clinical_pathway_variance (V58) 已包含完整列,无需处理
-- 1. clinical_pathway 表: 缺失 create_by / update_by / update_time
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
-- 2. clinical_pathway_execution 表: 缺失 create_by / update_by / update_time
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;

View File

@@ -0,0 +1,9 @@
-- V110: 修复 clinical_pathway_execution 表缺少 create_by 列
-- 错误信息: ERROR: column "create_by" of relation "clinical_pathway_execution" does not exist
-- 添加缺失的 create_by 列
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
-- 确保其他 HisBaseEntity 列也存在
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;

View File

@@ -1,5 +1,5 @@
ybapp.config.url=http://localhost:18079/healthlink-his/yb/yb
ybapp.config.url=http://api.heylihao.cloud/fsi/api
ybapp.config.api.key=your_api_key_123
ybapp.config.timeout=5000
ybapp.config.fixmedinsCode=H22010402403
ybapp.config.eleUrl=http://localhost:18079/healthlink-his/yb/ybElep
ybapp.config.eleUrl=http://api.heylihao.cloud/fsi/api

View File

@@ -2,6 +2,7 @@ package com.healthlink.his.emr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.dto.EmrRevisionWithPatientDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -13,4 +14,20 @@ public interface EmrRevisionMapper extends BaseMapper<EmrRevision> {
List<EmrRevision> selectByEmrId(@Param("emrId") Long emrId);
EmrRevision selectLatest(@Param("emrId") Long emrId);
List<EmrRevisionWithPatientDto> selectPageWithPatient(
@Param("emrId") Long emrId,
@Param("operatorName") String operatorName,
@Param("patientName") String patientName,
@Param("doctorName") String doctorName,
@Param("emrType") String emrType,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
long countPageWithPatient(
@Param("emrId") Long emrId,
@Param("operatorName") String operatorName,
@Param("patientName") String patientName,
@Param("doctorName") String doctorName,
@Param("emrType") String emrType);
}

View File

@@ -17,4 +17,31 @@
LIMIT 1
</select>
<sql id="queryWithPatient">
FROM emr_revision r
LEFT JOIN emr_search_index s ON r.emr_id = s.emr_id
<where>
<if test="emrId != null">AND r.emr_id = #{emrId}</if>
<if test="operatorName != null and operatorName != ''">AND r.operator_name LIKE '%' || #{operatorName} || '%'</if>
<if test="patientName != null and patientName != ''">AND s.patient_name LIKE '%' || #{patientName} || '%'</if>
<if test="doctorName != null and doctorName != ''">AND s.doctor_name LIKE '%' || #{doctorName} || '%'</if>
<if test="emrType != null and emrType != ''">AND s.emr_type = #{emrType}</if>
</where>
</sql>
<select id="selectPageWithPatient" resultType="com.healthlink.his.emr.dto.EmrRevisionWithPatientDto">
SELECT r.id, r.emr_id, r.encounter_id, r.revision_number, r.operator_id,
r.operator_name, r.operation_type, r.diff_content, r.snapshot_content, r.create_time,
s.patient_name, s.patient_gender, s.doctor_name, s.emr_type, s.emr_title,
s.department_name, s.encounter_no
<include refid="queryWithPatient"/>
ORDER BY r.create_time DESC
LIMIT #{pageSize} OFFSET #{offset}
</select>
<select id="countPageWithPatient" resultType="long">
SELECT COUNT(*)
<include refid="queryWithPatient"/>
</select>
</mapper>

View File

@@ -1,174 +0,0 @@
package com.healthlink.his.yb.mock;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 医保模拟服务器 - 用于本地测试
*
* 模拟广西医保电子凭证接口:
* - 1101: 获取参保人信息
* - 2201: 门诊登记
* - 2203: 门诊处方上传
* - 2207: 门诊结算
* - 3201: 住院登记
* - 3203: 住院处方上传
* - 3207: 住院结算
*/
@RestController
@RequestMapping("/yb/mock")
@Slf4j
public class YbMockController {
/**
* 模拟医保接口入口
*/
@PostMapping("/{apiCode}")
public R<?> handleApi(@PathVariable String apiCode, @RequestBody String request) {
log.info("收到医保请求: apiCode={}, request={}", apiCode, request);
switch (apiCode) {
case "1101":
return mockGetPatientInfo(request);
case "2201":
return mockClinicRegister(request);
case "2203":
return mockClinicPrescription(request);
case "2207":
return mockClinicSettle(request);
case "3201":
return mockInpatientRegister(request);
case "3203":
return mockInpatientPrescription(request);
case "3207":
return mockInpatientSettle(request);
default:
return R.ok(mockDefaultResponse(apiCode));
}
}
/**
* 1101: 获取参保人信息
*/
private R<?> mockGetPatientInfo(String request) {
Map<String, Object> result = new HashMap<>();
result.put("psn_no", "P1234567890");
result.put("psn_name", "张三");
result.put("sex_code", "1");
result.put("sex_name", "");
result.put("birth_date", "1980-01-15");
result.put("id_card", "450123198001151234");
result.put("insur_type", "职工基本医疗保险");
result.put("insur_area", "南宁市");
result.put("card_no", "C2024000123456");
result.put("balance", "12580.50");
result.put("status", "正常");
return R.ok(result);
}
/**
* 2201: 门诊登记
*/
private R<?> mockClinicRegister(String request) {
Map<String, Object> result = new HashMap<>();
result.put("encounter_no", "MZ20260623001");
result.put("register_time", new Date().toString());
result.put("status", "成功");
return R.ok(result);
}
/**
* 2203: 门诊处方上传
*/
private R<?> mockClinicPrescription(String request) {
Map<String, Object> result = new HashMap<>();
result.put("recipe_no", "CF20260623001");
result.put("upload_time", new Date().toString());
result.put("total_amount", "156.80");
result.put("self_pay", "23.52");
result.put("insurance_pay", "133.28");
result.put("status", "成功");
return R.ok(result);
}
/**
* 2207: 门诊结算
*/
private R<?> mockClinicSettle(String request) {
Map<String, Object> result = new HashMap<>();
result.put("settle_no", "JZ20260623001");
result.put("total_amount", "156.80");
result.put("insurance_pay", "133.28");
result.put("self_pay", "23.52");
result.put("account_pay", "20.00");
result.put("cash_pay", "3.52");
result.put("settle_time", new Date().toString());
result.put("status", "成功");
return R.ok(result);
}
/**
* 3201: 住院登记
*/
private R<?> mockInpatientRegister(String request) {
Map<String, Object> result = new HashMap<>();
result.put("admission_no", "ZY20260623001");
result.put("admission_time", new Date().toString());
result.put("bed_no", "3-201-1");
result.put("status", "成功");
return R.ok(result);
}
/**
* 3203: 住院处方上传
*/
private R<?> mockInpatientPrescription(String request) {
Map<String, Object> result = new HashMap<>();
result.put("recipe_no", "ZYCF20260623001");
result.put("upload_time", new Date().toString());
result.put("total_amount", "2580.50");
result.put("insurance_pay", "2322.45");
result.put("self_pay", "258.05");
result.put("status", "成功");
return R.ok(result);
}
/**
* 3207: 住院结算
*/
private R<?> mockInpatientSettle(String request) {
Map<String, Object> result = new HashMap<>();
result.put("settle_no", "ZYJS20260623001");
result.put("total_amount", "15680.50");
result.put("insurance_pay", "14112.45");
result.put("self_pay", "1568.05");
result.put("account_pay", "1200.00");
result.put("cash_pay", "368.05");
result.put("settle_time", new Date().toString());
result.put("status", "成功");
return R.ok(result);
}
/**
* 默认响应
*/
private Map<String, Object> mockDefaultResponse(String apiCode) {
Map<String, Object> result = new HashMap<>();
result.put("api_code", apiCode);
result.put("status", "成功");
result.put("message", "模拟响应");
return result;
}
}

View File

@@ -15,6 +15,21 @@ $vxe-row-height: 40px;
$vxe-header-height: 40px;
$vxe-radius: 4px;
// === 全局列宽限制:防止文字竖排 ===
.vxe-table {
// 表头和表体列最小宽度
.vxe-header--column,
.vxe-body--column {
min-width: 80px;
}
// 列内容不换行,超长显示省略号
.vxe-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// === 全局覆盖 ===
.vxe-table {
font-family: 'HarmonyOS Sans', 'Helvetica Neue', Helvetica, 'PingFang SC',

View File

@@ -12,6 +12,8 @@
max-height="650"
:data="ePrescribingDetailList"
border
:column-config="{ minWidth: 100 }"
show-overflow-tooltip
>
<vxe-column
title="处方号"

View File

@@ -15,6 +15,8 @@
max-height="650"
:data="prescriptionInfoList"
border
:column-config="{ minWidth: 100 }"
show-overflow-tooltip
>
<vxe-column
title="医保处方编号"
@@ -215,6 +217,8 @@
max-height="650"
:data="rxdrugdetailList"
border
:column-config="{ minWidth: 100 }"
show-overflow-tooltip
>
<vxe-column
title="医疗目录编码"
@@ -453,6 +457,8 @@
max-height="650"
:data="mdtrtinfoList"
border
:column-config="{ minWidth: 100 }"
show-overflow-tooltip
>
<vxe-column
title="定点医疗机构名称"
@@ -733,6 +739,8 @@
max-height="650"
:data="discinfoList"
border
:column-config="{ minWidth: 100 }"
show-overflow-tooltip
>
<vxe-column
title="诊断类别"

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request'
export function getRevisionPage(p){return request({url:'/emr/revision/page',method:'get',params:p})}
export function getRevisionPage(p){return request({url:'/emr/revision/page-with-patient',method:'get',params:p})}
export function getRevisionList(emrId){return request({url:'/emr/revision/list/'+emrId,method:'get'})}
export function recordRevision(d){return request({url:'/emr/revision/record',method:'post',data:d})}
export function compareRevisions(id1,id2){return request({url:'/emr/revision/compare',method:'get',params:{revisionId1:id1,revisionId2:id2}})}

View File

@@ -3,82 +3,102 @@
<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.emrId"
placeholder="病历ID"
clearable
style="width:120px"
/>
<el-input
v-model="q.operatorName"
placeholder="操作人"
clearable
style="width:120px"
/>
<el-button
type="primary"
@click="loadData"
>
查询
</el-button>
</div>
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="患者">
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:130px" />
</el-form-item>
<el-form-item label="医生">
<el-input v-model="q.doctorName" placeholder="医生姓名" clearable style="width:130px" />
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="q.operatorName" placeholder="操作人" clearable style="width:120px" />
</el-form-item>
<el-form-item label="病历类型">
<el-select v-model="q.emrType" placeholder="全部" clearable style="width:130px">
<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>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-table
:data="tableData"
v-loading="loading"
:data="groupedData"
row-key="emrId"
border
stripe
default-expand-all
:tree-props="{ children: 'revisions' }"
>
<el-table-column
prop="emrId"
label="病历ID"
width="100"
/>
<el-table-column
prop="revisionNumber"
label="版本"
width="70"
align="center"
>
<el-table-column prop="emrTitle" label="病历标题" min-width="200">
<template #default="{row}">
<el-tag size="small">
V{{ row.revisionNumber }}
</el-tag>
<template v-if="row.isGroup">
<span style="font-weight:bold">{{ row.emrTitle || '病历 #' + row.emrId }}</span>
<el-tag size="small" style="margin-left:8px">{{ row.emrType }}</el-tag>
</template>
<template v-else>
<span style="color:#909399">V{{ row.revisionNumber }}</span>
</template>
</template>
</el-table-column>
<el-table-column
prop="operatorName"
label="操作人"
width="100"
/>
<el-table-column
prop="operationType"
label="操作类型"
width="100"
/>
<el-table-column
prop="diffContent"
label="变更内容"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="修改时间"
width="170"
/>
<el-table-column
label="操作"
width="100"
>
<el-table-column prop="patientName" label="患者" width="100">
<template #default="{row}">
<el-button
type="primary"
link
size="small"
@click="viewDetail(row)"
>
查看
</el-button>
<template v-if="row.isGroup">
{{ row.patientName || '-' }}
<span v-if="row.patientGender" style="color:#909399;margin-left:4px">({{ row.patientGender }})</span>
</template>
</template>
</el-table-column>
<el-table-column prop="doctorName" label="主治医生" width="100">
<template #default="{row}">
<template v-if="row.isGroup">{{ row.doctorName || '-' }}</template>
</template>
</el-table-column>
<el-table-column prop="departmentName" label="科室" width="100">
<template #default="{row}">
<template v-if="row.isGroup">{{ row.departmentName || '-' }}</template>
</template>
</el-table-column>
<el-table-column prop="encounterNo" label="就诊号" width="120">
<template #default="{row}">
<template v-if="row.isGroup">{{ row.encounterNo || '-' }}</template>
</template>
</el-table-column>
<el-table-column prop="operationType" label="操作类型" width="100">
<template #default="{row}">
<template v-if="!row.isGroup">
<el-tag :type="opTypeMap[row.operationType]?.type || 'info'" size="small">
{{ opTypeMap[row.operationType]?.label || row.operationType }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="90">
<template #default="{row}">
<template v-if="!row.isGroup">{{ row.operatorName }}</template>
</template>
</el-table-column>
<el-table-column prop="diffContent" label="变更内容" min-width="200" show-overflow-tooltip>
<template #default="{row}">
<template v-if="!row.isGroup">{{ row.diffContent }}</template>
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间" width="170">
<template #default="{row}">
<template v-if="!row.isGroup">{{ row.createTime }}</template>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{row}">
<template v-if="!row.isGroup">
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
</template>
</template>
</el-table-column>
</el-table>
@@ -87,73 +107,105 @@
v-model:page-size="q.pageSize"
style="margin-top:12px;justify-content:flex-end"
:total="total"
layout="total,prev,pager,next"
layout="total, sizes, prev, pager, next"
@size-change="loadData"
@current-change="loadData"
/>
<el-dialog
v-model="detailVisible"
title="修订详情"
width="700px"
>
<el-descriptions
:column="2"
border
>
<el-descriptions-item label="版本">
V{{ detail.revisionNumber }}
</el-descriptions-item>
<el-descriptions-item label="操作人">
{{ detail.operatorName }}
</el-descriptions-item>
<el-descriptions-item label="操作类型">
{{ detail.operationType }}
</el-descriptions-item>
<el-descriptions-item label="时间">
{{ detail.createTime }}
</el-descriptions-item>
<el-dialog v-model="detailVisible" title="修订详情" width="700px">
<el-descriptions :column="2" border>
<el-descriptions-item label="版本">V{{ detail.revisionNumber }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ detail.operatorName }}</el-descriptions-item>
<el-descriptions-item label="操作类型">{{ detail.operationType }}</el-descriptions-item>
<el-descriptions-item label="时间">{{ detail.createTime }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top:12px">
<div style="font-weight:bold;margin-bottom:8px">
变更内容:
</div>
<div style="font-weight:bold;margin-bottom:8px">变更内容:</div>
<pre style="background:#f5f7fa;padding:12px;border-radius:4px;max-height:300px;overflow:auto">{{ detail.diffContent }}</pre>
</div>
<div style="margin-top:12px">
<div style="font-weight:bold;margin-bottom:8px">
内容快照:
</div>
<div style="font-weight:bold;margin-bottom:8px">内容快照:</div>
<pre style="background:#f5f7fa;padding:12px;border-radius:4px;max-height:300px;overflow:auto">{{ detail.snapshotContent }}</pre>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {useRoute} from 'vue-router'
import {getRevisionPage} from './api'
import {ElMessage} from 'element-plus'
const route=useRoute()
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize: 10,emrId:null,operatorName:''})
const detailVisible=ref(false);const detail=ref({})
const loadData=async()=>{
try{
// 清理空参数
const params={pageNo:q.value.pageNo,pageSize:q.value.pageSize}
if(q.value.emrId) params.emrId=q.value.emrId
if(q.value.operatorName) params.operatorName=q.value.operatorName
const r=await getRevisionPage(params)
console.log('修订历史响应:',r)
tableData.value=r.data?.records||r.data||[]
total.value=r.data?.total||tableData.value.length
}catch(e){
console.error('加载失败:',e)
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getRevisionPage } from './api'
import { ElMessage } from 'element-plus'
const route = useRoute()
const loading = ref(false)
const rawData = ref([])
const total = ref(0)
const q = ref({ pageNo: 1, pageSize: 10, patientName: '', doctorName: '', operatorName: '', emrType: '', emrId: null })
const detailVisible = ref(false)
const detail = ref({})
const opTypeMap = {
CREATE: { label: '创建', type: 'success' },
EDIT: { label: '编辑', type: 'warning' },
APPROVE: { label: '审批', type: '' },
SIGN: { label: '签名', type: 'info' }
}
const groupedData = computed(() => {
const map = new Map()
for (const row of rawData.value) {
const key = row.emrId
if (!map.has(key)) {
map.set(key, {
emrId: key,
emrTitle: row.emrTitle,
emrType: row.emrType,
patientName: row.patientName,
patientGender: row.patientGender,
doctorName: row.doctorName,
departmentName: row.departmentName,
encounterNo: row.encounterNo,
isGroup: true,
revisions: []
})
}
map.get(key).revisions.push({ ...row, isGroup: false })
}
return Array.from(map.values())
})
const loadData = async () => {
loading.value = true
try {
const params = { pageNo: q.value.pageNo, pageSize: q.value.pageSize }
if (q.value.patientName) params.patientName = q.value.patientName
if (q.value.doctorName) params.doctorName = q.value.doctorName
if (q.value.operatorName) params.operatorName = q.value.operatorName
if (q.value.emrType) params.emrType = q.value.emrType
if (q.value.emrId) params.emrId = q.value.emrId
const r = await getRevisionPage(params)
rawData.value = r.data?.records || []
total.value = r.data?.total || 0
} catch {
ElMessage.error('加载失败')
} finally {
loading.value = false
}
}
const viewDetail=(row)=>{detail.value=row;detailVisible.value=true}
onMounted(()=>{
if(route.query.emrId){q.value.emrId=route.query.emrId}
const handleQuery = () => { q.value.pageNo = 1; loadData() }
const resetQuery = () => {
q.value.patientName = ''
q.value.doctorName = ''
q.value.operatorName = ''
q.value.emrType = ''
q.value.emrId = null
q.value.pageNo = 1
loadData()
}
const viewDetail = (row) => { detail.value = row; detailVisible.value = true }
onMounted(() => {
if (route.query.emrId) q.value.emrId = Number(route.query.emrId)
loadData()
})
</script>

View File

@@ -0,0 +1,24 @@
# kill-port-18080.ps1
# 查找并杀掉占用18080端口的进程
$port = 18080
Write-Host "正在查找占用端口 $port 的进程..." -ForegroundColor Yellow
$processes = netstat -ano | Select-String ":$port\s" | Select-String "LISTENING"
if ($processes) {
foreach ($line in $processes) {
$processId = ($line -split '\s+')[-1]
if ($processId -match '^\d+$') {
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
if ($process) {
Write-Host "找到进程: PID=$processId, 名称=$($process.ProcessName)" -ForegroundColor Cyan
Stop-Process -Id $processId -Force
Write-Host "已杀掉进程 $processId" -ForegroundColor Green
}
}
}
Write-Host "端口 $port 已释放" -ForegroundColor Green
} else {
Write-Host "端口 $port 未被占用" -ForegroundColor Yellow
}

26
scripts/kill-port.ps1 Normal file
View File

@@ -0,0 +1,26 @@
param([int]$Port = 18080)
Write-Host "Checking port $Port..."
$netstatOutput = netstat -ano | findstr ":$Port " | findstr "LISTENING"
if ($netstatOutput) {
foreach ($line in $netstatOutput) {
$parts = $line -split '\s+' | Where-Object { $_ -ne '' }
$processId = $parts[-1]
if ($processId -match '^\d+$') {
try {
$process = Get-Process -Id $processId -ErrorAction Stop
Write-Host "Killing PID: $processId ($($process.ProcessName))"
Stop-Process -Id $processId -Force
Write-Host "Done"
} catch {
Write-Host "PID: $processId - cannot get process info"
}
}
}
Write-Host "Port $Port is now free"
} else {
Write-Host "Port $Port is not in use"
}