feat: 修复会诊时限编译错误 + 新增知情同意/病程记录/院感增强模块

1. 修复 ConsultationAppServiceImpl 编译错误
   - 重写 checkTimeLimit/getTimeLimitStats/getConsultationUrgency 方法
   - 使用 ConsultationRequest 实体替代 RequestForm
   - 使用 consultationRequestMapper 替代 requestFormService

2. 新增知情同意管理模块 (V15)
   - 实体: InformedConsent (含7种同意类型、双签名、版本管理)
   - Controller: 完整CRUD + 医生签名/患者签名/拒绝/归档/作废
   - 前端: 列表页 + 手写板签名 + 拒绝弹窗

3. 新增病程记录模块 (V16)
   - 实体: ProgressNote + ProgressNoteReminder
   - Controller: CRUD + 签名/审核 + 时限监控面板 + 提醒
   - 10种记录类型(首次/日常/上级查房/疑难/阶段/抢救/转科/接收/出院/死亡)
   - 前端: 列表页 + 时限监控面板 + 超时预警

4. 院感管理增强模块 (V17)
   - 暴发预警: 预警/处理/排除流程
   - 目标性监测: ICU/手术部位/导管相关
   - 手卫生监测: 依从性统计+总体依从率
   - 多重耐药菌: 菌种/耐药类型/隔离管理
   - 环境卫生学监测: 空气/物表/手/消毒液/无菌物品
   - 前端: Tab页整合5个子模块

所有模块编译通过 (mvn clean compile -DskipTests)
This commit is contained in:
2026-06-06 16:09:20 +08:00
parent c683f4aac3
commit f68fe39897
50 changed files with 3282 additions and 2 deletions

View File

@@ -157,6 +157,16 @@ public interface IConsultationAppService {
* @return 会诊申请详情
*/
ConsultationRequestDto getConsultationById(Long id);
/**
* 检查会诊时限铁律15: 三甲要求)
*/
java.util.Map<String, Object> checkTimeLimit(String consultationId);
/**
* 获取会诊时限统计
*/
java.util.Map<String, Object> getTimeLimitStats();
}

View File

@@ -1943,5 +1943,118 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
throw new RuntimeException("查询会诊申请详情失败: " + e.getMessage());
}
}
}
/**
* 检查会诊时限铁律15: 三甲要求)
* 急会诊10分钟内到位
* 科间会诊48小时内完成
*/
public Map<String, Object> checkTimeLimit(String consultationId) {
Map<String, Object> result = new java.util.HashMap<>();
try {
var wrapper = new LambdaQueryWrapper<ConsultationRequest>();
wrapper.eq(ConsultationRequest::getConsultationId, consultationId);
ConsultationRequest request = consultationRequestMapper.selectOne(wrapper);
if (request == null) {
result.put("hasIssue", false);
result.put("message", "未找到会诊申请");
return result;
}
Date submitTime = request.getConsultationRequestDate();
if (submitTime == null) {
result.put("hasIssue", false);
result.put("message", "会诊申请尚未提交");
return result;
}
long elapsedMinutes = (System.currentTimeMillis() - submitTime.getTime()) / (1000 * 60);
String urgency = request.getConsultationUrgency();
if (urgency == null) urgency = "1";
// 急会诊10分钟内到位
if ("2".equals(urgency)) {
if (elapsedMinutes > 10) {
result.put("hasIssue", true);
result.put("type", "URGENT_OVERDUE");
result.put("message", "急会诊已超时" + (elapsedMinutes - 10) + "分钟要求10分钟内到位");
result.put("severity", "HIGH");
result.put("elapsedMinutes", elapsedMinutes);
} else {
result.put("hasIssue", false);
result.put("remainingMinutes", 10 - elapsedMinutes);
result.put("message", "急会诊剩余" + (10 - elapsedMinutes) + "分钟");
}
} else {
// 普通会诊48小时完成
long elapsedHours = elapsedMinutes / 60;
if (elapsedHours > 48) {
result.put("hasIssue", true);
result.put("type", "NORMAL_OVERDUE");
result.put("message", "科间会诊已超时" + (elapsedHours - 48) + "小时要求48小时内完成");
result.put("severity", "MEDIUM");
result.put("elapsedHours", elapsedHours);
} else {
result.put("hasIssue", false);
result.put("remainingHours", 48 - elapsedHours);
result.put("message", "科间会诊剩余" + (48 - elapsedHours) + "小时");
}
}
} catch (Exception e) {
log.error("检查会诊时限异常: {}", e.getMessage(), e);
result.put("hasIssue", false);
result.put("error", e.getMessage());
}
return result;
}
/**
* 获取会诊时限统计
*/
public Map<String, Object> getTimeLimitStats() {
Map<String, Object> stats = new java.util.HashMap<>();
try {
Date now = new Date();
long nowMs = now.getTime();
// 查询所有已提交或已确认的会诊status=10已提交, 20已确认
var wrapper = new LambdaQueryWrapper<ConsultationRequest>();
wrapper.in(ConsultationRequest::getConsultationStatus, ConsultationStatusEnum.SUBMITTED.getCode(),
ConsultationStatusEnum.CONFIRMED.getCode());
List<ConsultationRequest> pendingList = consultationRequestMapper.selectList(wrapper);
int urgentOverdue = 0;
int urgentNormal = 0;
int normalOverdue = 0;
int normalNormal = 0;
for (ConsultationRequest req : pendingList) {
Date submitTime = req.getConsultationRequestDate();
if (submitTime == null) continue;
long elapsedMin = (nowMs - submitTime.getTime()) / (1000 * 60);
String urgency = req.getConsultationUrgency();
if (urgency == null) urgency = "1";
if ("2".equals(urgency)) {
if (elapsedMin > 10) urgentOverdue++;
else urgentNormal++;
} else {
if (elapsedMin > 48 * 60) normalOverdue++;
else normalNormal++;
}
}
stats.put("urgentOverdue", urgentOverdue);
stats.put("urgentNormal", urgentNormal);
stats.put("normalOverdue", normalOverdue);
stats.put("normalNormal", normalNormal);
stats.put("totalPending", pendingList.size());
} catch (Exception e) {
log.error("获取会诊时限统计异常: {}", e.getMessage(), e);
}
return stats;
}
}

View File

@@ -319,5 +319,39 @@ public class ConsultationController {
return R.fail("查询会诊申请详情失败: " + e.getMessage());
}
}
}
// ==================== 时限控制相关接口铁律15: 三甲要求)====================
/**
* 检查会诊时限
*/
@Operation(summary = "检查会诊时限")
@GetMapping("/time-limit/check")
public R<Map<String, Object>> checkTimeLimit(
@Parameter(description = "会诊申请单号") @RequestParam String consultationId) {
try {
Map<String, Object> result = consultationAppService.checkTimeLimit(consultationId);
return R.ok(result);
} catch (Exception e) {
log.error("检查会诊时限失败", e);
return R.fail("检查会诊时限失败: " + e.getMessage());
}
}
/**
* 获取会诊时限统计
*/
@Operation(summary = "获取会诊时限统计")
@GetMapping("/time-limit/stats")
public R<Map<String, Object>> getTimeLimitStats() {
try {
Map<String, Object> stats = consultationAppService.getTimeLimitStats();
return R.ok(stats);
} catch (Exception e) {
log.error("获取会诊时限统计失败", e);
return R.fail("获取会诊时限统计失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,223 @@
package com.healthlink.his.web.document.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.document.domain.InformedConsent;
import com.healthlink.his.document.service.IInformedConsentService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 知情同意书Controller
* 依据: 《医疗纠纷预防和处理条例》患者知情同意权
*/
@RestController
@RequestMapping("/informed-consent")
@Slf4j
@AllArgsConstructor
public class InformedConsentController {
private final IInformedConsentService consentService;
/**
* 分页查询知情同意书列表
*/
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "consentType", required = false) Integer consentType,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<InformedConsent> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(patientName), InformedConsent::getPatientName, patientName)
.eq(consentType != null, InformedConsent::getConsentType, consentType)
.eq(status != null, InformedConsent::getStatus, status)
.eq(encounterId != null, InformedConsent::getEncounterId, encounterId)
.orderByDesc(InformedConsent::getCreateTime);
return R.ok(consentService.page(new Page<>(pageNo, pageSize), wrapper));
}
/**
* 查询知情同意书详情
*/
@GetMapping("/detail")
public R<?> getDetail(@RequestParam Long id) {
InformedConsent consent = consentService.getById(id);
if (consent == null) {
return R.fail("知情同意书不存在");
}
return R.ok(consent);
}
/**
* 新增知情同意书(草稿)
*/
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody InformedConsent consent) {
consent.setStatus(0); // 草稿
consent.setPatientSignStatus(0); // 未签名
consent.setVersion(1);
consent.setCreateTime(new Date());
consentService.save(consent);
return R.ok(consent);
}
/**
* 修改知情同意书
*/
@PutMapping("/update")
@Transactional(rollbackFor = Exception.class)
public R<?> update(@RequestBody InformedConsent consent) {
InformedConsent existing = consentService.getById(consent.getId());
if (existing == null) {
return R.fail("知情同意书不存在");
}
if (existing.getStatus() != 0) {
return R.fail("只有草稿状态可以修改");
}
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 删除知情同意书(仅草稿状态)
*/
@DeleteMapping("/delete")
@Transactional(rollbackFor = Exception.class)
public R<?> delete(@RequestParam Long id) {
InformedConsent consent = consentService.getById(id);
if (consent == null) {
return R.fail("知情同意书不存在");
}
if (consent.getStatus() != 0) {
return R.fail("只有草稿状态可以删除");
}
consentService.removeById(id);
return R.ok();
}
/**
* 医生签名 → 发送给患者
*/
@PostMapping("/doctor-sign")
@Transactional(rollbackFor = Exception.class)
public R<?> doctorSign(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String signImage = (String) params.get("signImage");
InformedConsent consent = consentService.getById(id);
if (consent == null) return R.fail("知情同意书不存在");
if (consent.getStatus() != 0) return R.fail("当前状态不允许签名");
consent.setDoctorSignTime(new Date());
consent.setDoctorSignImage(signImage);
consent.setStatus(1); // 待患者签名
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 患者签名
*/
@PostMapping("/patient-sign")
@Transactional(rollbackFor = Exception.class)
public R<?> patientSign(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String signImage = (String) params.get("signImage");
InformedConsent consent = consentService.getById(id);
if (consent == null) return R.fail("知情同意书不存在");
if (consent.getStatus() != 1) return R.fail("当前状态不接受患者签名");
consent.setPatientSignStatus(1); // 已签
consent.setPatientSignTime(new Date());
consent.setPatientSignImage(signImage);
consent.setStatus(2); // 已完成
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 患者拒绝签署
*/
@PostMapping("/patient-reject")
@Transactional(rollbackFor = Exception.class)
public R<?> patientReject(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String reason = (String) params.get("rejectReason");
String witness = (String) params.get("witnessName");
InformedConsent consent = consentService.getById(id);
if (consent == null) return R.fail("知情同意书不存在");
consent.setPatientSignStatus(2); // 拒绝
consent.setRejectReason(reason);
consent.setWitnessName(witness);
consent.setStatus(4); // 已作废
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 归档到病历
*/
@PostMapping("/archive")
@Transactional(rollbackFor = Exception.class)
public R<?> archive(@RequestParam Long id) {
InformedConsent consent = consentService.getById(id);
if (consent == null) return R.fail("知情同意书不存在");
if (consent.getStatus() != 2) return R.fail("只有已完成状态可以归档");
consent.setStatus(3); // 已归档
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 作废知情同意书
*/
@PostMapping("/void")
@Transactional(rollbackFor = Exception.class)
public R<?> voidConsent(@RequestParam Long id, @RequestParam(required = false) String reason) {
InformedConsent consent = consentService.getById(id);
if (consent == null) return R.fail("知情同意书不存在");
if (consent.getStatus() >= 3) return R.fail("已归档/已作废的知情同意书不能再次作废");
consent.setStatus(4); // 已作废
consent.setOtherNotes(consent.getOtherNotes() != null ? consent.getOtherNotes() + "\n作废原因: " + reason : "作废原因: " + reason);
consent.setUpdateTime(new Date());
consentService.updateById(consent);
return R.ok();
}
/**
* 获取统计信息
*/
@GetMapping("/stats")
public R<?> getStats(@RequestParam Long encounterId) {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<InformedConsent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(InformedConsent::getEncounterId, encounterId);
stats.put("total", consentService.count(wrapper));
wrapper.eq(InformedConsent::getStatus, 1);
stats.put("pendingSign", consentService.count(wrapper));
wrapper.eq(InformedConsent::getStatus, 2);
stats.put("completed", consentService.count(wrapper));
wrapper.eq(InformedConsent::getStatus, 3);
stats.put("archived", consentService.count(wrapper));
return R.ok(stats);
}
}

View File

@@ -0,0 +1,271 @@
package com.healthlink.his.web.document.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.document.domain.ProgressNote;
import com.healthlink.his.document.domain.ProgressNoteReminder;
import com.healthlink.his.document.service.IProgressNoteReminderService;
import com.healthlink.his.document.service.IProgressNoteService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 病程记录Controller
* 依据: 《病历书写基本规范》
*/
@RestController
@RequestMapping("/progress-note")
@Slf4j
@AllArgsConstructor
public class ProgressNoteController {
private final IProgressNoteService progressNoteService;
private final IProgressNoteReminderService reminderService;
/** 记录类型时限(小时) */
private static final Map<Integer, Integer> TYPE_DEADLINE_HOURS = Map.of(
1, 8, // 首次病程记录: 8小时
2, 72, // 日常病程记录: 3天(一般)
3, 72, // 上级查房: 72小时
5, 720, // 阶段小结: 30天
6, 6, // 抢救记录: 6小时
9, 24, // 出院记录: 24小时
10, 168 // 死亡讨论: 7天
);
/** 病情等级对应的日常病程频率(小时) */
private static final Map<Integer, Integer> CONDITION_FREQUENCY = Map.of(
1, 24, // 病危: 每天1次
2, 48, // 病重: 2天1次
3, 72 // 一般: 3天1次
);
/**
* 分页查询病程记录列表
*/
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "noteType", required = false) Integer noteType,
@RequestParam(value = "authorName", required = false) String authorName,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "isOverdue", required = false) Boolean isOverdue,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(patientName), ProgressNote::getPatientName, patientName)
.eq(noteType != null, ProgressNote::getNoteType, noteType)
.like(StringUtils.hasText(authorName), ProgressNote::getAuthorName, authorName)
.eq(encounterId != null, ProgressNote::getEncounterId, encounterId)
.eq(isOverdue != null, ProgressNote::getIsOverdue, isOverdue)
.orderByDesc(ProgressNote::getCreateTime);
return R.ok(progressNoteService.page(new Page<>(pageNo, pageSize), wrapper));
}
/**
* 查询病程记录详情
*/
@GetMapping("/detail")
public R<?> getDetail(@RequestParam Long id) {
ProgressNote note = progressNoteService.getById(id);
if (note == null) return R.fail("病程记录不存在");
return R.ok(note);
}
/**
* 新增病程记录
*/
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody ProgressNote note) {
note.setSignStatus(0);
note.setIsOverdue(false);
note.setOverdueHours(0);
note.setVersion(1);
// 计算时限
Integer hours = TYPE_DEADLINE_HOURS.getOrDefault(note.getNoteType(), 72);
Date now = new Date();
note.setDeadline(new Date(now.getTime() + TimeUnit.HOURS.toMillis(hours)));
note.setCreateTime(now);
progressNoteService.save(note);
// 创建提醒记录
createReminder(note);
return R.ok(note);
}
/**
* 修改病程记录(仅未签名可修改)
*/
@PutMapping("/update")
@Transactional(rollbackFor = Exception.class)
public R<?> update(@RequestBody ProgressNote note) {
ProgressNote existing = progressNoteService.getById(note.getId());
if (existing == null) return R.fail("病程记录不存在");
if (existing.getSignStatus() == 1) return R.fail("已签名的病程记录不能修改");
note.setVersion(existing.getVersion() + 1);
note.setUpdateTime(new Date());
progressNoteService.updateById(note);
return R.ok();
}
/**
* 删除病程记录(仅未签名可删除)
*/
@DeleteMapping("/delete")
@Transactional(rollbackFor = Exception.class)
public R<?> delete(@RequestParam Long id) {
ProgressNote note = progressNoteService.getById(id);
if (note == null) return R.fail("病程记录不存在");
if (note.getSignStatus() == 1) return R.fail("已签名的病程记录不能删除");
progressNoteService.removeById(id);
return R.ok();
}
/**
* 签名病程记录
*/
@PostMapping("/sign")
@Transactional(rollbackFor = Exception.class)
public R<?> sign(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
ProgressNote note = progressNoteService.getById(id);
if (note == null) return R.fail("病程记录不存在");
if (note.getSignStatus() == 1) return R.fail("已签名");
note.setSignStatus(1);
note.setSignTime(new Date());
note.setUpdateTime(new Date());
progressNoteService.updateById(note);
// 更新提醒状态
updateReminderStatus(note.getEncounterId(), note.getNoteType(), 1);
return R.ok();
}
/**
* 审核病程记录(上级医师)
*/
@PostMapping("/review")
@Transactional(rollbackFor = Exception.class)
public R<?> review(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String reviewerName = (String) params.get("reviewUserName");
Long reviewerId = Long.valueOf(params.get("reviewUserId").toString());
ProgressNote note = progressNoteService.getById(id);
if (note == null) return R.fail("病程记录不存在");
note.setReviewUserId(reviewerId);
note.setReviewUserName(reviewerName);
note.setUpdateTime(new Date());
progressNoteService.updateById(note);
return R.ok();
}
/**
* 获取时限监控面板
*/
@GetMapping("/monitor")
public R<?> getMonitor(@RequestParam(required = false) Long encounterId) {
Map<String, Object> result = new HashMap<>();
Date now = new Date();
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
if (encounterId != null) {
wrapper.eq(ProgressNote::getEncounterId, encounterId);
}
List<ProgressNote> allNotes = progressNoteService.list(wrapper);
int overdueCount = 0;
int warningCount = 0; // 即将超时(2小时内)
int normalCount = 0;
for (ProgressNote note : allNotes) {
if (note.getSignStatus() == 1) {
normalCount++;
continue;
}
long remainingMs = note.getDeadline().getTime() - now.getTime();
long remainingHours = remainingMs / (1000 * 60 * 60);
if (remainingMs < 0) {
overdueCount++;
// 更新超时状态
if (!Boolean.TRUE.equals(note.getIsOverdue())) {
note.setIsOverdue(true);
note.setOverdueHours((int)(-remainingHours));
progressNoteService.updateById(note);
}
} else if (remainingHours <= 2) {
warningCount++;
} else {
normalCount++;
}
}
result.put("overdueCount", overdueCount);
result.put("warningCount", warningCount);
result.put("normalCount", normalCount);
result.put("totalCount", allNotes.size());
return R.ok(result);
}
/**
* 获取提醒列表
*/
@GetMapping("/reminders")
public R<?> getReminders(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "encounterId", required = false) Long encounterId) {
LambdaQueryWrapper<ProgressNoteReminder> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(status != null, ProgressNoteReminder::getStatus, status)
.eq(encounterId != null, ProgressNoteReminder::getEncounterId, encounterId)
.orderByAsc(ProgressNoteReminder::getDeadline);
return R.ok(reminderService.list(wrapper));
}
/**
* 获取病程记录统计
*/
@GetMapping("/stats")
public R<?> getStats(@RequestParam Long encounterId) {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProgressNote::getEncounterId, encounterId);
stats.put("total", progressNoteService.count(wrapper));
wrapper.eq(ProgressNote::getSignStatus, 0);
stats.put("unsigned", progressNoteService.count(wrapper));
wrapper.eq(ProgressNote::getSignStatus, 1);
stats.put("signed", progressNoteService.count(wrapper));
wrapper.eq(ProgressNote::getIsOverdue, true);
stats.put("overdue", progressNoteService.count(wrapper));
return R.ok(stats);
}
private void createReminder(ProgressNote note) {
ProgressNoteReminder reminder = new ProgressNoteReminder();
reminder.setEncounterId(note.getEncounterId());
reminder.setPatientName(note.getPatientName());
reminder.setNoteType(note.getNoteType());
reminder.setDeadline(note.getDeadline());
reminder.setStatus(0);
reminder.setRemindUserId(note.getAuthorUserId());
reminder.setRemindUserName(note.getAuthorName());
reminder.setCreatedTime(new Date());
reminderService.save(reminder);
}
private void updateReminderStatus(Long encounterId, Integer noteType, int status) {
LambdaQueryWrapper<ProgressNoteReminder> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProgressNoteReminder::getEncounterId, encounterId)
.eq(ProgressNoteReminder::getNoteType, noteType)
.eq(ProgressNoteReminder::getStatus, 0);
ProgressNoteReminder reminder = reminderService.getOne(wrapper);
if (reminder != null) {
reminder.setStatus(status);
reminderService.updateById(reminder);
}
}
}

View File

@@ -0,0 +1,271 @@
package com.healthlink.his.web.infection.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.infection.domain.*;
import com.healthlink.his.infection.service.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* 院感管理增强Controller
* 补全: 暴发预警、目标性监测、手卫生监测、多重耐药菌、环境卫生学监测
*/
@RestController
@RequestMapping("/infection-enhanced")
@Slf4j
@AllArgsConstructor
public class InfectionEnhancedController {
private final IOutbreakWarningService outbreakService;
private final ITargetedSurveillanceService surveillanceService;
private final IHandHygieneService handHygieneService;
private final IMultiDrugResistantService mdrService;
private final IEnvironmentalMonitorService envMonitorService;
// ==================== 暴发预警 ====================
@GetMapping("/outbreak/page")
public R<?> getOutbreakPage(
@RequestParam(value = "departmentName", required = false) String departmentName,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<OutbreakWarning> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(departmentName), OutbreakWarning::getDepartmentName, departmentName)
.eq(status != null, OutbreakWarning::getStatus, status)
.orderByDesc(OutbreakWarning::getCreateTime);
return R.ok(outbreakService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/outbreak/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addOutbreak(@RequestBody OutbreakWarning warning) {
warning.setStatus(0);
warning.setCreateTime(new Date());
outbreakService.save(warning);
return R.ok(warning);
}
@PostMapping("/outbreak/handle")
@Transactional(rollbackFor = Exception.class)
public R<?> handleOutbreak(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String result = (String) params.get("handleResult");
OutbreakWarning warning = outbreakService.getById(id);
if (warning == null) return R.fail("预警记录不存在");
warning.setStatus(2);
warning.setHandleResult(result);
warning.setHandleTime(new Date());
warning.setUpdateTime(new Date());
outbreakService.updateById(warning);
return R.ok();
}
@PostMapping("/outbreak/exclude")
@Transactional(rollbackFor = Exception.class)
public R<?> excludeOutbreak(@RequestParam Long id, @RequestParam(required = false) String reason) {
OutbreakWarning warning = outbreakService.getById(id);
if (warning == null) return R.fail("预警记录不存在");
warning.setStatus(3);
warning.setHandleResult("排除: " + (reason != null ? reason : "误报"));
warning.setHandleTime(new Date());
warning.setUpdateTime(new Date());
outbreakService.updateById(warning);
return R.ok();
}
// ==================== 目标性监测 ====================
@GetMapping("/surveillance/page")
public R<?> getSurveillancePage(
@RequestParam(value = "surveillanceType", required = false) Integer type,
@RequestParam(value = "departmentName", required = false) String deptName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<TargetedSurveillance> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(type != null, TargetedSurveillance::getSurveillanceType, type)
.like(StringUtils.hasText(deptName), TargetedSurveillance::getDepartmentName, deptName)
.orderByDesc(TargetedSurveillance::getStartDate);
return R.ok(surveillanceService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/surveillance/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addSurveillance(@RequestBody TargetedSurveillance sv) {
sv.setStatus(0);
sv.setTotalCases(0);
sv.setInfectionCases(0);
sv.setInfectionRate(BigDecimal.ZERO);
sv.setCreateTime(new Date());
surveillanceService.save(sv);
return R.ok(sv);
}
@PostMapping("/surveillance/update-stats")
@Transactional(rollbackFor = Exception.class)
public R<?> updateSurveillanceStats(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
Integer totalCases = Integer.valueOf(params.get("totalCases").toString());
Integer infectionCases = Integer.valueOf(params.get("infectionCases").toString());
TargetedSurveillance sv = surveillanceService.getById(id);
if (sv == null) return R.fail("监测记录不存在");
sv.setTotalCases(totalCases);
sv.setInfectionCases(infectionCases);
if (totalCases > 0) {
sv.setInfectionRate(BigDecimal.valueOf(infectionCases)
.divide(BigDecimal.valueOf(totalCases), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP));
}
sv.setUpdateTime(new Date());
surveillanceService.updateById(sv);
return R.ok();
}
// ==================== 手卫生监测 ====================
@GetMapping("/hand-hygiene/page")
public R<?> getHandHygienePage(
@RequestParam(value = "departmentName", required = false) String deptName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<HandHygiene> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(deptName), HandHygiene::getDepartmentName, deptName)
.orderByDesc(HandHygiene::getMonitorDate);
return R.ok(handHygieneService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/hand-hygiene/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addHandHygiene(@RequestBody HandHygiene hh) {
if (hh.getObserveCount() != null && hh.getObserveCount() > 0 && hh.getComplyCount() != null) {
hh.setComplyRate(BigDecimal.valueOf(hh.getComplyCount())
.divide(BigDecimal.valueOf(hh.getObserveCount()), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP));
}
hh.setCreateTime(new Date());
handHygieneService.save(hh);
return R.ok(hh);
}
@GetMapping("/hand-hygiene/stats")
public R<?> getHandHygieneStats(@RequestParam(required = false) Long departmentId) {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<HandHygiene> wrapper = new LambdaQueryWrapper<>();
if (departmentId != null) {
wrapper.eq(HandHygiene::getDepartmentId, departmentId);
}
List<HandHygiene> list = handHygieneService.list(wrapper);
int totalObserve = 0, totalComply = 0;
for (HandHygiene hh : list) {
totalObserve += hh.getObserveCount() != null ? hh.getObserveCount() : 0;
totalComply += hh.getComplyCount() != null ? hh.getComplyCount() : 0;
}
stats.put("totalObserve", totalObserve);
stats.put("totalComply", totalComply);
stats.put("overallRate", totalObserve > 0 ?
BigDecimal.valueOf(totalComply).divide(BigDecimal.valueOf(totalObserve), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
stats.put("recordCount", list.size());
return R.ok(stats);
}
// ==================== 多重耐药菌 ====================
@GetMapping("/mdr/page")
public R<?> getMdrPage(
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "bacteriaName", required = false) String bacteriaName,
@RequestParam(value = "isolationStatus", required = false) Integer isolationStatus,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<MultiDrugResistant> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(patientName), MultiDrugResistant::getPatientName, patientName)
.like(StringUtils.hasText(bacteriaName), MultiDrugResistant::getBacteriaName, bacteriaName)
.eq(isolationStatus != null, MultiDrugResistant::getIsolationStatus, isolationStatus)
.orderByDesc(MultiDrugResistant::getReportDate);
return R.ok(mdrService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/mdr/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addMdr(@RequestBody MultiDrugResistant mdr) {
mdr.setIsolationStatus(0);
mdr.setStatus(0);
mdr.setCreateTime(new Date());
mdrService.save(mdr);
return R.ok(mdr);
}
@PostMapping("/mdr/isolate")
@Transactional(rollbackFor = Exception.class)
public R<?> isolateMdr(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
MultiDrugResistant mdr = mdrService.getById(id);
if (mdr == null) return R.fail("记录不存在");
mdr.setIsolationStatus(1);
mdr.setIsolationStartDate(new Date());
mdr.setUpdateTime(new Date());
mdrService.updateById(mdr);
return R.ok();
}
@PostMapping("/mdr/release")
@Transactional(rollbackFor = Exception.class)
public R<?> releaseMdr(@RequestParam Long id) {
MultiDrugResistant mdr = mdrService.getById(id);
if (mdr == null) return R.fail("记录不存在");
mdr.setIsolationStatus(2);
mdr.setIsolationEndDate(new Date());
mdr.setUpdateTime(new Date());
mdrService.updateById(mdr);
return R.ok();
}
// ==================== 环境卫生学监测 ====================
@GetMapping("/env-monitor/page")
public R<?> getEnvMonitorPage(
@RequestParam(value = "departmentName", required = false) String deptName,
@RequestParam(value = "monitorType", required = false) String monitorType,
@RequestParam(value = "result", required = false) String result,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<EnvironmentalMonitor> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(deptName), EnvironmentalMonitor::getDepartmentName, deptName)
.eq(StringUtils.hasText(monitorType), EnvironmentalMonitor::getMonitorType, monitorType)
.eq(StringUtils.hasText(result), EnvironmentalMonitor::getResult, result)
.orderByDesc(EnvironmentalMonitor::getMonitorDate);
return R.ok(envMonitorService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/env-monitor/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addEnvMonitor(@RequestBody EnvironmentalMonitor env) {
env.setCreateTime(new Date());
envMonitorService.save(env);
return R.ok(env);
}
@GetMapping("/env-monitor/stats")
public R<?> getEnvMonitorStats() {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<EnvironmentalMonitor> wrapper = new LambdaQueryWrapper<>();
stats.put("total", envMonitorService.count(wrapper));
wrapper.eq(EnvironmentalMonitor::getResult, "合格");
stats.put("qualified", envMonitorService.count(wrapper));
wrapper.eq(EnvironmentalMonitor::getResult, "不合格");
stats.put("unqualified", envMonitorService.count(wrapper));
return R.ok(stats);
}
}

View File

@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS sys_preop_discussion (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
surgery_id BIGINT,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50),
discussion_type INT DEFAULT 1,
surgery_level INT,
preop_diagnosis TEXT,
surgery_name VARCHAR(200),
surgery_indication TEXT,
main_plan TEXT,
backup_plan TEXT,
anesthesia_type VARCHAR(50),
risks_and_countermeasures TEXT,
postop_notes TEXT,
discussion_conclusion INT,
discussion_result TEXT,
host_user_id BIGINT,
host_user_name VARCHAR(50),
status INT DEFAULT 0,
discussion_time TIMESTAMP,
discussion_location VARCHAR(200),
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_preop_encounter ON sys_preop_discussion(encounter_id);
CREATE INDEX IF NOT EXISTS idx_preop_surgery ON sys_preop_discussion(surgery_id);
CREATE INDEX IF NOT EXISTS idx_preop_status ON sys_preop_discussion(status);
CREATE TABLE IF NOT EXISTS sys_preop_participant (
id BIGSERIAL PRIMARY KEY,
discussion_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
user_name VARCHAR(50),
role VARCHAR(20),
title VARCHAR(50),
sign_status INT DEFAULT 0,
sign_time TIMESTAMP,
sign_image TEXT,
opinion TEXT,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_preop_part_disc ON sys_preop_participant(discussion_id);

View File

@@ -0,0 +1,54 @@
-- V15: 知情同意管理模块
-- 依据: 《医疗纠纷预防和处理条例》《侵权责任法》患者知情同意权
CREATE TABLE IF NOT EXISTS sys_informed_consent (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50) NOT NULL,
consent_type INT NOT NULL,
related_surgery_id BIGINT,
related_advice_id BIGINT,
diagnosis TEXT,
procedure_name VARCHAR(200),
procedure_purpose TEXT,
procedure_method TEXT,
expected_outcome TEXT,
risks_and_complications TEXT,
alternative_plans TEXT,
consequences_of_refusal TEXT,
other_notes TEXT,
doctor_user_id BIGINT NOT NULL,
doctor_name VARCHAR(50) NOT NULL,
doctor_sign_time TIMESTAMP,
doctor_sign_image TEXT,
patient_sign_status INT NOT NULL DEFAULT 0,
patient_sign_time TIMESTAMP,
patient_sign_image TEXT,
guardian_name VARCHAR(50),
guardian_relation VARCHAR(20),
witness_name VARCHAR(50),
reject_reason TEXT,
status INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
remark VARCHAR(500)
);
COMMENT ON TABLE sys_informed_consent IS '知情同意书';
COMMENT ON COLUMN sys_informed_consent.encounter_id IS '就诊ID';
COMMENT ON COLUMN sys_informed_consent.patient_id IS '患者ID';
COMMENT ON COLUMN sys_informed_consent.consent_type IS '类型(1手术 2麻醉 3输血 4特殊检查 5特殊治疗 6病危 7自费)';
COMMENT ON COLUMN sys_informed_consent.status IS '状态(0草稿 1待患者签名 2已完成 3已归档 4已作废)';
COMMENT ON COLUMN sys_informed_consent.patient_sign_status IS '患者签名状态(0未签 1已签 2拒绝)';
COMMENT ON COLUMN sys_informed_consent.version IS '版本号';
CREATE INDEX idx_ic_encounter ON sys_informed_consent(encounter_id);
CREATE INDEX idx_ic_patient ON sys_informed_consent(patient_id);
CREATE INDEX idx_ic_type ON sys_informed_consent(consent_type);
CREATE INDEX idx_ic_status ON sys_informed_consent(status);

View File

@@ -0,0 +1,58 @@
-- V16: 病程记录管理模块
-- 依据: 《病历书写基本规范》《电子病历应用管理规范》
CREATE TABLE IF NOT EXISTS sys_progress_note (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50) NOT NULL,
note_type INT NOT NULL,
note_content TEXT,
author_user_id BIGINT NOT NULL,
author_name VARCHAR(50) NOT NULL,
author_title VARCHAR(50),
review_user_id BIGINT,
review_user_name VARCHAR(50),
sign_status INT NOT NULL DEFAULT 0,
sign_time TIMESTAMP,
deadline TIMESTAMP NOT NULL,
is_overdue BOOLEAN NOT NULL DEFAULT FALSE,
overdue_hours INT DEFAULT 0,
template_id BIGINT,
version INT NOT NULL DEFAULT 1,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
remark VARCHAR(500)
);
COMMENT ON TABLE sys_progress_note IS '病程记录';
COMMENT ON COLUMN sys_progress_note.note_type IS '类型(1首次 2日常 3上级查房 4疑难讨论 5阶段小结 6抢救 7转科 8接收 9出院 10死亡)';
COMMENT ON COLUMN sys_progress_note.sign_status IS '签名状态(0未签 1已签)';
COMMENT ON COLUMN sys_progress_note.is_overdue IS '是否超时';
CREATE INDEX idx_pn_encounter ON sys_progress_note(encounter_id);
CREATE INDEX idx_pn_patient ON sys_progress_note(patient_id);
CREATE INDEX idx_pn_type ON sys_progress_note(note_type);
CREATE INDEX idx_pn_deadline ON sys_progress_note(deadline);
CREATE TABLE IF NOT EXISTS sys_progress_note_reminder (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_name VARCHAR(50) NOT NULL,
note_type INT NOT NULL,
deadline TIMESTAMP NOT NULL,
status INT NOT NULL DEFAULT 0,
remind_user_id BIGINT,
remind_user_name VARCHAR(50),
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE sys_progress_note_reminder IS '病程记录提醒';
COMMENT ON COLUMN sys_progress_note_reminder.status IS '状态(0待书写 1已书写 2已超时 3已提醒)';
CREATE INDEX idx_pnr_encounter ON sys_progress_note_reminder(encounter_id);
CREATE INDEX idx_pnr_status ON sys_progress_note_reminder(status);

View File

@@ -0,0 +1,132 @@
-- V17: 院感管理增强模块
-- 补全: 暴发预警、目标性监测、手卫生监测、多重耐药菌、环境卫生学监测
-- 1. 暴发预警表
CREATE TABLE IF NOT EXISTS hir_outbreak_warning (
id BIGSERIAL PRIMARY KEY,
department_id BIGINT NOT NULL,
department_name VARCHAR(100) NOT NULL,
infection_type VARCHAR(100) NOT NULL,
case_count INT NOT NULL DEFAULT 0,
warning_level VARCHAR(20) NOT NULL DEFAULT 'YELLOW',
time_range_days INT NOT NULL DEFAULT 7,
threshold_count INT NOT NULL DEFAULT 3,
status INT NOT NULL DEFAULT 0,
handle_user_id BIGINT,
handle_user_name VARCHAR(50),
handle_time TIMESTAMP,
handle_result TEXT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hir_outbreak_warning IS '院感暴发预警';
COMMENT ON COLUMN hir_outbreak_warning.warning_level IS '预警级别(YELLOW/RED)';
COMMENT ON COLUMN hir_outbreak_warning.status IS '状态(0待处理 1处理中 2已处理 3已排除)';
-- 2. 目标性监测表
CREATE TABLE IF NOT EXISTS hir_targeted_surveillance (
id BIGSERIAL PRIMARY KEY,
surveillance_type INT NOT NULL,
department_id BIGINT NOT NULL,
department_name VARCHAR(100) NOT NULL,
monitor_object VARCHAR(200) NOT NULL,
monitor_item VARCHAR(200),
start_date DATE NOT NULL,
end_date DATE,
total_cases INT NOT NULL DEFAULT 0,
infection_cases INT NOT NULL DEFAULT 0,
infection_rate DECIMAL(5,2) DEFAULT 0,
status INT NOT NULL DEFAULT 0,
report_content TEXT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hir_targeted_surveillance IS '目标性监测';
COMMENT ON COLUMN hir_targeted_surveillance.surveillance_type IS '监测类型(1ICU 2手术部位 3导管相关 4其他)';
-- 3. 手卫生监测表
CREATE TABLE IF NOT EXISTS hir_hand_hygiene (
id BIGSERIAL PRIMARY KEY,
department_id BIGINT NOT NULL,
department_name VARCHAR(100) NOT NULL,
monitor_date DATE NOT NULL,
observe_count INT NOT NULL DEFAULT 0,
comply_count INT NOT NULL DEFAULT 0,
comply_rate DECIMAL(5,2) DEFAULT 0,
observer_name VARCHAR(50),
remarks TEXT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hir_hand_hygiene IS '手卫生依从性监测';
-- 4. 多重耐药菌表
CREATE TABLE IF NOT EXISTS hir_multi_drug_resistant (
id BIGSERIAL PRIMARY KEY,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50) NOT NULL,
encounter_id BIGINT,
department_id BIGINT NOT NULL,
department_name VARCHAR(100) NOT NULL,
bacteria_name VARCHAR(200) NOT NULL,
resistance_type VARCHAR(200) NOT NULL,
specimen_type VARCHAR(50),
specimen_date DATE,
report_date DATE NOT NULL,
isolation_status INT NOT NULL DEFAULT 0,
isolation_start_date DATE,
isolation_end_date DATE,
treatment_plan TEXT,
outcome VARCHAR(50),
status INT NOT NULL DEFAULT 0,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hir_multi_drug_resistant IS '多重耐药菌管理';
COMMENT ON COLUMN hir_multi_drug_resistant.isolation_status IS '隔离状态(0未隔离 1已隔离 2解除隔离)';
-- 5. 环境卫生学监测表
CREATE TABLE IF NOT EXISTS hir_environmental_monitor (
id BIGSERIAL PRIMARY KEY,
department_id BIGINT NOT NULL,
department_name VARCHAR(100) NOT NULL,
monitor_type VARCHAR(50) NOT NULL,
monitor_item VARCHAR(200) NOT NULL,
monitor_date DATE NOT NULL,
standard_value VARCHAR(100),
actual_value VARCHAR(100),
result VARCHAR(20) NOT NULL,
tester_name VARCHAR(50),
remarks TEXT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hir_environmental_monitor IS '环境卫生学监测';
COMMENT ON COLUMN hir_environmental_monitor.monitor_type IS '监测类型(1空气 2物表 3手 4消毒液 5无菌物品)';
COMMENT ON COLUMN hir_environmental_monitor.result IS '结果(合格/不合格)';
CREATE INDEX idx_outbreak_dept ON hir_outbreak_warning(department_id);
CREATE INDEX idx_surveillance_type ON hir_targeted_surveillance(surveillance_type);
CREATE INDEX idx_hand_hygiene_date ON hir_hand_hygiene(monitor_date);
CREATE INDEX idx_mdr_patient ON hir_multi_drug_resistant(patient_id);
CREATE INDEX idx_env_dept ON hir_environmental_monitor(department_id);

View File

@@ -0,0 +1,136 @@
package com.healthlink.his.document.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 知情同意书实体类
* 依据: 《医疗纠纷预防和处理条例》
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_informed_consent")
public class InformedConsent extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/** 就诊ID */
@TableField("encounter_id")
private Long encounterId;
/** 患者ID */
@TableField("patient_id")
private Long patientId;
/** 患者姓名 */
@TableField("patient_name")
private String patientName;
/** 类型(1手术 2麻醉 3输血 4特殊检查 5特殊治疗 6病危 7自费) */
@TableField("consent_type")
private Integer consentType;
/** 关联手术ID */
@TableField("related_surgery_id")
private Long relatedSurgeryId;
/** 关联医嘱ID */
@TableField("related_advice_id")
private Long relatedAdviceId;
/** 疾病诊断 */
@TableField("diagnosis")
private String diagnosis;
/** 拟实施手术/操作名称 */
@TableField("procedure_name")
private String procedureName;
/** 手术/操作目的 */
@TableField("procedure_purpose")
private String procedurePurpose;
/** 手术/操作方式 */
@TableField("procedure_method")
private String procedureMethod;
/** 预期效果 */
@TableField("expected_outcome")
private String expectedOutcome;
/** 可能出现的风险和并发症 */
@TableField("risks_and_complications")
private String risksAndComplications;
/** 替代方案及其利弊 */
@TableField("alternative_plans")
private String alternativePlans;
/** 不接受治疗的后果 */
@TableField("consequences_of_refusal")
private String consequencesOfRefusal;
/** 其他需要说明的事项 */
@TableField("other_notes")
private String otherNotes;
/** 签署医生ID */
@TableField("doctor_user_id")
private Long doctorUserId;
/** 签署医生姓名 */
@TableField("doctor_name")
private String doctorName;
/** 医生签名时间 */
@TableField("doctor_sign_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date doctorSignTime;
/** 医生签名图片(base64) */
@TableField("doctor_sign_image")
private String doctorSignImage;
/** 患者签名状态(0未签 1已签 2拒绝) */
@TableField("patient_sign_status")
private Integer patientSignStatus;
/** 患者签名时间 */
@TableField("patient_sign_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date patientSignTime;
/** 患者签名图片(base64) */
@TableField("patient_sign_image")
private String patientSignImage;
/** 代理人姓名 */
@TableField("guardian_name")
private String guardianName;
/** 代理人与患者关系 */
@TableField("guardian_relation")
private String guardianRelation;
/** 见证人姓名 */
@TableField("witness_name")
private String witnessName;
/** 拒绝原因 */
@TableField("reject_reason")
private String rejectReason;
/** 状态(0草稿 1待患者签名 2已完成 3已归档 4已作废) */
@TableField("status")
private Integer status;
/** 版本号 */
@TableField("version")
private Integer version;
}

View File

@@ -0,0 +1,92 @@
package com.healthlink.his.document.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 病程记录实体类
* 依据: 《病历书写基本规范》
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_progress_note")
public class ProgressNote extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/** 就诊ID */
@TableField("encounter_id")
private Long encounterId;
/** 患者ID */
@TableField("patient_id")
private Long patientId;
/** 患者姓名 */
@TableField("patient_name")
private String patientName;
/** 记录类型(1首次 2日常 3上级查房 4疑难讨论 5阶段小结 6抢救 7转科 8接收 9出院 10死亡) */
@TableField("note_type")
private Integer noteType;
/** 记录内容(结构化) */
@TableField("note_content")
private String noteContent;
/** 书写人ID */
@TableField("author_user_id")
private Long authorUserId;
/** 书写人姓名 */
@TableField("author_name")
private String authorName;
/** 书写人职称 */
@TableField("author_title")
private String authorTitle;
/** 审核人ID */
@TableField("review_user_id")
private Long reviewUserId;
/** 审核人姓名 */
@TableField("review_user_name")
private String reviewUserName;
/** 签名状态(0未签 1已签) */
@TableField("sign_status")
private Integer signStatus;
/** 签名时间 */
@TableField("sign_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date signTime;
/** 时限要求 */
@TableField("deadline")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deadline;
/** 是否超时 */
@TableField("is_overdue")
private Boolean isOverdue;
/** 超时小时数 */
@TableField("overdue_hours")
private Integer overdueHours;
/** 使用的模板ID */
@TableField("template_id")
private Long templateId;
/** 版本号 */
@TableField("version")
private Integer version;
}

View File

@@ -0,0 +1,55 @@
package com.healthlink.his.document.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 病程记录提醒实体类
*/
@Data
@TableName("sys_progress_note_reminder")
public class ProgressNoteReminder implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/** 就诊ID */
@TableField("encounter_id")
private Long encounterId;
/** 患者姓名 */
@TableField("patient_name")
private String patientName;
/** 记录类型 */
@TableField("note_type")
private Integer noteType;
/** 截止时间 */
@TableField("deadline")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deadline;
/** 状态(0待书写 1已书写 2已超时 3已提醒) */
@TableField("status")
private Integer status;
/** 提醒对象ID */
@TableField("remind_user_id")
private Long remindUserId;
/** 提醒对象姓名 */
@TableField("remind_user_name")
private String remindUserName;
/** 创建时间 */
@TableField("created_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdTime;
}

View File

@@ -0,0 +1,12 @@
package com.healthlink.his.document.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.document.domain.InformedConsent;
import org.apache.ibatis.annotations.Mapper;
/**
* 知情同意书Mapper接口
*/
@Mapper
public interface InformedConsentMapper extends BaseMapper<InformedConsent> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.document.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.document.domain.ProgressNote;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProgressNoteMapper extends BaseMapper<ProgressNote> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.document.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.document.domain.ProgressNoteReminder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProgressNoteReminderMapper extends BaseMapper<ProgressNoteReminder> {
}

View File

@@ -0,0 +1,10 @@
package com.healthlink.his.document.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.document.domain.InformedConsent;
/**
* 知情同意书Service接口
*/
public interface IInformedConsentService extends IService<InformedConsent> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.document.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.document.domain.ProgressNoteReminder;
public interface IProgressNoteReminderService extends IService<ProgressNoteReminder> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.document.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.document.domain.ProgressNote;
public interface IProgressNoteService extends IService<ProgressNote> {
}

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.document.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.document.domain.InformedConsent;
import com.healthlink.his.document.mapper.InformedConsentMapper;
import com.healthlink.his.document.service.IInformedConsentService;
import org.springframework.stereotype.Service;
/**
* 知情同意书Service实现
*/
@Service
public class InformedConsentServiceImpl extends ServiceImpl<InformedConsentMapper, InformedConsent>
implements IInformedConsentService {
}

View File

@@ -0,0 +1,12 @@
package com.healthlink.his.document.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.document.domain.ProgressNoteReminder;
import com.healthlink.his.document.mapper.ProgressNoteReminderMapper;
import com.healthlink.his.document.service.IProgressNoteReminderService;
import org.springframework.stereotype.Service;
@Service
public class ProgressNoteReminderServiceImpl extends ServiceImpl<ProgressNoteReminderMapper, ProgressNoteReminder>
implements IProgressNoteReminderService {
}

View File

@@ -0,0 +1,12 @@
package com.healthlink.his.document.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.document.domain.ProgressNote;
import com.healthlink.his.document.mapper.ProgressNoteMapper;
import com.healthlink.his.document.service.IProgressNoteService;
import org.springframework.stereotype.Service;
@Service
public class ProgressNoteServiceImpl extends ServiceImpl<ProgressNoteMapper, ProgressNote>
implements IProgressNoteService {
}

View File

@@ -0,0 +1,37 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hir_environmental_monitor")
public class EnvironmentalMonitor extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("department_id")
private Long departmentId;
@TableField("department_name")
private String departmentName;
@TableField("monitor_type")
private String monitorType;
@TableField("monitor_item")
private String monitorItem;
@TableField("monitor_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date monitorDate;
@TableField("standard_value")
private String standardValue;
@TableField("actual_value")
private String actualValue;
@TableField("result")
private String result;
@TableField("tester_name")
private String testerName;
@TableField("remarks")
private String remarks;
}

View File

@@ -0,0 +1,34 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hir_hand_hygiene")
public class HandHygiene extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("department_id")
private Long departmentId;
@TableField("department_name")
private String departmentName;
@TableField("monitor_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date monitorDate;
@TableField("observe_count")
private Integer observeCount;
@TableField("comply_count")
private Integer complyCount;
@TableField("comply_rate")
private BigDecimal complyRate;
@TableField("observer_name")
private String observerName;
@TableField("remarks")
private String remarks;
}

View File

@@ -0,0 +1,52 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hir_multi_drug_resistant")
public class MultiDrugResistant extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("patient_id")
private Long patientId;
@TableField("patient_name")
private String patientName;
@TableField("encounter_id")
private Long encounterId;
@TableField("department_id")
private Long departmentId;
@TableField("department_name")
private String departmentName;
@TableField("bacteria_name")
private String bacteriaName;
@TableField("resistance_type")
private String resistanceType;
@TableField("specimen_type")
private String specimenType;
@TableField("specimen_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date specimenDate;
@TableField("report_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date reportDate;
@TableField("isolation_status")
private Integer isolationStatus;
@TableField("isolation_start_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date isolationStartDate;
@TableField("isolation_end_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date isolationEndDate;
@TableField("treatment_plan")
private String treatmentPlan;
@TableField("outcome")
private String outcome;
@TableField("status")
private Integer status;
}

View File

@@ -0,0 +1,41 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hir_outbreak_warning")
public class OutbreakWarning extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("department_id")
private Long departmentId;
@TableField("department_name")
private String departmentName;
@TableField("infection_type")
private String infectionType;
@TableField("case_count")
private Integer caseCount;
@TableField("warning_level")
private String warningLevel;
@TableField("time_range_days")
private Integer timeRangeDays;
@TableField("threshold_count")
private Integer thresholdCount;
@TableField("status")
private Integer status;
@TableField("handle_user_id")
private Long handleUserId;
@TableField("handle_user_name")
private String handleUserName;
@TableField("handle_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date handleTime;
@TableField("handle_result")
private String handleResult;
}

View File

@@ -0,0 +1,43 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hir_targeted_surveillance")
public class TargetedSurveillance extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("surveillance_type")
private Integer surveillanceType;
@TableField("department_id")
private Long departmentId;
@TableField("department_name")
private String departmentName;
@TableField("monitor_object")
private String monitorObject;
@TableField("monitor_item")
private String monitorItem;
@TableField("start_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date startDate;
@TableField("end_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date endDate;
@TableField("total_cases")
private Integer totalCases;
@TableField("infection_cases")
private Integer infectionCases;
@TableField("infection_rate")
private BigDecimal infectionRate;
@TableField("status")
private Integer status;
@TableField("report_content")
private String reportContent;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.EnvironmentalMonitor;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EnvironmentalMonitorMapper extends BaseMapper<EnvironmentalMonitor> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.HandHygiene;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface HandHygieneMapper extends BaseMapper<HandHygiene> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.MultiDrugResistant;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MultiDrugResistantMapper extends BaseMapper<MultiDrugResistant> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.OutbreakWarning;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OutbreakWarningMapper extends BaseMapper<OutbreakWarning> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.TargetedSurveillance;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TargetedSurveillanceMapper extends BaseMapper<TargetedSurveillance> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.EnvironmentalMonitor;
public interface IEnvironmentalMonitorService extends IService<EnvironmentalMonitor> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.HandHygiene;
public interface IHandHygieneService extends IService<HandHygiene> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.MultiDrugResistant;
public interface IMultiDrugResistantService extends IService<MultiDrugResistant> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.OutbreakWarning;
public interface IOutbreakWarningService extends IService<OutbreakWarning> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.TargetedSurveillance;
public interface ITargetedSurveillanceService extends IService<TargetedSurveillance> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.EnvironmentalMonitor;
import com.healthlink.his.infection.mapper.EnvironmentalMonitorMapper;
import com.healthlink.his.infection.service.IEnvironmentalMonitorService;
import org.springframework.stereotype.Service;
@Service
public class EnvironmentalMonitorServiceImpl extends ServiceImpl<EnvironmentalMonitorMapper, EnvironmentalMonitor> implements IEnvironmentalMonitorService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.HandHygiene;
import com.healthlink.his.infection.mapper.HandHygieneMapper;
import com.healthlink.his.infection.service.IHandHygieneService;
import org.springframework.stereotype.Service;
@Service
public class HandHygieneServiceImpl extends ServiceImpl<HandHygieneMapper, HandHygiene> implements IHandHygieneService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.MultiDrugResistant;
import com.healthlink.his.infection.mapper.MultiDrugResistantMapper;
import com.healthlink.his.infection.service.IMultiDrugResistantService;
import org.springframework.stereotype.Service;
@Service
public class MultiDrugResistantServiceImpl extends ServiceImpl<MultiDrugResistantMapper, MultiDrugResistant> implements IMultiDrugResistantService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.OutbreakWarning;
import com.healthlink.his.infection.mapper.OutbreakWarningMapper;
import com.healthlink.his.infection.service.IOutbreakWarningService;
import org.springframework.stereotype.Service;
@Service
public class OutbreakWarningServiceImpl extends ServiceImpl<OutbreakWarningMapper, OutbreakWarning> implements IOutbreakWarningService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.TargetedSurveillance;
import com.healthlink.his.infection.mapper.TargetedSurveillanceMapper;
import com.healthlink.his.infection.service.ITargetedSurveillanceService;
import org.springframework.stereotype.Service;
@Service
public class TargetedSurveillanceServiceImpl extends ServiceImpl<TargetedSurveillanceMapper, TargetedSurveillance> implements ITargetedSurveillanceService {
}

View File

@@ -0,0 +1,27 @@
import request from '@/utils/request'
// 暴发预警
export function getOutbreakPage(params) { return request({ url: '/infection-enhanced/outbreak/page', method: 'get', params }) }
export function addOutbreak(data) { return request({ url: '/infection-enhanced/outbreak/add', method: 'post', data }) }
export function handleOutbreak(data) { return request({ url: '/infection-enhanced/outbreak/handle', method: 'post', data }) }
export function excludeOutbreak(id, reason) { return request({ url: '/infection-enhanced/outbreak/exclude', method: 'post', params: { id, reason } }) }
// 目标性监测
export function getSurveillancePage(params) { return request({ url: '/infection-enhanced/surveillance/page', method: 'get', params }) }
export function addSurveillance(data) { return request({ url: '/infection-enhanced/surveillance/add', method: 'post', data }) }
// 手卫生
export function getHandHygienePage(params) { return request({ url: '/infection-enhanced/hand-hygiene/page', method: 'get', params }) }
export function addHandHygiene(data) { return request({ url: '/infection-enhanced/hand-hygiene/add', method: 'post', data }) }
export function getHandHygieneStats(params) { return request({ url: '/infection-enhanced/hand-hygiene/stats', method: 'get', params }) }
// 多重耐药菌
export function getMdrPage(params) { return request({ url: '/infection-enhanced/mdr/page', method: 'get', params }) }
export function addMdr(data) { return request({ url: '/infection-enhanced/mdr/add', method: 'post', data }) }
export function isolateMdr(data) { return request({ url: '/infection-enhanced/mdr/isolate', method: 'post', data }) }
export function releaseMdr(id) { return request({ url: '/infection-enhanced/mdr/release', method: 'post', params: { id } }) }
// 环境监测
export function getEnvMonitorPage(params) { return request({ url: '/infection-enhanced/env-monitor/page', method: 'get', params }) }
export function addEnvMonitor(data) { return request({ url: '/infection-enhanced/env-monitor/add', method: 'post', data }) }
export function getEnvMonitorStats() { return request({ url: '/infection-enhanced/env-monitor/stats', method: 'get' }) }

View File

@@ -0,0 +1,336 @@
<template>
<div class="infection-enhanced-container">
<div class="page-header">
<span class="tab-title">院感管理增强</span>
</div>
<el-tabs v-model="activeTab" type="border-card">
<!-- 暴发预警 -->
<el-tab-pane label="暴发预警" name="outbreak">
<div style="margin-bottom: 12px">
<el-button type="success" @click="showAddOutbreak = true">新增预警</el-button>
</div>
<el-table :data="outbreakData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="departmentName" label="科室" width="120" />
<el-table-column prop="infectionType" label="感染类型" width="120" />
<el-table-column prop="caseCount" label="病例数" width="80" align="center" />
<el-table-column prop="warningLevel" label="预警级别" width="100">
<template #default="{ row }">
<el-tag :type="row.warningLevel === 'RED' ? 'danger' : 'warning'" size="small">
{{ row.warningLevel === 'RED' ? '🔴 红色' : '🟡 黄色' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="['warning','info','success',''][row.status]" size="small">
{{ ['待处理','处理中','已处理','已排除'][row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="预警时间" width="170" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button v-if="row.status === 0" type="primary" link size="small" @click="handleOutbreakAction(row)">处理</el-button>
<el-button v-if="row.status === 0" type="info" link size="small" @click="excludeOutbreakAction(row)">排除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 目标性监测 -->
<el-tab-pane label="目标性监测" name="surveillance">
<div style="margin-bottom: 12px">
<el-button type="success" @click="showAddSurveillance = true">新增监测</el-button>
</div>
<el-table :data="surveillanceData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="surveillanceType" label="监测类型" width="120">
<template #default="{ row }">
{{ ['','ICU监测','手术部位','导管相关','其他'][row.surveillanceType] || '未知' }}
</template>
</el-table-column>
<el-table-column prop="departmentName" label="科室" width="120" />
<el-table-column prop="monitorObject" label="监测对象" width="150" />
<el-table-column prop="totalCases" label="总例数" width="80" align="center" />
<el-table-column prop="infectionCases" label="感染例数" width="80" align="center" />
<el-table-column prop="infectionRate" label="感染率" width="80" align="center">
<template #default="{ row }">{{ row.infectionRate }}%</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" width="120" />
</el-table>
</el-tab-pane>
<!-- 手卫生监测 -->
<el-tab-pane label="手卫生监测" name="handHygiene">
<div style="margin-bottom: 12px">
<el-button type="success" @click="showAddHandHygiene = true">新增记录</el-button>
</div>
<el-card shadow="never" style="margin-bottom: 12px">
<div style="display: flex; gap: 40px; text-align: center">
<div><div style="font-size: 28px; font-weight: bold; color: #409eff">{{ hhStats.totalObserve || 0 }}</div><div>总观察次数</div></div>
<div><div style="font-size: 28px; font-weight: bold; color: #67c23a">{{ hhStats.totalComply || 0 }}</div><div>总依从次数</div></div>
<div><div style="font-size: 28px; font-weight: bold; color: #e6a23c">{{ hhStats.overallRate || 0 }}%</div><div>总体依从率</div></div>
</div>
</el-card>
<el-table :data="handHygieneData" border stripe style="width: 100%">
<el-table-column prop="departmentName" label="科室" width="120" />
<el-table-column prop="monitorDate" label="监测日期" width="120" />
<el-table-column prop="observeCount" label="观察次数" width="100" align="center" />
<el-table-column prop="complyCount" label="依从次数" width="100" align="center" />
<el-table-column prop="complyRate" label="依从率" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.complyRate >= 90 ? 'success' : row.complyRate >= 70 ? 'warning' : 'danger'" size="small">
{{ row.complyRate }}%
</el-tag>
</template>
</el-table-column>
<el-table-column prop="observerName" label="观察者" width="100" />
</el-table>
</el-tab-pane>
<!-- 多重耐药菌 -->
<el-tab-pane label="多重耐药菌" name="mdr">
<div style="margin-bottom: 12px">
<el-button type="success" @click="showAddMdr = true">新增记录</el-button>
</div>
<el-table :data="mdrData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="patientName" label="患者" width="90" />
<el-table-column prop="departmentName" label="科室" width="100" />
<el-table-column prop="bacteriaName" label="菌种" width="150" />
<el-table-column prop="resistanceType" label="耐药类型" width="150" show-overflow-tooltip />
<el-table-column prop="specimenType" label="标本类型" width="100" />
<el-table-column prop="isolationStatus" label="隔离状态" width="100">
<template #default="{ row }">
<el-tag :type="['info','danger','success'][row.isolationStatus]" size="small">
{{ ['未隔离','已隔离','已解除'][row.isolationStatus] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="reportDate" label="报告日期" width="120" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button v-if="row.isolationStatus === 0" type="danger" link size="small" @click="isolateAction(row)">隔离</el-button>
<el-button v-if="row.isolationStatus === 1" type="success" link size="small" @click="releaseAction(row)">解除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 环境卫生学监测 -->
<el-tab-pane label="环境卫生学监测" name="envMonitor">
<div style="margin-bottom: 12px">
<el-button type="success" @click="showAddEnv = true">新增记录</el-button>
</div>
<el-table :data="envData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="departmentName" label="科室" width="120" />
<el-table-column prop="monitorType" label="监测类型" width="100">
<template #default="{ row }">
{{ {1:'空气',2:'物表',3:'手',4:'消毒液',5:'无菌物品'}[row.monitorType] || row.monitorType }}
</template>
</el-table-column>
<el-table-column prop="monitorItem" label="监测项目" width="150" />
<el-table-column prop="monitorDate" label="监测日期" width="120" />
<el-table-column prop="standardValue" label="标准值" width="100" />
<el-table-column prop="actualValue" label="实测值" width="100" />
<el-table-column prop="result" label="结果" width="80">
<template #default="{ row }">
<el-tag :type="row.result === '合格' ? 'success' : 'danger'" size="small">{{ row.result }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="testerName" label="检测人" width="100" />
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 新增暴发预警弹窗 -->
<el-dialog v-model="showAddOutbreak" title="新增暴发预警" width="500px">
<el-form :model="outbreakForm" label-width="100px">
<el-form-item label="科室"><el-input v-model="outbreakForm.departmentName" /></el-form-item>
<el-form-item label="感染类型"><el-input v-model="outbreakForm.infectionType" /></el-form-item>
<el-form-item label="病例数"><el-input-number v-model="outbreakForm.caseCount" :min="1" /></el-form-item>
<el-form-item label="预警级别">
<el-select v-model="outbreakForm.warningLevel">
<el-option label="黄色" value="YELLOW" /><el-option label="红色" value="RED" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddOutbreak = false">取消</el-button>
<el-button type="primary" @click="submitOutbreak">确认</el-button>
</template>
</el-dialog>
<!-- 新增手卫生弹窗 -->
<el-dialog v-model="showAddHandHygiene" title="新增手卫生监测" width="500px">
<el-form :model="hhForm" label-width="100px">
<el-form-item label="科室"><el-input v-model="hhForm.departmentName" /></el-form-item>
<el-form-item label="监测日期"><el-date-picker v-model="hhForm.monitorDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="观察次数"><el-input-number v-model="hhForm.observeCount" :min="0" /></el-form-item>
<el-form-item label="依从次数"><el-input-number v-model="hhForm.complyCount" :min="0" /></el-form-item>
<el-form-item label="观察者"><el-input v-model="hhForm.observerName" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddHandHygiene = false">取消</el-button>
<el-button type="primary" @click="submitHandHygiene">确认</el-button>
</template>
</el-dialog>
<!-- 新增多重耐药菌弹窗 -->
<el-dialog v-model="showAddMdr" title="新增多重耐药菌记录" width="550px">
<el-form :model="mdrForm" label-width="100px">
<el-form-item label="患者姓名"><el-input v-model="mdrForm.patientName" /></el-form-item>
<el-form-item label="科室"><el-input v-model="mdrForm.departmentName" /></el-form-item>
<el-form-item label="菌种名称"><el-input v-model="mdrForm.bacteriaName" /></el-form-item>
<el-form-item label="耐药类型"><el-input v-model="mdrForm.resistanceType" /></el-form-item>
<el-form-item label="标本类型"><el-input v-model="mdrForm.specimenType" /></el-form-item>
<el-form-item label="报告日期"><el-date-picker v-model="mdrForm.reportDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddMdr = false">取消</el-button>
<el-button type="primary" @click="submitMdr">确认</el-button>
</template>
</el-dialog>
<!-- 新增环境监测弹窗 -->
<el-dialog v-model="showAddEnv" title="新增环境卫生学监测" width="550px">
<el-form :model="envForm" label-width="100px">
<el-form-item label="科室"><el-input v-model="envForm.departmentName" /></el-form-item>
<el-form-item label="监测类型">
<el-select v-model="envForm.monitorType">
<el-option label="空气" value="1" /><el-option label="物表" value="2" />
<el-option label="手" value="3" /><el-option label="消毒液" value="4" />
<el-option label="无菌物品" value="5" />
</el-select>
</el-form-item>
<el-form-item label="监测项目"><el-input v-model="envForm.monitorItem" /></el-form-item>
<el-form-item label="监测日期"><el-date-picker v-model="envForm.monitorDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="标准值"><el-input v-model="envForm.standardValue" /></el-form-item>
<el-form-item label="实测值"><el-input v-model="envForm.actualValue" /></el-form-item>
<el-form-item label="结果">
<el-radio-group v-model="envForm.result">
<el-radio value="合格">合格</el-radio><el-radio value="不合格">不合格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="检测人"><el-input v-model="envForm.testerName" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddEnv = false">取消</el-button>
<el-button type="primary" @click="submitEnv">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOutbreakPage, addOutbreak, handleOutbreak, excludeOutbreak, getSurveillancePage, addSurveillance, getHandHygienePage, addHandHygiene, getHandHygieneStats, getMdrPage, addMdr, isolateMdr, releaseMdr, getEnvMonitorPage, addEnvMonitor } from './api'
const activeTab = ref('outbreak')
const loading = ref(false)
// Data
const outbreakData = ref([])
const surveillanceData = ref([])
const handHygieneData = ref([])
const hhStats = ref({})
const mdrData = ref([])
const envData = ref([])
// Dialogs
const showAddOutbreak = ref(false)
const showAddSurveillance = ref(false)
const showAddHandHygiene = ref(false)
const showAddMdr = ref(false)
const showAddEnv = ref(false)
// Forms
const outbreakForm = reactive({ departmentName: '', infectionType: '', caseCount: 1, warningLevel: 'YELLOW' })
const hhForm = reactive({ departmentName: '', monitorDate: '', observeCount: 0, complyCount: 0, observerName: '' })
const mdrForm = reactive({ patientName: '', departmentName: '', bacteriaName: '', resistanceType: '', specimenType: '', reportDate: '' })
const envForm = reactive({ departmentName: '', monitorType: '1', monitorItem: '', monitorDate: '', standardValue: '', actualValue: '', result: '合格', testerName: '' })
const loadData = async () => {
loading.value = true
try {
const [o, s, h, hh, m, e] = await Promise.all([
getOutbreakPage({ pageNo: 1, pageSize: 50 }),
getSurveillancePage({ pageNo: 1, pageSize: 50 }),
getHandHygienePage({ pageNo: 1, pageSize: 50 }),
getHandHygieneStats({}),
getMdrPage({ pageNo: 1, pageSize: 50 }),
getEnvMonitorPage({ pageNo: 1, pageSize: 50 })
])
outbreakData.value = o.data?.records || []
surveillanceData.value = s.data?.records || []
handHygieneData.value = h.data?.records || []
hhStats.value = hh.data || {}
mdrData.value = m.data?.records || []
envData.value = e.data?.records || []
} finally { loading.value = false }
}
const handleOutbreakAction = async (row) => {
const { value } = await ElMessageBox.prompt('请输入处理结果', '处理预警', { inputType: 'textarea' })
await handleOutbreak({ id: row.id, handleResult: value })
ElMessage.success('处理成功')
loadData()
}
const excludeOutbreakAction = async (row) => {
await ElMessageBox.confirm('确认排除此预警?', '确认')
await excludeOutbreak(row.id, '误报')
ElMessage.success('已排除')
loadData()
}
const isolateAction = async (row) => {
await ElMessageBox.confirm('确认标记为隔离状态?', '确认隔离')
await isolateMdr({ id: row.id })
ElMessage.success('已隔离')
loadData()
}
const releaseAction = async (row) => {
await ElMessageBox.confirm('确认解除隔离?', '确认解除')
await releaseMdr(row.id)
ElMessage.success('已解除')
loadData()
}
const submitOutbreak = async () => {
await addOutbreak(outbreakForm)
ElMessage.success('新增成功')
showAddOutbreak.value = false
loadData()
}
const submitHandHygiene = async () => {
await addHandHygiene(hhForm)
ElMessage.success('新增成功')
showAddHandHygiene.value = false
loadData()
}
const submitMdr = async () => {
await addMdr(mdrForm)
ElMessage.success('新增成功')
showAddMdr.value = false
loadData()
}
const submitEnv = async () => {
await addEnvMonitor(envForm)
ElMessage.success('新增成功')
showAddEnv.value = false
loadData()
}
onMounted(() => loadData())
</script>
<style scoped>
.infection-enhanced-container { padding: 16px; }
.page-header { margin-bottom: 16px; }
.tab-title { font-size: 18px; font-weight: bold; }
</style>

View File

@@ -0,0 +1,45 @@
import request from '@/utils/request'
export function getConsentPage(params) {
return request({ url: '/informed-consent/page', method: 'get', params })
}
export function getConsentDetail(id) {
return request({ url: '/informed-consent/detail', method: 'get', params: { id } })
}
export function addConsent(data) {
return request({ url: '/informed-consent/add', method: 'post', data })
}
export function updateConsent(data) {
return request({ url: '/informed-consent/update', method: 'put', data })
}
export function deleteConsent(id) {
return request({ url: '/informed-consent/delete', method: 'delete', params: { id } })
}
export function doctorSign(data) {
return request({ url: '/informed-consent/doctor-sign', method: 'post', data })
}
export function patientSign(data) {
return request({ url: '/informed-consent/patient-sign', method: 'post', data })
}
export function patientReject(data) {
return request({ url: '/informed-consent/patient-reject', method: 'post', data })
}
export function archiveConsent(id) {
return request({ url: '/informed-consent/archive', method: 'post', params: { id } })
}
export function voidConsent(id, reason) {
return request({ url: '/informed-consent/void', method: 'post', params: { id, reason } })
}
export function getConsentStats(encounterId) {
return request({ url: '/informed-consent/stats', method: 'get', params: { encounterId } })
}

View File

@@ -0,0 +1,374 @@
<template>
<div class="informed-consent-container">
<div class="page-header">
<span class="tab-title">知情同意管理</span>
</div>
<!-- 查询条件 -->
<div class="search-section">
<el-form :model="queryParams" inline>
<el-form-item label="患者姓名">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable style="width: 150px" />
</el-form-item>
<el-form-item label="同意类型">
<el-select v-model="queryParams.consentType" placeholder="请选择" clearable style="width: 140px">
<el-option v-for="item in consentTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="success" @click="handleAdd">新增</el-button>
</el-form-item>
</el-form>
</div>
<!-- 列表 -->
<el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="patientName" label="患者姓名" width="100" />
<el-table-column prop="consentType" label="同意类型" width="120">
<template #default="{ row }">
{{ getConsentTypeName(row.consentType) }}
</template>
</el-table-column>
<el-table-column prop="procedureName" label="手术/操作名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="doctorName" label="签署医生" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ getStatusName(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="patientSignStatus" label="患者签名" width="100">
<template #default="{ row }">
<el-tag :type="getSignStatusType(row.patientSignStatus)" size="small">
{{ getSignStatusName(row.patientSignStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="60" align="center" />
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === 0" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.status === 0" type="primary" link size="small" @click="handleDoctorSign(row)">医生签名</el-button>
<el-button v-if="row.status === 1" type="success" link size="small" @click="handlePatientSign(row)">患者签名</el-button>
<el-button v-if="row.status === 1" type="warning" link size="small" @click="handlePatientReject(row)">患者拒绝</el-button>
<el-button v-if="row.status === 2" type="info" link size="small" @click="handleArchive(row)">归档</el-button>
<el-button v-if="row.status < 3" type="danger" link size="small" @click="handleVoid(row)">作废</el-button>
<el-button type="info" link size="small" @click="handleDetail(row)">查看</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"
@size-change="handleSearch"
@current-change="handleSearch"
style="margin-top: 16px; text-align: right"
/>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" destroy-on-close>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="formData.patientName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="就诊ID" prop="encounterId">
<el-input-number v-model="formData.encounterId" :min="1" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="同意类型" prop="consentType">
<el-select v-model="formData.consentType" placeholder="请选择" style="width: 100%">
<el-option v-for="item in consentTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术/操作名称" prop="procedureName">
<el-input v-model="formData.procedureName" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="疾病诊断" prop="diagnosis">
<el-input v-model="formData.diagnosis" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="手术/操作目的" prop="procedurePurpose">
<el-input v-model="formData.procedurePurpose" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="手术/操作方式" prop="procedureMethod">
<el-input v-model="formData.procedureMethod" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="预期效果" prop="expectedOutcome">
<el-input v-model="formData.expectedOutcome" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="风险和并发症" prop="risksAndComplications">
<el-input v-model="formData.risksAndComplications" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="替代方案" prop="alternativePlans">
<el-input v-model="formData.alternativePlans" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="拒绝后果" prop="consequencesOfRefusal">
<el-input v-model="formData.consequencesOfRefusal" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="其他事项">
<el-input v-model="formData.otherNotes" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
<!-- 签名弹窗 -->
<el-dialog v-model="signDialogVisible" :title="signDialogTitle" width="500px">
<div style="text-align: center; padding: 20px">
<p style="margin-bottom: 16px; color: #666">请在下方区域签名</p>
<canvas ref="signCanvas" width="400" height="200" style="border: 1px solid #ccc; cursor: crosshair" @mousedown="startSign" @mousemove="doSign" @mouseup="endSign" />
<div style="margin-top: 12px">
<el-button @click="clearSign">清除</el-button>
</div>
</div>
<template #footer>
<el-button @click="signDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitSign">确认签名</el-button>
</template>
</el-dialog>
<!-- 拒绝弹窗 -->
<el-dialog v-model="rejectDialogVisible" title="患者拒绝签署" width="500px">
<el-form :model="rejectForm" label-width="100px">
<el-form-item label="拒绝原因">
<el-input v-model="rejectForm.rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
</el-form-item>
<el-form-item label="见证人">
<el-input v-model="rejectForm.witnessName" placeholder="请输入见证人姓名" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="warning" @click="submitReject">确认拒绝</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getConsentPage, getConsentDetail, addConsent, updateConsent, deleteConsent, doctorSign, patientSign, patientReject, archiveConsent, voidConsent } from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref(null)
const signDialogVisible = ref(false)
const signDialogTitle = ref('')
const rejectDialogVisible = ref(false)
const signCanvas = ref(null)
let signCtx = null
let isDrawing = false
let currentSignType = ''
let currentSignRow = null
const consentTypeOptions = [
{ label: '手术知情同意', value: 1 },
{ label: '麻醉知情同意', value: 2 },
{ label: '输血知情同意', value: 3 },
{ label: '特殊检查知情同意', value: 4 },
{ label: '特殊治疗知情同意', value: 5 },
{ label: '病危通知书', value: 6 },
{ label: '自费项目知情同意', value: 7 }
]
const statusOptions = [
{ label: '草稿', value: 0 },
{ label: '待患者签名', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已归档', value: 3 },
{ label: '已作废', value: 4 }
]
const queryParams = reactive({ patientName: '', consentType: null, status: null, encounterId: null, pageNo: 1, pageSize: 20 })
const formData = reactive({
id: null, patientName: '', encounterId: null, patientId: null, consentType: null,
procedureName: '', diagnosis: '', procedurePurpose: '', procedureMethod: '',
expectedOutcome: '', risksAndComplications: '', alternativePlans: '',
consequencesOfRefusal: '', otherNotes: '', doctorUserId: null, doctorName: ''
})
const formRules = {
patientName: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }],
encounterId: [{ required: true, message: '请输入就诊ID', trigger: 'blur' }],
consentType: [{ required: true, message: '请选择同意类型', trigger: 'change' }],
procedureName: [{ required: true, message: '请输入手术/操作名称', trigger: 'blur' }]
}
const rejectForm = reactive({ rejectReason: '', witnessName: '' })
const getConsentTypeName = (type) => consentTypeOptions.find(o => o.value === type)?.label || '未知'
const getStatusName = (status) => statusOptions.find(o => o.value === status)?.label || '未知'
const getStatusType = (status) => ['info', 'warning', 'success', '', 'danger'][status] || 'info'
const getSignStatusName = (s) => ['未签名', '已签名', '拒绝'][s] || '未知'
const getSignStatusType = (s) => ['info', 'success', 'danger'][s] || 'info'
const handleSearch = async () => {
loading.value = true
try {
const res = await getConsentPage(queryParams)
if (res.data) {
tableData.value = res.data.records || []
total.value = res.data.total || 0
}
} finally { loading.value = false }
}
const resetQuery = () => {
Object.assign(queryParams, { patientName: '', consentType: null, status: null, encounterId: null, pageNo: 1 })
handleSearch()
}
const handleAdd = () => {
dialogTitle.value = '新增知情同意书'
Object.assign(formData, { id: null, patientName: '', encounterId: null, consentType: null, procedureName: '', diagnosis: '', procedurePurpose: '', procedureMethod: '', expectedOutcome: '', risksAndComplications: '', alternativePlans: '', consequencesOfRefusal: '', otherNotes: '' })
dialogVisible.value = true
}
const handleEdit = async (row) => {
dialogTitle.value = '编辑知情同意书'
const res = await getConsentDetail(row.id)
if (res.data) Object.assign(formData, res.data)
dialogVisible.value = true
}
const handleSubmit = async () => {
await formRef.value.validate()
if (formData.id) {
await updateConsent(formData)
ElMessage.success('修改成功')
} else {
await addConsent(formData)
ElMessage.success('新增成功')
}
dialogVisible.value = false
handleSearch()
}
const handleDoctorSign = (row) => {
currentSignType = 'doctor'
currentSignRow = row
signDialogTitle.value = '医生签名'
signDialogVisible.value = true
nextTick(() => initCanvas())
}
const handlePatientSign = (row) => {
currentSignType = 'patient'
currentSignRow = row
signDialogTitle.value = '患者签名'
signDialogVisible.value = true
nextTick(() => initCanvas())
}
const initCanvas = () => {
const canvas = signCanvas.value
if (!canvas) return
signCtx = canvas.getContext('2d')
signCtx.clearRect(0, 0, 400, 200)
signCtx.strokeStyle = '#000'
signCtx.lineWidth = 2
}
const startSign = (e) => { isDrawing = true; signCtx.beginPath(); signCtx.moveTo(e.offsetX, e.offsetY) }
const doSign = (e) => { if (isDrawing) { signCtx.lineTo(e.offsetX, e.offsetY); signCtx.stroke() } }
const endSign = () => { isDrawing = false }
const clearSign = () => { if (signCtx) signCtx.clearRect(0, 0, 400, 200) }
const submitSign = async () => {
const canvas = signCanvas.value
const signImage = canvas.toDataURL('image/png')
if (currentSignType === 'doctor') {
await doctorSign({ id: currentSignRow.id, signImage })
ElMessage.success('医生签名成功')
} else {
await patientSign({ id: currentSignRow.id, signImage })
ElMessage.success('患者签名成功')
}
signDialogVisible.value = false
handleSearch()
}
const handlePatientReject = (row) => {
currentSignRow = row
rejectForm.rejectReason = ''
rejectForm.witnessName = ''
rejectDialogVisible.value = true
}
const submitReject = async () => {
await patientReject({ id: currentSignRow.id, ...rejectForm })
ElMessage.warning('已记录患者拒绝')
rejectDialogVisible.value = false
handleSearch()
}
const handleArchive = async (row) => {
await ElMessageBox.confirm('确认归档到病历?', '提示')
await archiveConsent(row.id)
ElMessage.success('归档成功')
handleSearch()
}
const handleVoid = async (row) => {
const { value } = await ElMessageBox.prompt('请输入作废原因', '作废确认', { inputType: 'textarea' })
await voidConsent(row.id, value)
ElMessage.success('已作废')
handleSearch()
}
const handleDetail = async (row) => {
const res = await getConsentDetail(row.id)
if (res.data) {
await ElMessageBox.alert(
`<div style="line-height:2">
<p><b>类型:</b> ${getConsentTypeName(res.data.consentType)}</p>
<p><b>患者:</b> ${res.data.patientName}</p>
<p><b>诊断:</b> ${res.data.diagnosis || '-'}</p>
<p><b>手术:</b> ${res.data.procedureName || '-'}</p>
<p><b>风险:</b> ${res.data.risksAndComplications || '-'}</p>
<p><b>状态:</b> ${getStatusName(res.data.status)}</p>
</div>`,
'知情同意书详情', { dangerouslyUseHTMLString: true, customStyle: { maxWidth: '600px' } }
)
}
}
onMounted(() => handleSearch())
</script>
<style scoped>
.informed-consent-container { padding: 16px; }
.page-header { margin-bottom: 16px; }
.tab-title { font-size: 18px; font-weight: bold; }
.search-section { margin-bottom: 16px; }
</style>

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getDiscussionPage(params) { return request({ url: '/preop-discussion/page', method: 'get', params }) }
export function getDiscussionDetail(id) { return request({ url: '/preop-discussion/detail', method: 'get', params: { id } }) }
export function addDiscussion(data) { return request({ url: '/preop-discussion/add', method: 'post', data }) }
export function submitDiscussion(id) { return request({ url: '/preop-discussion/submit', method: 'put', params: { id } }) }
export function signDiscussion(discussionId, userId, signImage) { return request({ url: '/preop-discussion/sign', method: 'put', params: { discussionId, userId, signImage } }) }
export function reviewDiscussion(id, action, remark) { return request({ url: '/preop-discussion/review', method: 'put', params: { id, action, remark } }) }
export function checkRequired(surgeryId, surgeryLevel) { return request({ url: '/preop-discussion/check-required', method: 'get', params: { surgeryId, surgeryLevel } }) }
export function getStatistics() { return request({ url: '/preop-discussion/statistics', method: 'get' }) }

View File

@@ -0,0 +1,235 @@
<template>
<div class="app-container">
<el-row :gutter="16" class="stat-row" v-if="stats">
<el-col :span="4"><div class="stat-card"><div class="stat-value">{{ stats.total || 0 }}</div><div class="stat-label">总讨论数</div></div></el-col>
<el-col :span="4"><div class="stat-card"><div class="stat-value draft">{{ stats.draft || 0 }}</div><div class="stat-label">草稿</div></div></el-col>
<el-col :span="4"><div class="stat-card"><div class="stat-value warning">{{ stats.pending || 0 }}</div><div class="stat-label">待签名</div></div></el-col>
<el-col :span="4"><div class="stat-card"><div class="stat-value info">{{ stats.reviewing || 0 }}</div><div class="stat-label">待审核</div></div></el-col>
<el-col :span="4"><div class="stat-card"><div class="stat-value success">{{ stats.completed || 0 }}</div><div class="stat-label">已完成</div></div></el-col>
<el-col :span="4"><div class="stat-card"><div class="stat-value danger">{{ stats.rejected || 0 }}</div><div class="stat-label">已驳回</div></div></el-col>
</el-row>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">术前讨论管理</span>
<el-button type="primary" icon="Plus" @click="handleAdd">新建讨论</el-button>
</div>
</template>
<el-form :inline="true" :model="queryParams" label-width="80px">
<el-form-item label="患者">
<el-input v-model="queryParams.patientName" placeholder="患者姓名" clearable @keyup.enter="handleQuery" style="width: 140px" />
</el-form-item>
<el-form-item label="手术级别">
<el-select v-model="queryParams.surgeryLevel" placeholder="全部" clearable style="width: 110px">
<el-option label="一级" :value="1" /><el-option label="二级" :value="2" />
<el-option label="三级" :value="3" /><el-option label="四级" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 110px">
<el-option label="草稿" :value="0" /><el-option label="待签名" :value="1" />
<el-option label="待审核" :value="2" /><el-option label="已完成" :value="3" />
<el-option label="已驳回" :value="5" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<vxe-table :data="tableData" border height="calc(100vh - 380px)" v-loading="loading">
<vxe-column type="seq" title="序号" width="60" />
<vxe-column field="patientName" title="患者" width="90" />
<vxe-column field="surgeryName" title="手术名称" min-width="160" show-overflow />
<vxe-column field="surgeryLevel" title="级别" width="60" align="center">
<template #default="{ row }">
<el-tag :type="row.surgeryLevel >= 3 ? 'danger' : 'info'" size="small">{{ row.surgeryLevel }}</el-tag>
</template>
</vxe-column>
<vxe-column field="hostUserName" title="主持人" width="90" />
<vxe-column field="discussionConclusion" title="结论" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.discussionConclusion === 1" type="success" size="small">同意手术</el-tag>
<el-tag v-else-if="row.discussionConclusion === 2" type="warning" size="small">需补充检查</el-tag>
<el-tag v-else-if="row.discussionConclusion === 3" type="danger" size="small">暂不手术</el-tag>
<span v-else>-</span>
</template>
</vxe-column>
<vxe-column field="status" title="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</vxe-column>
<vxe-column field="createTime" title="创建时间" width="150" />
<vxe-column title="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button type="primary" link icon="View" @click="handleDetail(row)">详情</el-button>
<el-button v-if="row.status === 0" type="warning" link @click="handleSubmit(row)">提交</el-button>
<el-button v-if="row.status === 2" type="success" link @click="handleReview(row, 1)">通过</el-button>
<el-button v-if="row.status === 2" type="danger" link @click="handleReview(row, 5)">驳回</el-button>
</template>
</vxe-column>
</vxe-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
<!-- 新建讨论弹窗 -->
<el-dialog v-model="formVisible" title="新建术前讨论" width="800px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patientName"><el-input v-model="form.patientName" placeholder="患者姓名" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="住院号"><el-input v-model="form.encounterId" placeholder="住院号" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="手术名称" prop="surgeryName"><el-input v-model="form.surgeryName" placeholder="手术名称" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术级别" prop="surgeryLevel">
<el-select v-model="form.surgeryLevel" style="width:100%">
<el-option label="一级" :value="1" /><el-option label="二级" :value="2" />
<el-option label="三级" :value="3" /><el-option label="四级" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="术前诊断" prop="preopDiagnosis"><el-input v-model="form.preopDiagnosis" type="textarea" :rows="2" placeholder="术前诊断" /></el-form-item>
<el-form-item label="手术指征" prop="surgeryIndication"><el-input v-model="form.surgeryIndication" type="textarea" :rows="2" placeholder="手术指征" /></el-form-item>
<el-form-item label="主手术方案" prop="mainPlan"><el-input v-model="form.mainPlan" type="textarea" :rows="2" placeholder="主手术方案" /></el-form-item>
<el-form-item label="备选方案" prop="backupPlan"><el-input v-model="form.backupPlan" type="textarea" :rows="2" placeholder="备选手术方案" /></el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="麻醉方式"><el-input v-model="form.anesthesiaType" placeholder="麻醉方式" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="讨论类型">
<el-select v-model="form.discussionType" style="width:100%">
<el-option label="科内讨论" :value="1" /><el-option label="全科讨论" :value="2" /><el-option label="全院讨论" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="风险及对策"><el-input v-model="form.risksAndCountermeasures" type="textarea" :rows="2" placeholder="术中可能风险及对策" /></el-form-item>
<el-form-item label="术后注意"><el-input v-model="form.postopNotes" type="textarea" :rows="2" placeholder="术后注意事项" /></el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="主持人" prop="hostUserName"><el-input v-model="form.hostUserName" placeholder="主持人姓名" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="讨论地点"><el-input v-model="form.discussionLocation" placeholder="讨论地点" /></el-form-item>
</el-col>
</el-row>
<el-form-item label="讨论结论">
<el-radio-group v-model="form.discussionConclusion">
<el-radio :value="1">同意手术</el-radio>
<el-radio :value="2">需进一步检查</el-radio>
<el-radio :value="3">暂不手术</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</template>
</el-dialog>
<!-- 详情抽屉 -->
<el-drawer v-model="detailVisible" title="术前讨论详情" size="650px">
<div v-if="detailData">
<el-descriptions :column="2" border>
<el-descriptions-item label="患者">{{ detailData.discussion?.patientName }}</el-descriptions-item>
<el-descriptions-item label="手术名称" :span="2">{{ detailData.discussion?.surgeryName }}</el-descriptions-item>
<el-descriptions-item label="手术级别">{{ detailData.discussion?.surgeryLevel }}</el-descriptions-item>
<el-descriptions-item label="主持人">{{ detailData.discussion?.hostUserName }}</el-descriptions-item>
<el-descriptions-item label="讨论结论">
<el-tag v-if="detailData.discussion?.discussionConclusion === 1" type="success">同意手术</el-tag>
<el-tag v-else-if="detailData.discussion?.discussionConclusion === 2" type="warning">需补充检查</el-tag>
<el-tag v-else-if="detailData.discussion?.discussionConclusion === 3" type="danger">暂不手术</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusType(detailData.discussion?.status)">{{ statusText(detailData.discussion?.status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="section-title">讨论内容</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="术前诊断">{{ detailData.discussion?.preopDiagnosis }}</el-descriptions-item>
<el-descriptions-item label="手术指征">{{ detailData.discussion?.surgeryIndication }}</el-descriptions-item>
<el-descriptions-item label="主方案">{{ detailData.discussion?.mainPlan }}</el-descriptions-item>
<el-descriptions-item label="备选方案">{{ detailData.discussion?.backupPlan }}</el-descriptions-item>
<el-descriptions-item label="风险及对策">{{ detailData.discussion?.risksAndCountermeasures }}</el-descriptions-item>
</el-descriptions>
<div class="section-title">参与者</div>
<vxe-table :data="detailData.participants || []" border size="small">
<vxe-column field="userName" title="姓名" width="100" />
<vxe-column field="title" title="职称" width="100" />
<vxe-column field="role" title="角色" width="80" />
<vxe-column field="signStatus" title="签名" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.signStatus === 1 ? 'success' : 'info'" size="small">{{ row.signStatus === 1 ? '已签' : '未签' }}</el-tag>
</template>
</vxe-column>
<vxe-column field="signTime" title="签名时间" width="150" />
</vxe-table>
</div>
</el-drawer>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDiscussionPage, getDiscussionDetail, addDiscussion, submitDiscussion, reviewDiscussion, getStatistics } from './components/api'
const loading = ref(false); const tableData = ref([]); const total = ref(0); const stats = ref(null)
const queryParams = ref({ patientName: '', surgeryLevel: undefined, status: undefined, pageNo: 1, pageSize: 20 })
const formVisible = ref(false); const formRef = ref()
const detailVisible = ref(false); const detailData = ref(null)
const form = ref({ patientName: '', encounterId: '', surgeryName: '', surgeryLevel: 3, preopDiagnosis: '', surgeryIndication: '', mainPlan: '', backupPlan: '', anesthesiaType: '', discussionType: 1, risksAndCountermeasures: '', postopNotes: '', hostUserName: '', discussionLocation: '', discussionConclusion: 1 })
const rules = { patientName: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }], surgeryName: [{ required: true, message: '请输入手术名称', trigger: 'blur' }], surgeryLevel: [{ required: true, message: '请选择手术级别', trigger: 'change' }], preopDiagnosis: [{ required: true, message: '请输入术前诊断', trigger: 'blur' }], mainPlan: [{ required: true, message: '请输入主手术方案', trigger: 'blur' }], hostUserName: [{ required: true, message: '请输入主持人', trigger: 'blur' }] }
const statusText = (s) => ({ 0: '草稿', 1: '待签名', 2: '待审核', 3: '已完成', 4: '已归档', 5: '已驳回' }[s] || '未知')
const statusType = (s) => ({ 0: 'info', 1: 'warning', 2: '', 3: 'success', 4: 'success', 5: 'danger' }[s] || 'info')
function getList() { loading.value = true; getDiscussionPage(queryParams.value).then(res => { tableData.value = res.data?.records || []; total.value = res.data?.total || 0 }).finally(() => { loading.value = false }) }
function handleQuery() { queryParams.value.pageNo = 1; getList() }
function resetQuery() { queryParams.value = { patientName: '', surgeryLevel: undefined, status: undefined, pageNo: 1, pageSize: 20 }; getList() }
function handleAdd() { form.value = { patientName: '', encounterId: '', surgeryName: '', surgeryLevel: 3, preopDiagnosis: '', surgeryIndication: '', mainPlan: '', backupPlan: '', anesthesiaType: '', discussionType: 1, risksAndCountermeasures: '', postopNotes: '', hostUserName: '', discussionLocation: '', discussionConclusion: 1 }; formVisible.value = true }
function handleDetail(row) { getDiscussionDetail(row.id).then(res => { detailData.value = res.data; detailVisible.value = true }) }
function handleSubmit(row) { submitDiscussion(row.id).then(res => { if (res.code === 200) { ElMessage.success('已提交'); getList(); loadStats() } else ElMessage.error(res.msg || '提交失败') }) }
function handleReview(row, action) {
const label = action === 1 ? '通过' : '驳回'
ElMessageBox.confirm('确认' + label + '该讨论?', '提示', { type: action === 1 ? 'success' : 'warning' }).then(() => {
reviewDiscussion(row.id, action).then(res => { if (res.code === 200) { ElMessage.success(label + '成功'); getList(); loadStats() } else ElMessage.error(res.msg || label + '失败') })
}).catch(() => {})
}
function submitForm() {
formRef.value.validate(valid => { if (!valid) return
form.value.patientId = 0; form.value.discussionTime = Date.now()
addDiscussion(form.value).then(res => { if (res.code === 200) { ElMessage.success('创建成功'); formVisible.value = false; getList(); loadStats() } else ElMessage.error(res.msg || '创建失败') })
})
}
function loadStats() { getStatistics().then(res => { stats.value = res.data }) }
onMounted(() => { getList(); loadStats() })
</script>
<style lang="scss" scoped>
.card-header { display: flex; justify-content: space-between; align-items: center; }
.card-title { font-weight: bold; font-size: 16px; }
.stat-row { margin-bottom: 16px; }
.stat-card { background: #fff; border-radius: 6px; padding: 12px; border: 1px solid #ebeef5; text-align: center; }
.stat-value { font-size: 20px; font-weight: bold; color: #303133; }
.stat-value.draft { color: #909399; }
.stat-value.warning { color: #e6a23c; }
.stat-value.info { color: #409eff; }
.stat-value.success { color: #67c23a; }
.stat-value.danger { color: #f56c6c; }
.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
.section-title { font-size: 14px; font-weight: bold; margin: 16px 0 8px; padding-left: 8px; border-left: 3px solid #409eff; }
</style>

View File

@@ -0,0 +1,32 @@
import request from '@/utils/request'
export function getNotePage(params) {
return request({ url: '/progress-note/page', method: 'get', params })
}
export function getNoteDetail(id) {
return request({ url: '/progress-note/detail', method: 'get', params: { id } })
}
export function addNote(data) {
return request({ url: '/progress-note/add', method: 'post', data })
}
export function updateNote(data) {
return request({ url: '/progress-note/update', method: 'put', data })
}
export function deleteNote(id) {
return request({ url: '/progress-note/delete', method: 'delete', params: { id } })
}
export function signNote(data) {
return request({ url: '/progress-note/sign', method: 'post', data })
}
export function reviewNote(data) {
return request({ url: '/progress-note/review', method: 'post', data })
}
export function getMonitor(params) {
return request({ url: '/progress-note/monitor', method: 'get', params })
}
export function getReminders(params) {
return request({ url: '/progress-note/reminders', method: 'get', params })
}
export function getNoteStats(encounterId) {
return request({ url: '/progress-note/stats', method: 'get', params: { encounterId } })
}

View File

@@ -0,0 +1,287 @@
<template>
<div class="progress-notes-container">
<div class="page-header">
<span class="tab-title">病程记录管理</span>
<div style="float: right">
<el-button type="success" @click="handleAdd">新建记录</el-button>
<el-button type="warning" @click="showMonitor = !showMonitor">时限监控</el-button>
</div>
</div>
<!-- 时限监控面板 -->
<div v-if="showMonitor" class="monitor-panel">
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never" class="monitor-card overdue">
<div class="monitor-value">{{ monitorData.overdueCount || 0 }}</div>
<div class="monitor-label">🔴 超时未完成</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="monitor-card warning">
<div class="monitor-value">{{ monitorData.warningCount || 0 }}</div>
<div class="monitor-label"> 即将超时(2h内)</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="monitor-card normal">
<div class="monitor-value">{{ monitorData.normalCount || 0 }}</div>
<div class="monitor-label"> 正常</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 查询条件 -->
<div class="search-section">
<el-form :model="queryParams" inline>
<el-form-item label="患者">
<el-input v-model="queryParams.patientName" placeholder="患者姓名" clearable style="width: 130px" />
</el-form-item>
<el-form-item label="记录类型">
<el-select v-model="queryParams.noteType" placeholder="全部" clearable style="width: 140px">
<el-option v-for="item in noteTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="书写人">
<el-input v-model="queryParams.authorName" placeholder="书写人" clearable style="width: 120px" />
</el-form-item>
<el-form-item label="超时">
<el-select v-model="queryParams.isOverdue" placeholder="全部" clearable style="width: 100px">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 列表 -->
<el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="patientName" label="患者" width="90" />
<el-table-column prop="noteType" label="记录类型" width="110">
<template #default="{ row }">
<el-tag size="small">{{ getNoteTypeName(row.noteType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="noteContent" label="内容摘要" min-width="250" show-overflow-tooltip />
<el-table-column prop="authorName" label="书写人" width="90" />
<el-table-column prop="authorTitle" label="职称" width="80" />
<el-table-column prop="deadline" label="时限" width="160">
<template #default="{ row }">
<span :class="{ 'text-danger': row.isOverdue }">{{ formatTime(row.deadline) }}</span>
</template>
</el-table-column>
<el-table-column prop="signStatus" label="签名" width="80">
<template #default="{ row }">
<el-tag :type="row.signStatus === 1 ? 'success' : 'info'" size="small">
{{ row.signStatus === 1 ? '已签' : '未签' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isOverdue" label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row.isOverdue" type="danger" size="small">超时</el-tag>
<el-tag v-else type="success" size="small">正常</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button v-if="row.signStatus === 0" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.signStatus === 0" type="success" link size="small" @click="handleSign(row)">签名</el-button>
<el-button type="info" link size="small" @click="handleDetail(row)">查看</el-button>
<el-button v-if="row.signStatus === 0" type="danger" link size="small" @click="handleDelete(row)">删除</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]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSearch"
@current-change="handleSearch"
style="margin-top: 16px; text-align: right"
/>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="750px" destroy-on-close>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="formData.patientName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="就诊ID" prop="encounterId">
<el-input-number v-model="formData.encounterId" :min="1" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者ID" prop="patientId">
<el-input-number v-model="formData.patientId" :min="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="记录类型" prop="noteType">
<el-select v-model="formData.noteType" placeholder="请选择" style="width: 100%">
<el-option v-for="item in noteTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="书写人" prop="authorName">
<el-input v-model="formData.authorName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职称">
<el-input v-model="formData.authorTitle" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="记录内容" prop="noteContent">
<el-input v-model="formData.noteContent" type="textarea" :rows="10" placeholder="请输入病程记录内容" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getNotePage, getNoteDetail, addNote, updateNote, deleteNote, signNote, getMonitor } from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showMonitor = ref(false)
const monitorData = ref({})
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref(null)
const noteTypeOptions = [
{ label: '首次病程记录', value: 1 },
{ label: '日常病程记录', value: 2 },
{ label: '上级医师查房', value: 3 },
{ label: '疑难病例讨论', value: 4 },
{ label: '阶段小结', value: 5 },
{ label: '抢救记录', value: 6 },
{ label: '转科记录', value: 7 },
{ label: '接收记录', value: 8 },
{ label: '出院记录', value: 9 },
{ label: '死亡记录', value: 10 }
]
const queryParams = reactive({ patientName: '', noteType: null, authorName: '', encounterId: null, isOverdue: null, pageNo: 1, pageSize: 20 })
const formData = reactive({
id: null, patientName: '', encounterId: null, patientId: null, noteType: null,
noteContent: '', authorName: '', authorTitle: ''
})
const formRules = {
patientName: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }],
encounterId: [{ required: true, message: '请输入就诊ID', trigger: 'blur' }],
noteType: [{ required: true, message: '请选择记录类型', trigger: 'change' }],
noteContent: [{ required: true, message: '请输入记录内容', trigger: 'blur' }]
}
const getNoteTypeName = (type) => noteTypeOptions.find(o => o.value === type)?.label || '未知'
const formatTime = (t) => t || '-'
const handleSearch = async () => {
loading.value = true
try {
const res = await getNotePage(queryParams)
if (res.data) { tableData.value = res.data.records || []; total.value = res.data.total || 0 }
const monRes = await getMonitor({})
if (monRes.data) monitorData.value = monRes.data
} finally { loading.value = false }
}
const resetQuery = () => {
Object.assign(queryParams, { patientName: '', noteType: null, authorName: '', isOverdue: null, pageNo: 1 })
handleSearch()
}
const handleAdd = () => {
dialogTitle.value = '新建病程记录'
Object.assign(formData, { id: null, patientName: '', encounterId: null, patientId: null, noteType: null, noteContent: '', authorName: '', authorTitle: '' })
dialogVisible.value = true
}
const handleEdit = async (row) => {
dialogTitle.value = '编辑病程记录'
const res = await getNoteDetail(row.id)
if (res.data) Object.assign(formData, res.data)
dialogVisible.value = true
}
const handleSubmit = async () => {
await formRef.value.validate()
if (formData.id) { await updateNote(formData); ElMessage.success('修改成功') }
else { await addNote(formData); ElMessage.success('新增成功') }
dialogVisible.value = false
handleSearch()
}
const handleSign = async (row) => {
await ElMessageBox.confirm('确认签名?签名后不可修改', '签名确认')
await signNote({ id: row.id })
ElMessage.success('签名成功')
handleSearch()
}
const handleDelete = async (row) => {
await ElMessageBox.confirm('确认删除此病程记录?', '删除确认')
await deleteNote(row.id)
ElMessage.success('删除成功')
handleSearch()
}
const handleDetail = async (row) => {
const res = await getNoteDetail(row.id)
if (res.data) {
await ElMessageBox.alert(
`<div style="line-height:2;white-space:pre-wrap">${res.data.noteContent || '无内容'}</div>`,
`${getNoteTypeName(res.data.noteType)} - ${res.data.patientName}`,
{ dangerouslyUseHTMLString: true, customStyle: { maxWidth: '700px', maxHeight: '500px', overflow: 'auto' } }
)
}
}
onMounted(() => handleSearch())
</script>
<style scoped>
.progress-notes-container { padding: 16px; }
.page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
.tab-title { font-size: 18px; font-weight: bold; }
.search-section { margin-bottom: 16px; }
.monitor-panel { margin-bottom: 16px; }
.monitor-card { text-align: center; }
.monitor-value { font-size: 32px; font-weight: bold; }
.monitor-label { font-size: 14px; color: #666; margin-top: 4px; }
.monitor-card.overdue .monitor-value { color: #f56c6c; }
.monitor-card.warning .monitor-value { color: #e6a23c; }
.monitor-card.normal .monitor-value { color: #67c23a; }
.text-danger { color: #f56c6c; font-weight: bold; }
</style>