Compare commits
17 Commits
6212e0d92f
...
2956296301
| Author | SHA1 | Date | |
|---|---|---|---|
| 2956296301 | |||
| 88b35c13f8 | |||
| 8b77710c19 | |||
| dc352ace4a | |||
| fde29104ab | |||
| ac7c611261 | |||
| f0a71700e4 | |||
| 732e4f5ffd | |||
| c285c1ba5e | |||
| 2f0baaa837 | |||
| 129eb2b606 | |||
| 7601fc26e7 | |||
| f7b99f8d9e | |||
| f4493cf74b | |||
| b965d80b12 | |||
| e04b2736c5 | |||
| 2de2b31e92 |
@@ -277,7 +277,7 @@
|
||||
**铁律10: 验证后信**
|
||||
- 每次修改后必须验证编译通过,不信记忆
|
||||
|
||||
**铁律13: 文档统一管理**
|
||||
**铁律13: 文档统一管理(P0绝对铁律)**
|
||||
- 所有文档存储在 `MD/` 目录
|
||||
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
||||
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
||||
@@ -684,7 +684,7 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
**铁律10: 验证后信**
|
||||
- 每次修改后必须验证编译通过,不信记忆
|
||||
|
||||
**铁律13: 文档统一管理**
|
||||
**铁律13: 文档统一管理(P0绝对铁律)**
|
||||
- 所有文档存储在 `MD/` 目录
|
||||
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
||||
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
||||
@@ -1077,3 +1077,5 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| #2 | Flyway 数据库迁移 | P0 | 数据库变更 |
|
||||
| #3 | 先分解再行动 | P1 | 非平凡任务 |
|
||||
| #4 | 验证后信 | P1 | 编译/构建 |
|
||||
| #5 | 文档统一管理 | P1 | 文档产出 |
|
||||
| #5 | 文档统一管理(P0绝对铁律) | P0 | 文档产出 |
|
||||
| #6 | 测试通过后才提交 | P0 | 代码提交 |
|
||||
| #7 | 前后端API路径对齐 | P0 | 接口开发 |
|
||||
| #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 |
|
||||
@@ -120,26 +120,38 @@ cd healthlink-his-ui && npm run build:dev
|
||||
|
||||
---
|
||||
|
||||
### 铁律 #5: 文档统一管理
|
||||
### 铁律 #5: 文档统一管理(P0 绝对铁律)
|
||||
|
||||
**所有文档必须存储在 `MD/` 目录中,遵循文档规范。**
|
||||
**所有文档必须存储在 `MD/` 目录中,禁止在项目其他位置创建文档文件。**
|
||||
|
||||
#### 目录结构
|
||||
#### 绝对禁止
|
||||
| ❌ 禁止行为 | 说明 |
|
||||
|------------|------|
|
||||
| 在项目根目录创建 `.md` 文件 | 如 `README.md`、`TODO.md`、`NOTES.md` 等 |
|
||||
| 在子模块目录创建文档 | 如 `healthlink-his-server/DESIGN.md` |
|
||||
| 在 `docs/` 目录存放文档 | 必须移动到 `MD/` |
|
||||
| 随意创建新目录 | 必须使用已有目录结构 |
|
||||
| 使用中文作文件名 | 必须使用大写英文+下划线 |
|
||||
|
||||
#### 目录结构(必须遵守)
|
||||
```
|
||||
MD/
|
||||
├── DOCUMENTATION_STANDARD.md # 文档管理规范
|
||||
├── architecture/ # 架构设计
|
||||
├── architecture/ # 架构设计文档
|
||||
├── design/ # 模块设计文档
|
||||
├── development/ # 开发计划与记录
|
||||
├── standards/ # 国家/行业标准
|
||||
├── specs/ # 技术规范与流程
|
||||
├── bugs/ # Bug分析与修复记录
|
||||
├── guides/ # 使用指南
|
||||
└── upgrade/ # 升级记录
|
||||
├── upgrade/ # 升级记录
|
||||
├── test/ # 测试文档
|
||||
└── 需求/ # 需求文档(允许中文目录名)
|
||||
```
|
||||
|
||||
#### 命名规范
|
||||
- 文件名使用 **大写英文+下划线**(如 `GRADE3A_DETAILED_DESIGN.md`)
|
||||
- 不使用中文作文件名
|
||||
- 不使用中文作文件名(需求目录除外)
|
||||
- 不使用空格分隔单词
|
||||
- 版本号标注在文件名末尾(如 `_V2`)
|
||||
|
||||
@@ -234,6 +246,7 @@ MD/
|
||||
|------|------|---------|
|
||||
| P0 违规 | 跳过测试直接提交 | 必须回滚并重新测试 |
|
||||
| P0 违规 | 数据库变更不走Flyway | 回滚数据库变更,重新用Flyway执行 |
|
||||
| P0 违规 | 在MD目录外创建文档 | 立即移动到MD目录,删除原文件 |
|
||||
| P1 违规 | 未分解就行动 | 补充分析和计划文档 |
|
||||
| P1 违规 | 文档不规范 | 补充元数据和格式 |
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@
|
||||
<artifactId>healthlink-his-domain</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.healthlink.his</groupId>
|
||||
<artifactId>healthlink-his-yb</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 基础模块 -->
|
||||
<dependency>
|
||||
@@ -89,6 +94,10 @@
|
||||
<groupId>com.core</groupId>
|
||||
<artifactId>core-generator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.core</groupId>
|
||||
<artifactId>core-admin</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- liteflow-->
|
||||
<dependency>
|
||||
|
||||
@@ -28,6 +28,10 @@ import com.healthlink.his.document.service.IEmrDetailService;
|
||||
import com.healthlink.his.document.service.IEmrDictService;
|
||||
import com.healthlink.his.document.service.IEmrService;
|
||||
import com.healthlink.his.document.service.IEmrTemplateService;
|
||||
import com.healthlink.his.emr.domain.EmrRevision;
|
||||
import com.healthlink.his.emr.domain.EmrSearchIndex;
|
||||
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||
import com.healthlink.his.emr.service.IEmrSearchIndexService;
|
||||
import com.healthlink.his.web.doctorstation.appservice.IDoctorStationEmrAppService;
|
||||
import com.healthlink.his.web.doctorstation.dto.EmrTemplateDto;
|
||||
import com.healthlink.his.web.doctorstation.dto.PatientEmrDto;
|
||||
@@ -63,6 +67,18 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
||||
@Resource
|
||||
IDocRecordService docRecordService;
|
||||
|
||||
@Resource
|
||||
IEmrRevisionService emrRevisionService;
|
||||
|
||||
@Resource
|
||||
IEmrSearchIndexService emrSearchIndexService;
|
||||
|
||||
@Resource
|
||||
PatientMapper patientMapper;
|
||||
|
||||
@Resource
|
||||
EncounterMapper encounterMapper;
|
||||
|
||||
@Resource
|
||||
private com.healthlink.his.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper;
|
||||
|
||||
@@ -79,10 +95,12 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
||||
String contextStr = patientEmrDto.getContextJson().toString();
|
||||
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
|
||||
boolean saveSuccess;
|
||||
boolean isUpdate = patientEmr != null;
|
||||
// 如果已经保存病历,再次保存走更新
|
||||
if (patientEmr != null) {
|
||||
if (isUpdate) {
|
||||
saveSuccess = emrService.update(new LambdaUpdateWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId())
|
||||
.set(Emr::getContextJson, contextStr));
|
||||
emr = patientEmr;
|
||||
} else {
|
||||
saveSuccess =
|
||||
emrService.save(emr.setContextJson(contextStr).setRecordId(SecurityUtils.getLoginUser().getUserId()));
|
||||
@@ -90,6 +108,21 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
||||
if (!saveSuccess) {
|
||||
return R.fail();
|
||||
}
|
||||
|
||||
// 自动触发:记录修订历史
|
||||
try {
|
||||
recordRevisionAutomatically(emr, contextStr, isUpdate);
|
||||
} catch (Exception e) {
|
||||
log.warn("自动记录修订历史失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 自动触发:更新搜索索引
|
||||
try {
|
||||
updateSearchIndexAutomatically(emr, contextStr);
|
||||
} catch (Exception e) {
|
||||
log.warn("自动更新搜索索引失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 获取电子病历字典表中全部key,用来判断病历JSON串中是否有需要加入到病历详情表的字段
|
||||
List<String> emrDictList = emrDictService.list(new LambdaQueryWrapper<EmrDict>().select(EmrDict::getEmrKey))
|
||||
.stream().map(EmrDict::getEmrKey).collect(Collectors.toList());
|
||||
@@ -114,6 +147,73 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
||||
return save ? R.ok() : R.fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动记录修订历史
|
||||
*/
|
||||
private void recordRevisionAutomatically(Emr emr, String contextStr, boolean isUpdate) {
|
||||
EmrRevision latest = emrRevisionService.selectLatest(emr.getId());
|
||||
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
|
||||
|
||||
EmrRevision revision = new EmrRevision();
|
||||
revision.setEmrId(emr.getId());
|
||||
revision.setEncounterId(emr.getEncounterId());
|
||||
revision.setRevisionNumber(nextNumber);
|
||||
revision.setOperatorId(SecurityUtils.getLoginUser().getUserId());
|
||||
revision.setOperatorName(SecurityUtils.getUsername());
|
||||
revision.setOperationType(isUpdate ? "UPDATE" : "CREATE");
|
||||
revision.setSnapshotContent(contextStr);
|
||||
if (isUpdate && latest != null) {
|
||||
revision.setDiffContent("内容已更新");
|
||||
}
|
||||
revision.setCreateTime(new Date());
|
||||
emrRevisionService.save(revision);
|
||||
log.info("自动记录修订历史: emrId={}, revision={}", emr.getId(), nextNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动更新搜索索引
|
||||
*/
|
||||
private void updateSearchIndexAutomatically(Emr emr, String contextStr) {
|
||||
Map<String, String> contentMap = JsonUtils.parseObject(contextStr, new TypeReference<Map<String, String>>() {});
|
||||
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
|
||||
String diagnosis = contentMap.getOrDefault("diagnosis", "");
|
||||
|
||||
// 获取患者信息
|
||||
Patient patient = patientMapper.selectById(emr.getPatientId());
|
||||
String patientName = patient != null ? patient.getName() : "";
|
||||
Long patientId = patient != null ? patient.getId() : null;
|
||||
|
||||
// 获取医生信息
|
||||
String doctorName = SecurityUtils.getUsername();
|
||||
|
||||
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EmrSearchIndex::getEmrId, emr.getId());
|
||||
EmrSearchIndex existing = emrSearchIndexService.getOne(wrapper);
|
||||
|
||||
if (existing != null) {
|
||||
existing.setPatientName(patientName);
|
||||
existing.setPatientId(patientId);
|
||||
existing.setEmrTitle(chiefComplaint);
|
||||
existing.setDiagnosisText(diagnosis);
|
||||
existing.setDoctorName(doctorName);
|
||||
existing.setUpdateTime(new Date());
|
||||
emrSearchIndexService.updateById(existing);
|
||||
} else {
|
||||
EmrSearchIndex index = new EmrSearchIndex();
|
||||
index.setEmrId(emr.getId());
|
||||
index.setEncounterId(emr.getEncounterId());
|
||||
index.setPatientId(patientId);
|
||||
index.setPatientName(patientName);
|
||||
index.setEmrType("OUTPATIENT");
|
||||
index.setEmrTitle(chiefComplaint);
|
||||
index.setDiagnosisText(diagnosis);
|
||||
index.setDoctorName(doctorName);
|
||||
index.setCreateTime(new Date());
|
||||
emrSearchIndexService.save(index);
|
||||
}
|
||||
log.info("自动更新搜索索引: emrId={}", emr.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取患者历史病历
|
||||
*
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ProgressNoteController {
|
||||
* 分页查询病程记录列表
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list') or hasAuthority('emr:list')")
|
||||
public R<?> getPage(
|
||||
@RequestParam(value = "patientName", required = false) String patientName,
|
||||
@RequestParam(value = "noteType", required = false) Integer noteType,
|
||||
@@ -75,7 +75,7 @@ public class ProgressNoteController {
|
||||
* 查询病程记录详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list') or hasAuthority('emr:list')")
|
||||
public R<?> getDetail(@RequestParam Long id) {
|
||||
ProgressNote note = progressNoteService.getById(id);
|
||||
if (note == null) return R.fail("病程记录不存在");
|
||||
@@ -86,7 +86,7 @@ public class ProgressNoteController {
|
||||
* 新增病程记录
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:add')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:add') or hasAuthority('emr:edit')")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> add(@RequestBody ProgressNote note) {
|
||||
note.setSignStatus(0);
|
||||
@@ -108,7 +108,7 @@ public class ProgressNoteController {
|
||||
* 修改病程记录(仅未签名可修改)
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit') or hasAuthority('emr:edit')")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> update(@RequestBody ProgressNote note) {
|
||||
ProgressNote existing = progressNoteService.getById(note.getId());
|
||||
@@ -124,7 +124,7 @@ public class ProgressNoteController {
|
||||
* 删除病程记录(仅未签名可删除)
|
||||
*/
|
||||
@DeleteMapping("/delete")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:remove')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:remove') or hasAuthority('emr:edit')")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> delete(@RequestParam Long id) {
|
||||
ProgressNote note = progressNoteService.getById(id);
|
||||
@@ -138,7 +138,7 @@ public class ProgressNoteController {
|
||||
* 签名病程记录
|
||||
*/
|
||||
@PostMapping("/sign")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit') or hasAuthority('emr:edit')")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> sign(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
@@ -158,7 +158,7 @@ public class ProgressNoteController {
|
||||
* 审核病程记录(上级医师)
|
||||
*/
|
||||
@PostMapping("/review")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:edit') or hasAuthority('emr:edit')")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> review(@RequestBody Map<String, Object> params) {
|
||||
Long id = Long.valueOf(params.get("id").toString());
|
||||
@@ -177,7 +177,7 @@ public class ProgressNoteController {
|
||||
* 获取时限监控面板
|
||||
*/
|
||||
@GetMapping("/monitor")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list') or hasAuthority('emr:list')")
|
||||
public R<?> getMonitor(@RequestParam(required = false) Long encounterId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Date now = new Date();
|
||||
@@ -225,7 +225,7 @@ public class ProgressNoteController {
|
||||
* 获取提醒列表
|
||||
*/
|
||||
@GetMapping("/reminders")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list') or hasAuthority('emr:list')")
|
||||
public R<?> getReminders(
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||
@@ -240,7 +240,7 @@ public class ProgressNoteController {
|
||||
* 获取病程记录统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
||||
@PreAuthorize("hasAuthority('document:progressnote:list') or hasAuthority('emr:list')")
|
||||
public R<?> getStats(@RequestParam Long encounterId) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
@@ -21,7 +21,7 @@ public class EmrCompletenessController {
|
||||
private final IEmrCompletenessAppService emrCompletenessAppService;
|
||||
|
||||
@PostMapping("/check")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||
@Operation(summary = "执行病历完整性检查")
|
||||
public R<Map<String, Object>> checkCompleteness(
|
||||
@RequestParam("emrId") Long emrId,
|
||||
@@ -30,7 +30,7 @@ public class EmrCompletenessController {
|
||||
}
|
||||
|
||||
@GetMapping("/results/{emrId}")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "获取完整性检查结果")
|
||||
public R<?> getCheckResults(@PathVariable Long emrId) {
|
||||
return R.ok(emrCompletenessAppService.getCheckResults(emrId));
|
||||
|
||||
@@ -23,28 +23,28 @@ public class EmrDataWarehouseController {
|
||||
private final IEmrDataWarehouseAppService emrDataWarehouseAppService;
|
||||
|
||||
@PostMapping("/extract")
|
||||
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||
@Operation(summary = "提取结构化数据")
|
||||
public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) {
|
||||
return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId));
|
||||
}
|
||||
|
||||
@GetMapping("/data/{encounterId}")
|
||||
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "查询结构化数据")
|
||||
public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) {
|
||||
return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId));
|
||||
}
|
||||
|
||||
@PostMapping("/quality-score")
|
||||
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||
@Operation(summary = "计算质控评分")
|
||||
public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) {
|
||||
return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId));
|
||||
}
|
||||
|
||||
@GetMapping("/quality-scores")
|
||||
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "查询质控评分列表")
|
||||
public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) {
|
||||
return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId));
|
||||
|
||||
@@ -30,21 +30,21 @@ public class EmrRevisionController {
|
||||
private final IEmrRevisionAppService emrRevisionAppService;
|
||||
|
||||
@PostMapping("/record")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||
@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('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "获取修改历史列表")
|
||||
public R<?> getRevisions(@PathVariable Long emrId) {
|
||||
return R.ok(emrRevisionAppService.getRevisions(emrId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "分页查询修改留痕")
|
||||
public R<?> getPage(
|
||||
@RequestParam(value = "emrId", required = false) Long emrId,
|
||||
@@ -61,14 +61,14 @@ public class EmrRevisionController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "获取修订详情")
|
||||
public R<?> getById(@PathVariable Long id) {
|
||||
return R.ok(emrRevisionAppService.getRevisionDetail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "对比两个修订版本")
|
||||
public R<?> compareRevisions(
|
||||
@RequestParam("revisionId1") Long id1,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
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;
|
||||
import com.healthlink.his.emr.domain.EmrSearchIndex;
|
||||
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||
import com.healthlink.his.emr.service.IEmrSearchIndexService;
|
||||
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.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* EMR数据同步Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/emr-sync")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@Tag(name = "EMR数据同步")
|
||||
public class EmrSyncController {
|
||||
|
||||
private final IEmrService emrService;
|
||||
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数据
|
||||
* 清空假数据,从doc_emr生成真实数据
|
||||
*/
|
||||
@PostMapping("/sync")
|
||||
@Operation(summary = "同步EMR修订历史和搜索索引")
|
||||
public R<?> syncEmrData() {
|
||||
log.info("开始同步EMR数据...");
|
||||
|
||||
// 1. 清空假数据(使用原生SQL避免全表删除限制)
|
||||
try {
|
||||
jdbcTemplate.execute("TRUNCATE TABLE emr_revision CASCADE");
|
||||
jdbcTemplate.execute("TRUNCATE TABLE emr_search_index CASCADE");
|
||||
log.info("已清空emr_revision和emr_search_index表");
|
||||
} catch (Exception e) {
|
||||
log.warn("TRUNCATE失败,尝试使用DELETE: {}", e.getMessage());
|
||||
// 备用方案:查询所有ID后删除
|
||||
List<Long> revisionIds = emrRevisionService.list(new LambdaQueryWrapper<EmrRevision>().select(EmrRevision::getId))
|
||||
.stream().map(EmrRevision::getId).toList();
|
||||
if (!revisionIds.isEmpty()) {
|
||||
emrRevisionService.removeByIds(revisionIds);
|
||||
}
|
||||
List<Long> searchIndexIds = emrSearchIndexService.list(new LambdaQueryWrapper<EmrSearchIndex>().select(EmrSearchIndex::getId))
|
||||
.stream().map(EmrSearchIndex::getId).toList();
|
||||
if (!searchIndexIds.isEmpty()) {
|
||||
emrSearchIndexService.removeByIds(searchIndexIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从doc_emr获取所有病历
|
||||
List<Emr> emrList = emrService.list(new LambdaQueryWrapper<Emr>()
|
||||
.orderByAsc(Emr::getCreateTime));
|
||||
|
||||
if (emrList.isEmpty()) {
|
||||
return R.ok("没有病历数据需要同步");
|
||||
}
|
||||
|
||||
int revisionCount = 0;
|
||||
int searchIndexCount = 0;
|
||||
|
||||
for (Emr emr : emrList) {
|
||||
// 3. 创建修订历史
|
||||
try {
|
||||
EmrRevision revision = new EmrRevision();
|
||||
revision.setEmrId(emr.getId());
|
||||
revision.setEncounterId(emr.getEncounterId());
|
||||
revision.setRevisionNumber(1);
|
||||
revision.setOperatorId(emr.getRecordId());
|
||||
revision.setOperatorName("系统同步");
|
||||
revision.setOperationType("CREATE");
|
||||
revision.setDiffContent("初始创建");
|
||||
revision.setSnapshotContent(emr.getContextJson());
|
||||
revision.setCreateTime(emr.getCreateTime());
|
||||
emrRevisionService.save(revision);
|
||||
revisionCount++;
|
||||
} catch (Exception e) {
|
||||
log.warn("创建修订历史失败: emrId={}, error={}", emr.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
// 4. 创建搜索索引
|
||||
try {
|
||||
Map<String, String> contentMap = parseContextJson(emr.getContextJson());
|
||||
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(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);
|
||||
// 获取医生姓名
|
||||
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++;
|
||||
} catch (Exception e) {
|
||||
log.warn("创建搜索索引失败: emrId={}, error={}", emr.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
String result = String.format("同步完成: 修订历史%d条, 搜索索引%d条", revisionCount, searchIndexCount);
|
||||
log.info(result);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
@Operation(summary = "获取EMR同步统计")
|
||||
public R<?> getSyncStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("emrCount", emrService.count());
|
||||
stats.put("revisionCount", emrRevisionService.count());
|
||||
stats.put("searchIndexCount", emrSearchIndexService.count());
|
||||
return R.ok(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析contextJson字符串
|
||||
*/
|
||||
private Map<String, String> parseContextJson(String contextJson) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
if (contextJson == null || contextJson.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
// 简单解析JSON字符串
|
||||
String json = contextJson.trim();
|
||||
if (json.startsWith("{") && json.endsWith("}")) {
|
||||
json = json.substring(1, json.length() - 1);
|
||||
String[] pairs = json.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] kv = pair.split(":");
|
||||
if (kv.length == 2) {
|
||||
String key = kv[0].trim().replace("\"", "");
|
||||
String value = kv[1].trim().replace("\"", "");
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析contextJson失败: {}", e.getMessage());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class EmrTimelinessController {
|
||||
private final IEmrTimelinessAppService emrTimelinessAppService;
|
||||
|
||||
@PostMapping("/check")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||
@Operation(summary = "执行病历时限检查")
|
||||
public R<EmrTimelinessStatisticsDto> checkTimeliness(
|
||||
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||
@@ -31,7 +31,7 @@ public class EmrTimelinessController {
|
||||
}
|
||||
|
||||
@GetMapping("/alerts")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "获取病历时限提醒列表")
|
||||
public R<Map<String, Object>> getTimelinessAlerts(
|
||||
@RequestParam(value = "emrType", required = false) String emrType,
|
||||
|
||||
@@ -20,21 +20,21 @@ public class EmrVersionController {
|
||||
private final IEmrVersionAppService emrVersionAppService;
|
||||
|
||||
@PostMapping("/save")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||
@Operation(summary = "保存病历版本")
|
||||
public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) {
|
||||
return R.ok(emrVersionAppService.saveVersion(version));
|
||||
}
|
||||
|
||||
@GetMapping("/list/{emrId}")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "获取病历版本列表")
|
||||
public R<?> getVersions(@PathVariable Long emrId) {
|
||||
return R.ok(emrVersionAppService.getVersions(emrId));
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||
@Operation(summary = "对比两个版本")
|
||||
public R<?> compareVersions(
|
||||
@RequestParam("versionId1") Long versionId1,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
-- V100__sync_existing_emr_data.sql
|
||||
-- 同步已有的门诊/住院病历到修订历史和搜索索引
|
||||
|
||||
-- 1. 清空假数据
|
||||
TRUNCATE TABLE emr_revision CASCADE;
|
||||
TRUNCATE TABLE emr_search_index CASCADE;
|
||||
|
||||
-- 2. 从doc_emr同步修订历史(为每条病历创建初始修订记录)
|
||||
INSERT INTO emr_revision (
|
||||
id, emr_id, encounter_id, revision_number,
|
||||
operator_id, operator_name, operation_type,
|
||||
diff_content, snapshot_content, create_time
|
||||
)
|
||||
SELECT
|
||||
nextval('emr_revision_id_seq'),
|
||||
e.id,
|
||||
e.encounter_id,
|
||||
1,
|
||||
COALESCE(e.record_id, 1),
|
||||
'系统同步',
|
||||
'CREATE',
|
||||
'初始创建 - 从历史病历同步',
|
||||
e.context_json,
|
||||
COALESCE(e.create_time, NOW())
|
||||
FROM doc_emr e
|
||||
WHERE e.id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM emr_revision r WHERE r.emr_id = e.id
|
||||
);
|
||||
|
||||
-- 3. 从doc_emr同步搜索索引
|
||||
INSERT INTO emr_search_index (
|
||||
id, emr_id, encounter_id, patient_id, patient_name,
|
||||
emr_type, emr_title, diagnosis_text,
|
||||
doctor_name, department_name, create_time, update_time
|
||||
)
|
||||
SELECT
|
||||
nextval('emr_search_index_id_seq'),
|
||||
e.id,
|
||||
e.encounter_id,
|
||||
e.patient_id,
|
||||
COALESCE(p.name, '患者' || e.patient_id),
|
||||
CASE
|
||||
WHEN e.class_enum = 1 THEN 'OUTPATIENT'
|
||||
WHEN e.class_enum = 2 THEN 'INPATIENT'
|
||||
ELSE 'OTHER'
|
||||
END,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(BOTH '"' FROM
|
||||
SPLIT_PART(
|
||||
SPLIT_PART(e.context_json, '"chiefComplaint"', 2),
|
||||
':', 2
|
||||
)
|
||||
), ''),
|
||||
'未命名病历'
|
||||
),
|
||||
COALESCE(
|
||||
NULLIF(TRIM(BOTH '"' FROM
|
||||
SPLIT_PART(
|
||||
SPLIT_PART(e.context_json, '"diagnosis"', 2),
|
||||
':', 2
|
||||
)
|
||||
), ''),
|
||||
''
|
||||
),
|
||||
COALESCE(u.name, '医生' || e.record_id),
|
||||
COALESCE(o.name, '未知科室'),
|
||||
COALESCE(e.create_time, NOW()),
|
||||
COALESCE(e.update_time, NOW())
|
||||
FROM doc_emr e
|
||||
LEFT JOIN patient p ON e.patient_id = p.id
|
||||
LEFT JOIN sys_user u ON e.record_id = u.user_id
|
||||
LEFT JOIN adm_encounter enc ON e.encounter_id = enc.id
|
||||
LEFT JOIN sys_organization o ON enc.organization_id = o.id
|
||||
WHERE e.id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM emr_search_index si WHERE si.emr_id = e.id
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
-- V101__add_emr_sync_menu_and_permissions.sql
|
||||
-- 添加EMR数据同步菜单和医生权限
|
||||
|
||||
-- 1. 添加EMR数据同步菜单(在电子病历管理下)
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (
|
||||
'EMR数据同步',
|
||||
(SELECT menu_id FROM sys_menu WHERE menu_name = '电子病历管理' LIMIT 1),
|
||||
99,
|
||||
'sync',
|
||||
'emr/sync/index',
|
||||
'C',
|
||||
'0',
|
||||
'0',
|
||||
'emr:sync:list',
|
||||
'upload',
|
||||
'admin',
|
||||
NOW(),
|
||||
'admin',
|
||||
NOW(),
|
||||
'EMR数据同步 - 从病历表同步数据到修订历史和搜索索引'
|
||||
);
|
||||
|
||||
-- 2. 为医生角色添加EMR权限
|
||||
-- 获取医生角色ID(假设角色名为'医生'或'doctor')
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT
|
||||
r.role_id,
|
||||
m.menu_id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_menu m
|
||||
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生')
|
||||
AND m.perms IN (
|
||||
'emr:list',
|
||||
'emr:edit',
|
||||
'emr:sync:list'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
-- 3. 更新EMR相关菜单的权限(将inpatient:emr改为emr)
|
||||
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'inpatient:emr:list';
|
||||
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'inpatient:emr:edit';
|
||||
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'infection:emr:list';
|
||||
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'infection:emr:edit';
|
||||
@@ -0,0 +1,44 @@
|
||||
-- V102__grant_emr_menu_to_doctor.sql
|
||||
-- 为医生角色授予电子病历管理相关菜单权限
|
||||
|
||||
-- 1. 获取医生角色ID并授予EMR菜单权限
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT
|
||||
r.role_id,
|
||||
m.menu_id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_menu m
|
||||
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
|
||||
AND m.menu_id IN (
|
||||
20201, -- 电子病历管理
|
||||
20202, -- 病案归档
|
||||
20203, -- 修订历史
|
||||
20204, -- 病历时效
|
||||
20205, -- 病历检索
|
||||
20206, -- 进程记录
|
||||
20207 -- 知识库
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
-- 2. 为医生角色授予EMR相关权限
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT
|
||||
r.role_id,
|
||||
m.menu_id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_menu m
|
||||
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
|
||||
AND m.perms IN (
|
||||
'emr:list',
|
||||
'emr:edit',
|
||||
'document:progressnote:list',
|
||||
'document:progressnote:add',
|
||||
'document:progressnote:edit'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,22 @@
|
||||
>
|
||||
历史病历
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
style="margin-left: 20px"
|
||||
@click="viewRevisionHistory()"
|
||||
>
|
||||
修订历史
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
style="margin-left: 20px"
|
||||
@click="checkCompleteness()"
|
||||
>
|
||||
完整性检查
|
||||
</el-button>
|
||||
<!-- 可选:添加打印按钮 -->
|
||||
<!-- <el-button type="primary" plain @click="printEmr" style="margin-left: 20px">
|
||||
打印病历
|
||||
@@ -196,10 +212,13 @@
|
||||
import {getEmrDetail, saveEmr, saveEmrTemplate} from '../api';
|
||||
import emrTemplate from '../emr/emrtemplate.vue';
|
||||
import emrhistory from '../emr/emrhistory.vue';
|
||||
|
||||
import {checkCompleteness as checkEmrCompleteness} from '@/api/emr';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {computed, getCurrentInstance, ref, watch} from 'vue';
|
||||
import {formatDate as formatDateUtil} from '@/utils/index';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
patientInfo: {
|
||||
@@ -320,6 +339,37 @@ function handleSaveTemplate() {
|
||||
openEmrTemplate.value = true;
|
||||
}
|
||||
|
||||
// 查看修订历史
|
||||
function viewRevisionHistory() {
|
||||
if (!props.patientInfo?.encounterId) {
|
||||
proxy.$message.warning('请先选择患者');
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
path: '/emr/revision-history',
|
||||
query: { emrId: props.patientInfo.encounterId }
|
||||
});
|
||||
}
|
||||
|
||||
// 完整性检查
|
||||
async function checkCompleteness() {
|
||||
if (!props.patientInfo?.encounterId) {
|
||||
proxy.$message.warning('请先选择患者');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await checkEmrCompleteness(props.patientInfo.encounterId, props.patientInfo.encounterId);
|
||||
const data = res.data || res;
|
||||
if (data.isComplete) {
|
||||
proxy.$modal.msgSuccess('病历完整性检查通过');
|
||||
} else {
|
||||
proxy.$message.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`);
|
||||
}
|
||||
} catch (e) {
|
||||
proxy.$message.error('检查失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 选择模板/历史
|
||||
function templateSelect(row) {
|
||||
form.value = { ...row };
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
<div style="padding:16px">
|
||||
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="font-size:18px;font-weight:bold">病历打印归档</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="loadStats"
|
||||
>
|
||||
刷新统计
|
||||
</el-button>
|
||||
<div>
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="syncing"
|
||||
@click="handleSync"
|
||||
style="margin-right:10px"
|
||||
>
|
||||
{{ syncing ? '同步中...' : '同步历史数据' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="loadStats"
|
||||
>
|
||||
刷新统计
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-row
|
||||
:gutter="12"
|
||||
@@ -226,13 +236,34 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {ref,onMounted} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {ElMessage,ElMessageBox} from 'element-plus'
|
||||
import {getArchivePage,archive,reprint,getArchiveStats} from './api'
|
||||
import request from '@/utils/request'
|
||||
const route=useRoute()
|
||||
const tableData=ref([]);const total=ref(0);const stats=ref({})
|
||||
const syncing=ref(false)
|
||||
const q=ref({pageNo:1,pageSize:20,patientName:'',archiveStatus:''})
|
||||
const loadData=async()=>{const r=await getArchivePage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
|
||||
const loadStats=async()=>{const r=await getArchiveStats();stats.value=r.data||{}}
|
||||
const doArchive=async(row)=>{const {value}=await ElMessageBox.prompt('归档人','确认归档');if(value){await archive(row.id,value);ElMessage.success('已归档');loadData();loadStats()}}
|
||||
const doReprint=async(row)=>{await reprint(row.id);ElMessage.success('补打记录已添加');loadData()}
|
||||
onMounted(()=>{loadData();loadStats()})
|
||||
const handleSync=async()=>{
|
||||
try{
|
||||
await ElMessageBox.confirm('将从病历表同步数据到修订历史和搜索索引,确定继续?','确认同步',{type:'warning'})
|
||||
syncing.value=true
|
||||
const res=await request({url:'/emr-sync/sync',method:'post'})
|
||||
ElMessage.success(res.data||'同步完成')
|
||||
loadData();loadStats()
|
||||
}catch(e){
|
||||
if(e!=='cancel') ElMessage.error('同步失败')
|
||||
}finally{
|
||||
syncing.value=false
|
||||
}
|
||||
}
|
||||
onMounted(()=>{
|
||||
if(route.query.encounterId){q.value.encounterId=route.query.encounterId}
|
||||
if(route.query.patientName){q.value.patientName=route.query.patientName}
|
||||
loadData();loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -90,10 +90,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { checkCompleteness, getCompletenessResults } from '@/api/emr'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const checkLoading = ref(false)
|
||||
const resultLoading = ref(false)
|
||||
const checkResult = ref(null)
|
||||
@@ -148,6 +150,12 @@ const loadResults = async () => {
|
||||
resultLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.emrId) { checkForm.emrId = route.query.emrId }
|
||||
if (route.query.encounterId) { checkForm.encounterId = route.query.encounterId }
|
||||
if (checkForm.emrId && checkForm.encounterId) { handleCheck() }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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 getRevisionList(p){return request({url:'/emr-revision/list',method:'get',params:p})}
|
||||
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}})}
|
||||
export function getRevisionPage(p){return request({url:'/emr/revision/page',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}})}
|
||||
|
||||
@@ -129,11 +129,31 @@
|
||||
</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:20,emrId:'',operatorName:''})
|
||||
const q=ref({pageNo:1,pageSize:20,emrId:null,operatorName:''})
|
||||
const detailVisible=ref(false);const detail=ref({})
|
||||
const loadData=async()=>{const r=await getRevisionPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
|
||||
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)
|
||||
ElMessage.error('加载失败')
|
||||
}
|
||||
}
|
||||
const viewDetail=(row)=>{detail.value=row;detailVisible.value=true}
|
||||
onMounted(()=>loadData())
|
||||
onMounted(()=>{
|
||||
if(route.query.emrId){q.value.emrId=route.query.emrId}
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
85
healthlink-his-ui/src/views/emr/sync/index.vue
Normal file
85
healthlink-his-ui/src/views/emr/sync/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div style="padding:16px">
|
||||
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="font-size:18px;font-weight:bold">EMR数据同步</span>
|
||||
</div>
|
||||
|
||||
<el-card shadow="hover" style="margin-bottom:16px">
|
||||
<template #header>
|
||||
<span>同步统计</span>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="病历总数" :value="stats.emrCount||0" />
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-statistic title="修订历史" :value="stats.revisionCount||0" />
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-statistic title="搜索索引" :value="stats.searchIndexCount||0" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>数据同步操作</span>
|
||||
</template>
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<template #title>
|
||||
同步操作将清空现有的修订历史和搜索索引数据,然后从门诊/住院病历表(doc_emr)重新生成。
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="handleSync"
|
||||
>
|
||||
{{ syncing ? '同步中...' : '开始同步' }}
|
||||
</el-button>
|
||||
<el-button @click="loadStats">
|
||||
刷新统计
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const syncing = ref(false)
|
||||
const stats = ref({})
|
||||
|
||||
const loadStats = async () => {
|
||||
const res = await request({url: '/emr-sync/stats', method: 'get'})
|
||||
stats.value = res.data || {}
|
||||
}
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要同步EMR数据吗?这将清空现有的修订历史和搜索索引,然后从病历表重新生成。',
|
||||
'确认同步',
|
||||
{type: 'warning'}
|
||||
)
|
||||
syncing.value = true
|
||||
const res = await request({url: '/emr-sync/sync', method: 'post'})
|
||||
ElMessage.success(res.data || '同步完成')
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error('同步失败: ' + (e.message || '未知错误'))
|
||||
}
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadStats())
|
||||
</script>
|
||||
@@ -132,14 +132,20 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getTimelinessByEncounter, getOverdueList, getTimelinessStatistics } from '@/api/emr'
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const dataList = ref([])
|
||||
const stats = reactive({ pending: 0, completed: 0, overdue: 0, rate: 0 })
|
||||
const queryParams = reactive({ departmentName: '', emrType: '' })
|
||||
const queryParams = reactive({ departmentName: '', emrType: '', encounterId: '' })
|
||||
const emrTypeMap = { ADMISSION: '入院记录', FIRST_COURSE: '首次病程', DAILY_COURSE: '日常病程', DISCHARGE: '出院记录' }
|
||||
const statusMap = { PENDING: { label: '待完成', type: 'info' }, COMPLETED: { label: '已完成', type: 'success' }, OVERDUE: { label: '超时', type: 'danger' } }
|
||||
const getList = async () => { loading.value = true; const res = await getOverdueList(); dataList.value = res.data || res.rows || []; loading.value = false }
|
||||
const handleQuery = () => getList()
|
||||
onMounted(() => { getList() })
|
||||
onMounted(() => {
|
||||
if (route.query.encounterId) { queryParams.encounterId = route.query.encounterId }
|
||||
if (route.query.departmentName) { queryParams.departmentName = route.query.departmentName }
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,136 +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 {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 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: 10
|
||||
})
|
||||
|
||||
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(()=>handleSearch())
|
||||
|
||||
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: 10
|
||||
})
|
||||
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>
|
||||
|
||||
@@ -78,6 +78,20 @@
|
||||
>
|
||||
打印表单
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
@click="viewRevisionHistory"
|
||||
>
|
||||
修订历史
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
@click="checkCompleteness"
|
||||
>
|
||||
完整性检查
|
||||
</el-button>
|
||||
<!-- <el-button type="primary" @click="" disabled>病案上传</el-button>
|
||||
<el-button type="primary" @click="">结算上传</el-button> -->
|
||||
<!-- <el-button type="primary" @click="onNursingStatus">护理状态</el-button> -->
|
||||
@@ -923,6 +937,41 @@ const onPrint = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 查看修订历史
|
||||
import {useRouter} from 'vue-router';
|
||||
import {checkCompleteness as checkEmrCompleteness} from '@/api/emr';
|
||||
const router = useRouter();
|
||||
|
||||
const viewRevisionHistory = () => {
|
||||
if (!patientInfo.value?.encounterId) {
|
||||
ElMessage.warning('请先选择患者');
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
path: '/emr/revision-history',
|
||||
query: { emrId: patientInfo.value.encounterId }
|
||||
});
|
||||
};
|
||||
|
||||
// 完整性检查
|
||||
const checkCompleteness = async () => {
|
||||
if (!patientInfo.value?.encounterId) {
|
||||
ElMessage.warning('请先选择患者');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await checkEmrCompleteness(patientInfo.value.encounterId, patientInfo.value.encounterId);
|
||||
const data = res.data || res;
|
||||
if (data.isComplete) {
|
||||
ElMessage.success('病历完整性检查通过');
|
||||
} else {
|
||||
ElMessage.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`);
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('检查失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 添加一个方法供父组件调用,处理患者切换
|
||||
const handlePatientChange = (patient) => {
|
||||
// 更新患者信息
|
||||
|
||||
31
scripts/test-emr-sync.sh
Normal file
31
scripts/test-emr-sync.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# EMR数据同步测试脚本
|
||||
|
||||
BASE_URL="http://localhost:18082"
|
||||
TOKEN=""
|
||||
|
||||
echo "=== EMR数据同步测试 ==="
|
||||
|
||||
# 1. 获取统计信息
|
||||
echo ""
|
||||
echo "1. 获取同步统计..."
|
||||
curl -s "${BASE_URL}/emr-sync/stats" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" | python3 -m json.tool 2>/dev/null || echo "请先登录获取Token"
|
||||
|
||||
# 2. 执行同步
|
||||
echo ""
|
||||
echo "2. 执行数据同步..."
|
||||
curl -s -X POST "${BASE_URL}/emr-sync/sync" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" | python3 -m json.tool 2>/dev/null || echo "同步失败,请检查Token"
|
||||
|
||||
# 3. 再次获取统计
|
||||
echo ""
|
||||
echo "3. 同步后统计..."
|
||||
curl -s "${BASE_URL}/emr-sync/stats" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" | python3 -m json.tool 2>/dev/null || echo "请先登录获取Token"
|
||||
|
||||
echo ""
|
||||
echo "=== 测试完成 ==="
|
||||
Reference in New Issue
Block a user