Compare commits

..

1 Commits

Author SHA1 Message Date
e8d10befbd fix(#613): 医生端医嘱列表增加退回原因展示列
根因(全链路6环分析):
- ① 前端/页面  医生端医嘱列表无退回原因列 → 无法展示护士填写的退回原因
- ② Controller  不涉及 — 纯转发层
- ③ Service  getRequestBaseInfo() 未填充 reasonText 字段
- ④ Mapper/XML  UNION ALL 查询未选取 back_reason/reason_text 字段
- ⑤ DB  med_medication_request.back_reason 列已存在(上一次修复已迁移)
- ⑥ 关联模块 ⚠️ wor_service_request.reason_text 已存在但未在查询中暴露

修复:
1. RequestBaseDto.java: 新增 reasonText 字段(映射退回原因)
2. DoctorStationAdviceAppMapper.xml: 5 个 UNION ALL 分支各自选取 reason_text
   - med_medication_request → T1.back_reason
   - charge item 回补 → T2.back_reason
   - device_request(2 处)→ NULL(无退回原因字段)
   - wor_service_request → T1.reason_text
3. prescriptionlist.vue: 在诊断列前新增退回原因列

全链路状态流转:
护士端弹窗→输入原因→API传backReason→DB保存→医生端列表展示
                ↑ 本次修复打通最后一环 ↑
2026-05-29 15:53:36 +08:00
78 changed files with 2847 additions and 2970 deletions

View File

@@ -3,8 +3,6 @@
> **模型决定上限Harness 决定底线。**
> 本文件是 OpenHIS 项目的 Harness Engineering 落地。整合了 OpenAI/Anthropic Harness Engineering 方法论与 walkinglabs 实战模式。
> **🔴 铁律统一文件**: `/root/.codex/rules/IRON_LAWS.md` — 所有智能体必须遵守,运行时自动加载。
---
## 📋 项目信息
@@ -157,66 +155,6 @@ Harness: .harness/ (init.sh, PROGRESS.md, feature_list.json, ...)
---
## 🚨 铁律(不可违反 — 来自实际 Bug 教训)
### 状态值一致性
涉及状态流转的 Bug修改前**必须**列出完整链路并逐项检查
1. 枚举定义 `SlotStatus``OrderStatus`的数值
2. Service 层设置的状态值是否与枚举一致
3. 查询/列表接口的状态映射是否覆盖所有枚举值
4. 前端 `STATUS_CLASS_MAP` 是否包含新状态
5. 前端过滤条件`v-if``v-for`是否兼容新状态
6. /统计表的聚合 SQL 是否包含新状态值
**禁止**只改一端不检查其他端必须全链路对齐
### 禁止删除源文件
- **绝对禁止**删除项目中已有的 Java/Vue/SQL 源文件
- 编译错误 修复错误不删除文件
- 重复文件 重构合并不删除文件
- AI 幻觉文件 检查 `git ls-tree baseline -- <file>` 确认后再删除
- **唯一例外**人类明确确认删除
### 全链路验证(状态流转 Bug 必做)
修复后按以下顺序验证**编译通过不等于修复完成**
```
① 数据库SELECT status FROM table WHERE id = ? → 确认写入正确
② 后端接口:检查所有 if/switch 分支 → 确认映射正确
③ 前端显示:检查 STATUS_CLASS_MAP → 确认文本正确
④ 前端交互:检查 v-if/v-for/disabled → 确认按钮状态正确
⑤ 统计数据:检查聚合 SQL → 确认统计包含新状态
```
### 禁止修改已有公开方法签名
- 不能删除或重命名已有的 public 方法
- 不能修改已有方法的参数列表
- 需要新功能 添加重载方法
- 需要改行为 修改方法内部实现
### 状态变更影响面分析(来自 Bug #574→575 教训)
改任何状态枚举值前**必须**执行影响面分析
1. `rg "原状态枚举名" --type java` 列出所有引用文件
2. 逐个检查设置值查询过滤显示映射统计聚合
3. 检查逆向流程退号取消停诊是否兼容新状态
4. 检查 XML mapper 中所有查询过滤条件
5. 检查前端 STATUS_CLASS_MAP 和所有 v-if/v-for 条件
**禁止**只改正向流程不验逆向流程
### 逆向流程验证(来自 Bug #575 教训)
涉及状态流转的 Bug验证时**必须**覆盖
- 正向预约签到就诊完成
- 逆向退号取消预约停诊退费
- 边界并发操作重复操作异常中断
**禁止**只测正向流程就标记"修复完成"
### 搜索所有相关代码路径
修复前必须用 `rg` 搜索
```
rg "状态枚举名\|相关方法名\|相关字段名" --type java --type vue
```
确保不遗漏任何引用该状态的代码路径
## 📐 代码风格规范
### Java 后端
@@ -268,14 +206,6 @@ rg "状态枚举名\|相关方法名\|相关字段名" --type java --type vue
---
## 📈 过往 Bug 教训
| Bug | 教训 |
|---|---|
| #574 | `checkInTicket()` 状态值写错BOOKED应为CHECKED_IN前端映射缺失池统计漏计根因没走完整状态链路 |
| #574 | AI 智能体看到编译错误直接删文件没检查 git baseline根因没验证文件来源 |
| #574 | 多次 fallback 修复改错文件OrderServiceImpl没触及真正问题TicketServiceImpl)。根因没用 rg 搜索所有引用 |
## 📈 成熟度追踪
| 等级 | 特征 | 本项目 |

View File

@@ -1,33 +0,0 @@
# Bug #632 修复报告
## 基本信息
- **标题**: Bug #632 测试完成,请验收。提出人: chenxj。
- **严重程度**: 待查
- **提出人**: chenxj
- **修复时间**: 15:49:42 ~ 16:01:30
- **修复耗时**: 662.1s
- **Commit**: `213568233222`
## 根因分析
Bug #632 修复完成。核心问题是 JavaScript `&&` 运算符的经典陷阱——当所有条件为 truthy 时,`&&` 返回最后一个操作数(`item.packageName` 字符串 `"肝功能12项"`),而非 `true`。两处 `Boolean()` 强制转换确保 `isPackage` 始终为布尔值。
| #
## 修复文件
.../src/main/java/com/openhis/lab/domain/InspectionPackage.java | 3 +++
.../src/main/java/com/openhis/lab/domain/InspectionPackageDetail.java | 3 +++
## 流程时间线
| 时间 | 智能体 | 事件 | 状态 | 耗时 |
|------|--------|------|------|------|
| 15:49:42 | guanyu | fix_start | ⏳ | 0.0s |
| 16:01:30 | guanyu | fix_done | ✅ | 662.1s |
| 16:01:36 | zhugeliang | analyze_done | ✅ | 0.0s |
|------|--------|------|------|------|
| 16:01:38 | chenlin | doc_done | ✅ | <1s |
## 测试结果
- **结果**: FAIL
- **输出**:
## 全流程完成
诸葛亮分析 guanyu 修复 张飞测试 华佗验收 陈琳归档

View File

@@ -1,35 +0,0 @@
# Bug #634 修复报告
## 基本信息
- **标题**: [系统维护-检验套餐] 保存套餐失败,报 JSON 反序列化日期解析异常 (LocalDateTime)
- **严重程度**: 致命
- **提出人**: chenxj
- **修复时间**: 15:21:28 ~ 15:27:25
- **修复耗时**: 357.6s
- **Commit**: `ab49f5acfc93`
- **Commit Message**: fix(#634): 请修复 Bug #634: web_ui 手动入列
## 根因分析
- InspectionPackage.java 和 InspectionPackageDetail.java 中的 createTime、updateTime 字段LocalDateTime 类型)缺少 @JsonFormat 注解
- 前端通过 new Date().toISOString() 发送 ISO 8601 格式日期字符串(含毫秒 + Z 时区后缀Jackson 反序列化失败
## 修复文件
.../core/framework/config/ApplicationConfig.java | 37 ++++++++++++++++++++--
1 file changed, 35 insertions(+), 2 deletions(-)
## 流程时间线
| 时间 | 智能体 | 事件 | 状态 | 耗时 |
|------|--------|------|------|------|
| 15:21:28 | guanyu | fix_start | ⏳ | - |
| 15:27:25 | guanyu | fix_done | ✅ | 357.6s |
| 15:27:28 | zhugeliang | analyze_done | ✅ | 0.0s |
| 15:27:31 | zhangfei | test_done | ✅ | 0.0s |
| 15:27:33 | huatuo | verify_done | ✅ | 0.0s |
| 15:27:33 | chenlin | doc_done | ✅ | 0.0s |
## 测试结果
- **结果**: ✅ PASS
- **Playwright**: @bug634 无头浏览器测试通过
## 全流程完成
诸葛亮分析 → guanyu 修复 → 张飞测试 → 华佗验收 → 陈琳归档

View File

@@ -1,32 +0,0 @@
# Bug #644 修复报告
## 基本信息
- **标题**: Bug #644 测试完成,请验收。提出人: chenxj。
- **提出人**: chenxj
- **修复时间**: 00:24:37 ~ 00:32:06
- **修复耗时**: 347.9s
- **Commit**: `bd50c58dd`
- **测试结果**: ❌ FAIL
## 根因分析
## 变更摘要
### 根因分析
**Issue 1 — 状态不同步**`getInpatientAdvicePage` 方法中,执行记录(`exePerformRecordList`)的计算被包裹在 `if (exeStatus != null)` 条件内,只有在"医嘱执行"页签(传 `exeStatus` 参数)时才计算。"已校对"页签不传 `exeStatus`,因此执行记录永远不会被
## 修复文件
.../impl/AdviceProcessAppServiceImpl.java | 89 +++++++++++++++-------
.../dto/InpatientAdviceDto.java | 3 +
## 流程时间线
| 时间 | 智能体 | 事件 | 状态 | 耗时 |
|------|--------|------|------|------|
| 00:24:37 | guanyu | fix_start | ⏳ | 0.0s |
| 00:25:39 | guanyu | fix_retry | ❓ | 0.0s |
| 00:32:06 | guanyu | fix_done | ✅ | 347.9s |
| 00:32:09 | zhugeliang | analyze_done | ✅ | 0.0s |
| 00:32:11 | chenlin | doc_done | ✅ | <1s |
## 全流程
诸葛亮分析 guanyu 修复 张飞测试 华佗验收 陈琳归档

View File

@@ -1,9 +1,7 @@
package com.core.framework.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
@@ -11,7 +9,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
@@ -27,36 +24,6 @@ import java.util.TimeZone;
// 指定要扫描的Mapper类的包的路径
@MapperScan({"com.core.**.mapper", "com.openhis.**.mapper"})
public class ApplicationConfig {
/** 支持多种日期格式的反序列化器 */
private static final JsonDeserializer<LocalDateTime> LOCAL_DATE_TIME_DESERIALIZER = new JsonDeserializer<LocalDateTime>() {
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
private static final DateTimeFormatter SIMPLE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter SLASH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String text = p.getText();
if (text == null || text.isEmpty()) {
return null;
}
// 去除时区后缀 Z/z 和偏移量 +HH:MM/+HHMMLocalDateTime 不含时区信息)
String cleaned = text.replaceAll("[Zz]$", "").replaceAll("[+-]\\d{2}:?\\d{2}$", "");
// 尝试 ISO 8601 格式yyyy-MM-ddTHH:mm:ss.SSS
try {
return LocalDateTime.parse(cleaned, ISO_FORMATTER);
} catch (Exception ignored) {
}
// 尝试简单格式yyyy-MM-dd HH:mm:ss
try {
return LocalDateTime.parse(cleaned, SIMPLE_FORMATTER);
} catch (Exception ignored) {
}
// 尝试斜杠格式yyyy/M/d HH:mm:ss
return LocalDateTime.parse(cleaned, SLASH_FORMATTER);
}
};
/**
* 时区配置
*/
@@ -69,7 +36,7 @@ public class ApplicationConfig {
builder.simpleDateFormat("yyyy/M/d HH:mm:ss");
// 添加JavaTimeModule支持用于LocalDateTime
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
builder.modules(javaTimeModule);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss")));
};

View File

@@ -207,12 +207,6 @@ public class TicketAppServiceImpl implements ITicketAppService {
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CHECKED_IN) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已签到");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) {
@@ -394,12 +388,6 @@ public class TicketAppServiceImpl implements ITicketAppService {
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CHECKED_IN) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已签到");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) {

View File

@@ -660,12 +660,10 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
return appointmentOrder.getId();
}
// 已预约(1)或已签到(3)的号源能退号
// 只有已预约(1)的号源能退号,对应签到后的 BOOKED 状态
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null ||
(!SlotStatus.BOOKED.getValue().equals(slot.getStatus()) &&
!SlotStatus.CHECKED_IN.getValue().equals(slot.getStatus()))) {
log.warn("退号跳过:槽位状态不允许退号, slotId={}, status={}", slotId,
if (slot == null || !SlotStatus.BOOKED.getValue().equals(slot.getStatus())) {
log.warn("退号跳过:槽位非已预约状态, slotId={}, status={}", slotId,
slot != null ? slot.getStatus() : null);
return appointmentOrder.getId();
}
@@ -678,8 +676,11 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
// 退号时刷新池统计(兼容 BOOKED 和 CHECKED_IN 状态)
schedulePoolMapper.refreshPoolStats(poolId, SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue());
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("booked_num = booked_num - 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return appointmentOrder.getId();
} catch (Exception e) {

View File

@@ -39,7 +39,6 @@ import com.openhis.web.clinicalmanage.appservice.ISurgeryAppService;
import com.openhis.web.clinicalmanage.dto.SurgeryDto;
import com.openhis.web.clinicalmanage.mapper.SurgeryAppMapper;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.service.IServiceRequestService;
import org.springframework.beans.BeanUtils;
@@ -366,21 +365,7 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
serviceRequest.setPrescriptionNo(prescriptionNo);
serviceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());// 治疗类型
serviceRequest.setQuantity(BigDecimal.valueOf(1)); // 请求数量
// 从诊疗目录获取使用单位,避免硬编码
String unitCode = ""; // 默认值
String surgeryCode = surgeryDto.getSurgeryCode();
if (surgeryCode != null && !surgeryCode.isEmpty()) {
ActivityDefinition activityDef = activityDefinitionService.getOne(
new LambdaQueryWrapper<ActivityDefinition>()
.eq(ActivityDefinition::getBusNo, surgeryCode)
.eq(ActivityDefinition::getCategoryCode, "24")
);
if (activityDef != null && activityDef.getPermittedUnitCode() != null
&& !activityDef.getPermittedUnitCode().isEmpty()) {
unitCode = activityDef.getPermittedUnitCode();
}
}
serviceRequest.setUnitCode(unitCode); // 请求单位编码
serviceRequest.setUnitCode(""); // 请求单位编码
serviceRequest.setCategoryEnum(24); // 请求类型24-手术(新值域,避开 adviceType 碰撞)
serviceRequest.setActivityId(surgeryId); // 手术ID作为诊疗定义id
serviceRequest.setPatientId(surgeryDto.getPatientId()); // 患者

View File

@@ -14,7 +14,6 @@ import com.core.common.exception.ServiceException;
import com.core.common.utils.AssignSeqUtil;
import com.core.common.utils.MessageUtils;
import com.core.common.utils.SecurityUtils;
import com.core.common.utils.DictUtils;
import com.core.common.utils.StringUtils;
import com.core.web.util.TenantOptionUtil;
import com.openhis.administration.domain.Account;
@@ -1921,7 +1920,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
Surgery surgery = iSurgeryService.getOne(
new LambdaQueryWrapper<Surgery>()
.eq(Surgery::getSurgeryNo, prescriptionNo)
.and(w -> w.isNull(Surgery::getDeleteFlag).or().eq(Surgery::getDeleteFlag, "0")).last("LIMIT 1"));
.and(w -> w.isNull(Surgery::getDeleteFlag).or().eq(Surgery::getDeleteFlag, "0")));
if (surgery != null) {
iSurgeryService.removeById(surgery.getId());
log.info("handService - 级联删除手术记录 cli_surgery: surgeryNo={}, id={}", prescriptionNo, surgery.getId());
@@ -2187,7 +2186,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.eq(ChargeItem::getServiceId, adviceSaveDto.getRequestId())
.eq(ChargeItem::getServiceTable, CommonConstants.TableName.WOR_SERVICE_REQUEST)
.eq(ChargeItem::getDeleteFlag, DelFlag.NO.getCode())
.last("LIMIT 1"));
);
log.info("BugFix#328: 通过requestId查询费用项requestId={}, chargeItem={}",
adviceSaveDto.getRequestId(), existingChargeItem != null ? existingChargeItem.getId() : "null");
}
@@ -2241,14 +2240,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 收费状态
requestBaseDto.setChargeStatus_enumText(
EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus()));
// 单位字典翻译:优先通过 unit_code 字典翻译编码值,失败时回退使用原始值
// 单位字典翻译失败时回退使用原始值(如手术申请硬编码了中文单位名)
if (StringUtils.isNotBlank(requestBaseDto.getUnitCode()) && StringUtils.isBlank(requestBaseDto.getUnitCode_dictText())) {
String dictLabel = DictUtils.getDictLabel("unit_code", requestBaseDto.getUnitCode());
if (StringUtils.isNotBlank(dictLabel)) {
requestBaseDto.setUnitCode_dictText(dictLabel);
} else {
requestBaseDto.setUnitCode_dictText(requestBaseDto.getUnitCode());
}
requestBaseDto.setUnitCode_dictText(requestBaseDto.getUnitCode());
}
}
return R.ok(requestBaseInfo);
@@ -2301,7 +2295,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
new LambdaQueryWrapper<InventoryItem>()
.eq(InventoryItem::getItemId, dispense.getMedicationId())
.eq(InventoryItem::getLotNumber, dispense.getLotNumber())
.last("LIMIT 1"));
);
if (inventoryItem != null) {
// 计算回滚后的数量(加上已发放的数量)
@@ -2388,52 +2382,21 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.map(UpdateGroupDto::getRequestId).collect(Collectors.toList());
if (!idsToSetNull.isEmpty()) {
// 对三个表都执行 group_id/group_no 置空(哪个表有该 id 就更新哪个)
UpdateWrapper<MedicationRequest> medUpdateWrapper = new UpdateWrapper<>();
medUpdateWrapper.set("group_id", null).in("id", idsToSetNull);
iMedicationRequestService.update(medUpdateWrapper);
// 创建更新条件
UpdateWrapper<MedicationRequest> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("group_id", null).in("id", idsToSetNull);
UpdateWrapper<ServiceRequest> srvUpdateWrapper = new UpdateWrapper<>();
srvUpdateWrapper.set("group_id", null).in("id", idsToSetNull);
iServiceRequestService.update(srvUpdateWrapper);
// DeviceRequest 使用 group_noString 类型)
UpdateWrapper<DeviceRequest> devUpdateWrapper = new UpdateWrapper<>();
devUpdateWrapper.set("group_no", null).in("id", idsToSetNull);
iDeviceRequestService.update(devUpdateWrapper);
// 执行更新
iMedicationRequestService.update(updateWrapper);
}
// 处理 groupId 非 null 的情况:按实际所属表分别更新
List<UpdateGroupDto> nonNullGroupList = groupList.stream()
.filter(dto -> dto.getGroupId() != null).collect(Collectors.toList());
if (!nonNullGroupList.isEmpty()) {
for (UpdateGroupDto dto : nonNullGroupList) {
Long reqId = dto.getRequestId();
Long grpId = dto.getGroupId();
// 先尝试药品表med_medication_request → group_id
MedicationRequest medReq = iMedicationRequestService.getById(reqId);
if (medReq != null) {
UpdateWrapper<MedicationRequest> uw = new UpdateWrapper<>();
uw.set("group_id", grpId).eq("id", reqId);
iMedicationRequestService.update(uw);
continue;
}
// 再尝试诊疗表wor_service_request → group_id
ServiceRequest srvReq = iServiceRequestService.getById(reqId);
if (srvReq != null) {
UpdateWrapper<ServiceRequest> uw = new UpdateWrapper<>();
uw.set("group_id", grpId).eq("id", reqId);
iServiceRequestService.update(uw);
continue;
}
// 最后尝试耗材表wor_device_request → group_no, String 类型)
DeviceRequest devReq = iDeviceRequestService.getById(reqId);
if (devReq != null) {
UpdateWrapper<DeviceRequest> uw = new UpdateWrapper<>();
uw.set("group_no", grpId != null ? grpId.toString() : null).eq("id", reqId);
iDeviceRequestService.update(uw);
}
}
// 处理null的情况
List<MedicationRequest> medicationRequestList = groupList.stream().filter(dto -> dto.getGroupId() != null)
.map(dto -> new MedicationRequest().setId(dto.getRequestId()).setGroupId(dto.getGroupId()))
.collect(Collectors.toList());
if (!medicationRequestList.isEmpty()) {
iMedicationRequestService.saveOrUpdateBatch(medicationRequestList);
}
}

View File

@@ -75,7 +75,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
Emr emr = new Emr();
BeanUtils.copyProperties(patientEmrDto, emr);
String contextStr = patientEmrDto.getContextJson().toString();
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()));
boolean saveSuccess;
// 如果已经保存病历,再次保存走更新
if (patientEmr != null) {
@@ -122,10 +122,6 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
*/
@Override
public R<?> getPatientEmrHistory(PatientEmrDto patientEmrDto, Integer pageNo, Integer pageSize) {
// 校验参数
if (patientEmrDto.getPatientId() == null) {
return R.ok(new Page<>(pageNo, pageSize));
}
Page<Emr> page = emrService.page(new Page<>(pageNo, pageSize),
new LambdaQueryWrapper<Emr>().eq(Emr::getPatientId, patientEmrDto.getPatientId()));
return R.ok(page);
@@ -140,12 +136,8 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
*/
@Override
public R<?> getEmrDetail(Long encounterId) {
// 校验参数
if (encounterId == null) {
return R.ok(null);
}
// 先查询门诊病历(emr表)
Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId));
if (emrDetail != null) {
return R.ok(emrDetail);
}
@@ -155,8 +147,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
new LambdaQueryWrapper<DocRecord>()
.eq(DocRecord::getEncounterId, encounterId)
.orderByDesc(DocRecord::getCreateTime)
.last("LIMIT 1"),
false
.last("LIMIT 1")
);
if (docRecord != null) {
// 住院病历存在,也返回数据
@@ -275,7 +266,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
public R<?> checkNeedWriteEmr(Long encounterId) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId)
);
// 如果没有病历,则需要写病历

View File

@@ -274,7 +274,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
new QueryWrapper<Organization>()
.eq("bus_no", performDeptCode)
.eq("delete_flag", "0")
.last("LIMIT 1"));
);
if (organization != null) {
positionId = organization.getId();
} else {
@@ -410,7 +410,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
new QueryWrapper<InspectionLabApply>()
.eq("apply_no", applyNo)
.eq("delete_flag", DelFlag.NO.getCode())
.last("LIMIT 1"));
);
if (mainEntity == null) {
return null;
@@ -532,7 +532,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
// 1. 根据申请单号查询检验申请单信息
InspectionLabApply inspectionLabApply = inspectionLabApplyService.getOne(
new QueryWrapper<InspectionLabApply>().eq("apply_no", applyNo)
.last("LIMIT 1"));
);
if (inspectionLabApply == null) {
log.warn("未找到申请单号为 [{}] 的检验申请单", applyNo);

View File

@@ -215,7 +215,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
// 限定当天日期,避免复诊患者匹配到历史队列记录
.eq(TriageQueueItem::getQueueDate, LocalDate.now())
.eq(TriageQueueItem::getDeleteFlag, "0")
.last("LIMIT 1"));
);
if (queueItem != null) {
// 使用 TriageQueueStatus 枚举替代原有硬编码数字 20保证状态值一致性
queueItem.setStatus(TriageQueueStatus.IN_CLINIC.getValue());
@@ -282,7 +282,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
.eq(TriageQueueItem::getEncounterId, encounterId)
.eq(TriageQueueItem::getQueueDate, LocalDate.now())
.eq(TriageQueueItem::getDeleteFlag, "0")
.last("LIMIT 1"));
);
// 当天未找到时回退:不限日期查最近一条(防止跨日就诊队列项遗漏更新)
if (queueItem == null) {
@@ -292,8 +292,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
.eq(TriageQueueItem::getEncounterId, encounterId)
.eq(TriageQueueItem::getDeleteFlag, "0")
.orderByDesc(TriageQueueItem::getQueueDate)
.last("LIMIT 1"));
.last("LIMIT 1")
);
if (queueItem != null) {
log.warn("完诊:当天队列项未找到,回退使用最近队列记录 queueDate={}, id={}",
queueItem.getQueueDate(), queueItem.getId());

View File

@@ -42,7 +42,4 @@ public class SurgeryItemDto {
/** 单位编码字典文本(前端用于显示单位) */
private String unitCodeDictText;
/** 所需标本编码(来自诊疗目录配置,对应字典 specimen_code 的 dictValue */
private String specimenCode;
}

View File

@@ -582,10 +582,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
// 处理长期已发放的药品
if (!longMedDispensedList.isEmpty()) {
// 生成退药单
this.creatRefundMedicationList(longMedDispensedList, procedureIdMap);
// 药品退药请求状态变更(待退药)
medicationRequestService.updateCancelledStatusBatch(
longMedDispensedList.stream().map(MedicationDispense::getMedReqId).toList(), null, null);
this.creatRefundMedicationList(tempMedDispensedList, procedureIdMap);
}
// 处理临时已发放药品
if (!tempMedDispensedList.isEmpty()) {

View File

@@ -191,8 +191,7 @@ public class WesternMedicineDispenseAppServiceImpl implements IWesternMedicineDi
Page<EncounterInfoDto> encounterInfoPage
= westernMedicineDispenseMapper.selectEncounterInfoListPage(new Page<>(pageNo, pageSize), queryWrapper,
statusEnum, DispenseStatus.IN_PROGRESS.getValue(), DispenseStatus.COMPLETED.getValue(),
DispenseStatus.PREPARATION.getValue(), DispenseStatus.PREPARED.getValue(),
DispenseStatus.SUMMARIZED.getValue());
DispenseStatus.PREPARATION.getValue(), DispenseStatus.PREPARED.getValue());
encounterInfoPage.getRecords().forEach(encounterInfo -> {
// 性别
encounterInfo.setGenderEnum_enumText(
@@ -230,7 +229,7 @@ public class WesternMedicineDispenseAppServiceImpl implements IWesternMedicineDi
= westernMedicineDispenseMapper.selectMedicineDispenseOrderPage(new Page<>(pageNo, pageSize), queryWrapper,
DispenseStatus.IN_PROGRESS.getValue(), DispenseStatus.COMPLETED.getValue(),
DispenseStatus.PREPARATION.getValue(), DispenseStatus.PREPARED.getValue(), dispenseStatus,
PublicationStatus.ACTIVE.getValue(), DispenseStatus.SUMMARIZED.getValue());
PublicationStatus.ACTIVE.getValue());
medicineDispenseOrderPage.getRecords().forEach(medicineDispenseOrder -> {
// 发药状态
medicineDispenseOrder.setStatusEnum_enumText(

View File

@@ -35,7 +35,7 @@ public interface WesternMedicineDispenseMapper {
@Param(Constants.WRAPPER) QueryWrapper<EncounterInfoSearchParam> queryWrapper,
@Param("statusEnum") Integer statusEnum, @Param("inProgress") Integer inProgress,
@Param("completed") Integer completed, @Param("preparation") Integer preparation,
@Param("prepared") Integer prepared, @Param("summarized") Integer summarized);
@Param("prepared") Integer prepared);
/**
* 发药单查询
@@ -54,8 +54,7 @@ public interface WesternMedicineDispenseMapper {
@Param(Constants.WRAPPER) QueryWrapper<ItemDispenseOrderDto> queryWrapper,
@Param("inProgress") Integer inProgress, @Param("completed") Integer completed,
@Param("preparation") Integer preparation, @Param("prepared") Integer prepared,
@Param("dispenseStatus") Integer dispenseStatus, @Param("active") Integer active,
@Param("summarized") Integer summarized);
@Param("dispenseStatus") Integer dispenseStatus, @Param("active") Integer active);
/**
* 获取配药人下拉选列表

View File

@@ -660,7 +660,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
longServiceRequest.setOrgId(regAdviceSaveDto.getEffectiveOrgId()); // 执行科室
longServiceRequest.setOrgId(regAdviceSaveDto.getPositionId()); // 执行科室
longServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
longServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
longServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
@@ -712,7 +712,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
tempServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
tempServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
tempServiceRequest.setAuthoredTime(curDate); // 请求签发时间
tempServiceRequest.setOrgId(regAdviceSaveDto.getEffectiveOrgId()); // 执行科室
tempServiceRequest.setOrgId(regAdviceSaveDto.getPositionId()); // 执行科室
tempServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
tempServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
tempServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id

View File

@@ -157,14 +157,9 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
} else {
// 根据申请单类型生成不同前缀的单号
String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
AssignSeqEnum seqEnum;
if (ActivityDefCategory.PROCEDURE.getCode().equals(typeCode)) {
seqEnum = AssignSeqEnum.SURGERY_APPLY_NO;
} else if (ActivityDefCategory.PROOF.getCode().equals(typeCode)) {
seqEnum = AssignSeqEnum.LAB_APPLY_NO;
} else {
seqEnum = AssignSeqEnum.CHECK_APPLY_NO;
}
AssignSeqEnum seqEnum = ActivityDefCategory.PROCEDURE.getCode().equals(typeCode)
? AssignSeqEnum.SURGERY_APPLY_NO
: AssignSeqEnum.CHECK_APPLY_NO;
int seq = assignSeqUtil.getSeqNoByDay(seqEnum.getPrefix());
prescriptionNo = seqEnum.getPrefix() + dateStr + String.format("%05d", seq);
}
@@ -342,25 +337,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
surgeryServiceRequest.setPrescriptionNo(prescriptionNo);
surgeryServiceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
surgeryServiceRequest.setQuantity(BigDecimal.valueOf(1));
// 从诊疗目录获取使用单位,避免硬编码
String unitCode = ""; // 默认值
if (activityList != null && !activityList.isEmpty()) {
String dtoUnitCode = activityList.get(0).getUnitCode();
if (dtoUnitCode != null && !dtoUnitCode.isEmpty()) {
unitCode = dtoUnitCode;
} else {
// 从 ActivityDefinition 查询使用单位
Long activityId = activityList.get(0).getAdviceDefinitionId();
if (activityId != null) {
ActivityDefinition activityDef = iActivityDefinitionService.getById(activityId);
if (activityDef != null && activityDef.getPermittedUnitCode() != null
&& !activityDef.getPermittedUnitCode().isEmpty()) {
unitCode = activityDef.getPermittedUnitCode();
}
}
}
}
surgeryServiceRequest.setUnitCode(unitCode);
surgeryServiceRequest.setUnitCode("");
surgeryServiceRequest.setCategoryEnum(24); // 24-手术(新值域,避开 adviceType 碰撞)
// 优先从 activityList 获取手术 ID
if (activityList != null && !activityList.isEmpty()) {

View File

@@ -10,4 +10,8 @@ import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class RegAdviceSaveDto extends AdviceSaveDto {
/** 请求类型 */
private Integer categoryEnum;
}

View File

@@ -920,8 +920,7 @@
t2.ID AS charge_item_definition_id,
t2.price AS price,
t1.permitted_unit_code AS unit_code,
t1.permitted_unit_code AS unit_code_dict_text,
t1.specimen_code AS specimen_code
t1.permitted_unit_code AS unit_code_dict_text
FROM wor_activity_definition t1
LEFT JOIN adm_charge_item_definition t2
ON t2.instance_id = t1.ID

View File

@@ -97,10 +97,10 @@
ON T4.med_req_id = T5.id
AND T5.delete_flag = '0'
WHERE <if test="statusEnum == null">
T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},#{summarized})
T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})
</if>
<if test="statusEnum == 3">
T4.status_enum IN (#{inProgress},#{preparation},#{prepared},#{summarized})
T4.status_enum IN (#{inProgress},#{preparation},#{prepared})
</if>
<if test="statusEnum == 4">
T4.status_enum = #{completed}
@@ -269,10 +269,10 @@
AND T1.summary_no != ''
AND
<if test="dispenseStatus == null">
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},#{summarized})
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})
</if>
<if test="dispenseStatus == 3">
T1.status_enum IN (#{inProgress},#{preparation},#{prepared},#{summarized})
T1.status_enum IN (#{inProgress},#{preparation},#{prepared})
</if>
<if test="dispenseStatus == 4">
T1.status_enum = #{completed}

View File

@@ -219,7 +219,6 @@
T1.effective_dose_start AS start_time,
T1.based_on_id AS based_on_id,
T1.medication_id AS advice_definition_id,
T1.content_json::jsonb ->> 'remark' AS remark,
T1.effective_dose_end AS stop_time,
T1.update_by AS stop_user_name
FROM med_medication_request AS T1
@@ -276,7 +275,6 @@
T1.req_authored_time AS start_time,
T1.based_on_id AS based_on_id,
T1.device_def_id AS advice_definition_id,
T1.content_json::jsonb ->> 'remark' AS remark,
NULL::timestamp AS stop_time,
'' AS stop_user_name
FROM wor_device_request AS T1
@@ -330,7 +328,6 @@
T1.occurrence_start_time AS start_time,
T1.based_on_id AS based_on_id,
T1.activity_id AS advice_definition_id,
T1.remark AS remark,
T1.occurrence_end_time AS stop_time,
T1.update_by AS stop_user_name
FROM wor_service_request AS T1

View File

@@ -278,10 +278,6 @@ public enum AssignSeqEnum {
* 手术申请单号(住院)
*/
SURGERY_APPLY_NO("73", "手术申请单号", "SSZ"),
/**
* 检验申请单号(住院)
*/
LAB_APPLY_NO("74", "检验申请单号", "JYZ"),
/**
* b 病历文书
*/

View File

@@ -24,7 +24,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND (s.status = #{bookedStatus} OR s.status = 3)
AND s.status = #{bookedStatus}
), 0),
locked_num = COALESCE((
SELECT COUNT(1)
@@ -42,7 +42,7 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
@Param("lockedStatus") Integer lockedStatus);
/**
* 签到时更新号源池统计:锁定数-1已约数+1
* 签到时更新号源池统计:锁定数-1约数+1
*
* @param poolId 号源池ID
* @return 结果

View File

@@ -329,16 +329,16 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 只有锁定态(2)的号源才能签到,签到时 2→3(LOCKED→CHECKED_IN)
// 2. 只有锁定态(2)的号源才能签到,签到时 2→1(LOCKED→BOOKED)
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
throw new RuntimeException("号源状态异常,无法签到");
}
// 3. 更新号源槽位状态 2→3LOCKED→CHECKED_IN已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 3. 更新号源槽位状态 2→1LOCKED→BOOKED已预约=已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 4. 更新号源池统计:锁定数-1签到数+1
// 4. 更新号源池统计:锁定数-1预约数+1
if (slot != null && slot.getPoolId() != null) {
schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId());
}

View File

@@ -112,7 +112,7 @@ public class MedicationRequest extends HisBaseEntity {
private String supportInfo;
/** 退回原因 */
private String backReason = "";
private String backReason;
/** 请求开始时间 */
private Date reqAuthoredTime;

View File

@@ -340,8 +340,8 @@
OR d.is_stopped = FALSE
)
</when>
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status) or '已签到'.equals(query.status)">
AND (<include refid="slotStatusNormExpr" /> = 1 OR <include refid="slotStatusNormExpr" /> = 3)
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE

View File

@@ -26,4 +26,3 @@ yarn.lock
test-results/
tests/e2e/report/
tests/tests/
vite.config.js.timestamp*

View File

@@ -279,7 +279,7 @@
</div>
<!-- 7. 已预约患者信息 -->
<div
v-if="(item.status === '已预约' || item.status === '已取号' || item.status === '已签到') && item.patientName"
v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName"
class="ticket-patient"
>
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
@@ -472,7 +472,6 @@ const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已签到': 'status-checked',
'已退号': 'status-returned',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled'

View File

@@ -115,6 +115,7 @@
v-model="form.categoryCode"
clearable
filterable
:disabled="form.isEditInfoDisable === 1"
no-data-text=""
>
<el-option
@@ -191,6 +192,7 @@
clearable
filterable
style="width: 240px"
:disabled="form.isEditInfoDisable === 1 || form.isEditInfoDisable === 2"
no-data-text=""
>
<el-option
@@ -256,6 +258,7 @@
placeholder=""
clearable
filterable
:disabled="form.isEditInfoDisable === 1"
no-data-text=""
>
<el-option
@@ -320,6 +323,7 @@
<el-input
v-model="form.retailPrice"
placeholder=""
:disabled="form.isEditInfoDisable === 1"
@input="updatePrices"
/>
</el-form-item>
@@ -400,6 +404,7 @@
controls-position="right"
:min="1"
:max="999"
:disabled="form.isEditInfoDisable === 1"
@change="calculateTotalPrice"
/>
</el-form-item>
@@ -600,6 +605,8 @@ function calculateTotalPrice() {
);
if (hasValidItem) {
form.value.retailPrice = parseFloat(totalPrice.value) || 0;
} else {
form.value.retailPrice = undefined;
}
} catch (error) {
totalPrice.value = '0.00';

View File

@@ -375,7 +375,7 @@
>
<template #default="scope">
<span v-if="!scope.row.isEdit">
{{ formatUnitText(scope.row) }}
{{ scope.row.quantity ? scope.row.quantity + ' ' + scope.row.unitCode_dictText : '' }}
</span>
</template>
</el-table-column>
@@ -613,26 +613,6 @@ function getRowDisabled(row) {
return row.isEdit;
}
function formatUnitText(row) {
if (!row.quantity) return ''
const unitText = row.unitCode_dictText
// unitCode_dictText 为有效文本时直接使用
if (unitText && !/^\d+$/.test(unitText)) return row.quantity + ' ' + unitText
// 优先从行级 unitCodeList 查找
const list = row.unitCodeList
if (list && list.length) {
const match = list.find(u => u.value === row.unitCode)
if (match) return row.quantity + ' ' + match.label
}
// 回退:从字典 unit_code 查找
if (unit_code.value && unit_code.value.length) {
const dictMatch = unit_code.value.find(d => d.value === row.unitCode)
if (dictMatch) return row.quantity + ' ' + dictMatch.label
}
// 最后兜底用 unitCode
return row.quantity + ' ' + (row.unitCode || '')
}
/**
* 是否已由医生接诊(非待诊)
* EncounterStatus: 1=待诊 2=在诊 3=暂离 …

View File

@@ -707,7 +707,7 @@
class="item-checkbox"
@change="(val) => handleItemSelect(val, item, cat)"
>
{{ getDisplayItemName(item) }}
{{ item.name }}
</el-checkbox>
<span class="item-price">¥{{ item.price }}/{{ item.unit || "" }}</span>
</div>
@@ -806,7 +806,7 @@
<div
v-for="(method, idx) in selectedMethods"
:key="'method-' + method.id"
class="selected-item-card method-child-card"
class="selected-item-card"
:class="{ 'is-expanded': method.expanded }"
>
<div
@@ -873,8 +873,12 @@
</div>
</template>
</div>
</div>
<!-- 检查方法勾选区:点击检查类型时出现在已选择框内 -->
</div>
</div>
<!-- 独立检查方法勾选区:与"已选择"区域解耦,支持分别手动勾选 -->
<div class="method-picker-section">
<div
v-if="methodsForActiveCategory.length > 0"
@@ -905,9 +909,6 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1264,12 +1265,7 @@ const activeCategory = computed(() => {
const activeCategoryName = computed(() => activeCategory.value?.typeName || activeCategory.value?.categoryName || '');
const methodsForActiveCategory = computed(() => {
// Bug #550修复: 直接从 categoryList 查找,避免 activeCategory 中间 computed 缓存阻断响应式
const id = activeNames.value;
if (id === '' || id === null || id === undefined) return [];
const cat = categoryList.value.find(c => String(c.typeId) === String(id));
if (!cat) return [];
const arr = cat.methods;
const arr = activeCategory.value?.methods;
return Array.isArray(arr) ? arr : [];
});
@@ -1948,11 +1944,6 @@ async function handleItemSelect(checked, item, cat) {
console.error('加载检查方法失败', err);
}
// Bug #550修复: 同步方法到分类,确保右侧方法选择器可见
if (methods.length > 0 && cat && (!cat.methods || cat.methods.length === 0)) {
cat.methods = methods;
}
if (selectedItems.value.length > 0) {
const currentCategory = selectedItems.value[0].checkType;
// Bug #428修复: 使用 cat.typeName 进行比较(与 effectiveCheckType 保持一致)
@@ -2468,24 +2459,28 @@ defineExpose({ getList });
flex-direction: column;
gap: 8px;
flex-shrink: 0;
width: 280px;
min-width: 260px;
}
.method-picker-section {
width: 260px;
min-width: 240px;
max-width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
margin-top: 8px;
}
.selected-panel {
width: 260px;
min-width: 240px;
max-width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.selected-panel {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.selected-tags {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: flex;
@@ -2493,27 +2488,6 @@ defineExpose({ getList });
gap: 8px;
padding-right: 2px;
}
/* 已选择面板中项目/方法区域分隔 */
.section-divider {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0 2px;
}
.section-divider::before {
content: '';
flex: 1;
height: 1px;
background: #dcdfe6;
}
.divider-label {
font-size: 11px;
font-weight: 600;
color: #909399;
letter-spacing: 0.03em;
flex-shrink: 0;
}
.selected-tag {
max-width: 100%;
overflow: hidden;
@@ -2536,13 +2510,6 @@ defineExpose({ getList });
overflow: hidden;
}
/* 方法卡片:子级缩进,表示从属于检查项目 */
.selected-item-card.method-child-card {
margin-left: 20px;
border-left: 3px solid #e6a23c;
border-radius: 0 6px 6px 0;
}
/* 项目上 / 方法下:各自独立下拉条 */
.fold-strip {
border-bottom: 1px solid var(--el-border-color-lighter);

View File

@@ -211,37 +211,14 @@ const handleRowClick = (row) => {
// 写病历
const handleWriteEmr = (row) => {
console.log('写病历:', row)
// 弹出写病历弹窗
ElMessageBox.confirm('确定要为患者 ' + row.patientName + ' 写病历吗?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
// 这里可以跳转到病历编辑页面或弹出病历编辑弹窗
ElMessage.success('正在打开病历编辑页面...')
// TODO: 实现写病历的具体逻辑
// 例如router.push({ path: '/doctorstation/emr', query: { encounterId: row.encounterId } })
}).catch(() => {
// 取消操作
})
// 这里可以触发写病历事件
// 可能需要跳转到病历编辑页面
}
// 查看患者
const handleViewPatient = (row) => {
console.log('查看患者:', row)
// 弹出查看患者弹窗
ElMessageBox.confirm('确定要查看患者 ' + row.patientName + ' 的详细信息吗?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
// 这里可以跳转到患者详情页面或弹出患者详情弹窗
ElMessage.success('正在打开患者详情页面...')
// TODO: 实现查看患者的具体逻辑
// 例如router.push({ path: '/doctorstation/patient-details', query: { encounterId: row.encounterId } })
}).catch(() => {
// 取消操作
})
// 这里可以触发查看患者事件
}
// 获取性别文本

View File

@@ -130,13 +130,7 @@
width="140"
>
<template #default="scope">
<el-tooltip
:content="buildFullName(scope.row)"
placement="top"
:disabled="!scope.row.requestFormDetailList || scope.row.requestFormDetailList.length <= 1"
>
<span>{{ buildApplicationName(scope.row) }}</span>
</el-tooltip>
<span>{{ buildApplicationName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column
@@ -645,8 +639,8 @@ const parseSpecimenType = (descJson) => {
if (!descJson) return '-';
try {
const obj = JSON.parse(descJson);
// 优先取标签字段(新格式),其次取码值字段,兼容旧数据 sampleType
return obj.specimenNameLabel || obj.specimenName || obj.sampleType || '-';
// specimenName 或 sampleType 字段
return obj.specimenName || obj.sampleType || '-';
} catch (e) {
console.error('解析 descJson 失败:', e);
return '-';
@@ -655,8 +649,8 @@ const parseSpecimenType = (descJson) => {
/**
* 根据申请单详情构建申请单名称
* 单一项目:直接显示项目全名(不拼接数量
* 多个项目:显示"项目1 + 项目2 等n项"缩略格式
* 单一项目:显示项目名称+数量
* 多个项目:显示首个项目名称+数量+"等X项"
*/
const buildApplicationName = (row) => {
const details = row.requestFormDetailList;
@@ -664,24 +658,11 @@ const buildApplicationName = (row) => {
return row.name || '-';
}
if (details.length === 1) {
// 单一项目:直接显示项目全名
return details[0].adviceName || row.name || '-';
const item = details[0];
return `${item.adviceName}${item.quantity || ''}`;
}
// 多个项目:首项 + 第二项 + 等n项
const names = details.map((d) => d.adviceName).filter(Boolean);
if (names.length === 0) return row.name || '-';
const first = names[0];
const second = names.length > 1 ? ` + ${names[1]}` : '';
return `${first}${second}${details.length}`;
};
/**
* 获取申请单完整项目名称列表(用于 tooltip 展示)
*/
const buildFullName = (row) => {
const details = row.requestFormDetailList;
if (!details || details.length === 0) return row.name || '-';
return details.map((d) => d.adviceName).filter(Boolean).join(' + ') || row.name || '-';
const first = details[0];
return `${first.adviceName}${first.quantity || ''}${details.length}`;
};
const isFieldMatched = (key) => {

View File

@@ -1,7 +1,7 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isUpdateMode ? '修改中医诊断' : '添加中医诊断'"
v-model="props.openAddDiagnosisDialog"
title="添加中医诊断"
width="1500px"
append-to-body
destroy-on-close
@@ -51,7 +51,7 @@
<div class="search-box">
<el-input
v-model="searchMiddleDisease"
placeholder="搜索证候名称或编码"
placeholder="搜索疾病名称或编码"
clearable
>
<template #prefix>
@@ -131,8 +131,8 @@
</template>
<script setup>
import { getTcmCondition, getTcmSyndrome, saveTcmDiagnosis, updateTcmDiagnosis, getTcmDiagnosis } from '@/views/doctorstation/components/api';
import { computed } from 'vue';
import {getTcmCondition, getTcmSyndrome, saveTcmDiagnosis,} from '@/views/doctorstation/components/api';
import {computed} from 'vue';
const props = defineProps({
openAddDiagnosisDialog: {
@@ -143,17 +143,13 @@ const props = defineProps({
type: Object,
required: true,
},
updateZy: {
type: Array,
default: () => [],
},
});
const conditionList = ref([]);
const syndromeList = ref([]);
const tcmDiagonsisList = ref([]);
const tcmDiagonsisSaveList = ref([]);
const syndromeSelected = ref(false);
const syndromeSelected = ref(false); // 当前诊断是否选择对应证候
const timestamp = ref('');
const selectedDisease = ref(false);
const searchDisease = ref('');
@@ -161,70 +157,35 @@ const searchMiddleDisease = ref('');
const { proxy } = getCurrentInstance();
const emit = defineEmits(['close']);
const dialogVisible = computed({
get: () => props.openAddDiagnosisDialog,
set: (val) => {
if (!val) {
emit('close');
}
},
});
const isUpdateMode = computed(() => {
return props.updateZy && props.updateZy.length > 0;
});
function handleOpen() {
getTcmCondition().then((res) => {
conditionList.value = res.data.records;
});
tcmDiagonsisSaveList.value = [];
tcmDiagonsisList.value = [];
syndromeSelected.value = true;
if (isUpdateMode.value) {
props.updateZy.forEach((item) => {
let updateIds = item.updateId ? item.updateId.split('-') : [];
let nameParts = item.name ? item.name.split('-') : [item.name || ''];
tcmDiagonsisSaveList.value.push({
conditionId: updateIds[0] || '',
definitionId: item.illnessDefinitionId || item.definitionId || '',
ybNo: item.ybNo,
syndromeGroupNo: item.syndromeGroupNo,
verificationStatusEnum: item.verificationStatusEnum || 4,
medTypeCode: item.medTypeCode,
});
tcmDiagonsisList.value.push({
conditionName: nameParts[0] || '',
syndromeName: nameParts[1] || '',
syndromeGroupNo: item.syndromeGroupNo,
illnessDefinitionId: item.illnessDefinitionId,
});
});
}
}
// 搜索诊断
const conditionDatas = computed(() => {
if (!searchDisease.value) {
return conditionList.value;
}
return conditionList.value.filter((item) => {
return item.name.includes(searchDisease.value) || item.ybNo.includes(searchDisease.value);
if (searchDisease.value) {
return searchDisease.value == item.name || searchDisease.value == item.ybNo;
}
return conditionList;
});
});
// 后证
const syndromeListDatas = computed(() => {
if (!searchMiddleDisease.value) {
return syndromeList.value;
}
return syndromeList.value.filter((item) => {
return item.name.includes(searchMiddleDisease.value) || item.ybNo.includes(searchMiddleDisease.value);
if (searchMiddleDisease.value) {
return searchMiddleDisease.value == item.name || searchMiddleDisease.value == item.ybNo;
}
return syndromeList;
});
});
// 点击诊断列表处理,点击以后才显示证候列表
function handleClickRow(row) {
if (syndromeSelected.value || tcmDiagonsisList.value.length === 0) {
if (syndromeSelected.value || tcmDiagonsisList.value == 0) {
selectedDisease.value = true;
syndromeSelected.value = false;
timestamp.value = Date.now();
@@ -236,7 +197,7 @@ function handleClickRow(row) {
ybNo: row.ybNo,
syndromeGroupNo: timestamp.value,
verificationStatusEnum: 4,
medTypeCode: undefined,
medTypeCode: undefined, // 不设默认值
});
tcmDiagonsisList.value.push({
conditionName: row.name,
@@ -255,6 +216,7 @@ function clickSyndromeRow(row) {
syndromeSelected.value = true;
}
// 删除诊断
function removeDiagnosis(row, index) {
tcmDiagonsisList.value.splice(index, 1);
tcmDiagonsisSaveList.value = tcmDiagonsisSaveList.value.filter((item) => {
@@ -263,67 +225,77 @@ function removeDiagnosis(row, index) {
}
function save() {
const newDiagnosisList = tcmDiagonsisSaveList.value.filter((item) => !item.conditionId);
if (isUpdateMode.value) {
updateTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: tcmDiagonsisSaveList.value,
}).then((res) => {
if (res.code == 200) {
if (newDiagnosisList.length > 0) {
saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: newDiagnosisList,
}).then((res2) => {
if (res2.code == 200) {
emit('close');
proxy.$modal.msgSuccess('诊断已保存');
}
});
} else {
emit('close');
proxy.$modal.msgSuccess('诊断已保存');
}
}
});
} else {
saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: tcmDiagonsisSaveList.value,
}).then((res) => {
if (res.code == 200) {
emit('close');
proxy.$modal.msgSuccess('诊断已保存');
}
});
}
saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: tcmDiagonsisSaveList.value,
}).then((res) => {
if (res.code == 200) {
emit('close');
proxy.$modal.msgSuccess('诊断已保存');
}
});
}
function submit() {
const hasNewDiagnosis = tcmDiagonsisSaveList.value.some((item) => !item.conditionId);
if (!hasNewDiagnosis && isUpdateMode.value) {
emit('close');
return;
}
if (syndromeSelected.value || tcmDiagonsisSaveList.value.length % 2 === 0) {
if (tcmDiagonsisSaveList.value.length > 0 && syndromeSelected.value) {
save();
} else {
proxy.$modal.msgWarning('请选择证候');
}
}
function close() {
emit('close');
}
</script>
<style scoped>
:deep(.pagination-container .el-pagination) {
right: 20px !important;
}
.app-container {
max-width: 1400px;
margin: 20px auto;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid var(--el-border-color);
margin-bottom: 20px;
}
.header h1 {
color: var(--el-color-primary);
font-size: 24px;
font-weight: 600;
}
.patient-info {
background: var(--el-color-primary-light-9);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.patient-info .info-row {
display: flex;
margin-bottom: 8px;
}
.patient-info .info-label {
width: 100px;
color: var(--el-text-color-secondary);
font-weight: 500;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr 1.2fr;
@@ -350,13 +322,125 @@ function close() {
border-bottom: 1px solid var(--el-border-color);
}
.disease-list {
max-height: 400px;
overflow-y: auto;
}
.disease-item {
padding: 12px 15px;
border-bottom: 1px solid var(--el-border-color-lighter);
cursor: pointer;
transition: all 0.3s;
border-radius: 4px;
}
.disease-item:hover {
background-color: var(--el-color-primary-light-9);
}
.disease-item.active {
background-color: var(--el-color-primary-light-8);
border-left: 3px solid var(--el-color-primary);
}
.disease-name {
font-weight: 500;
margin-bottom: 5px;
}
.disease-code {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.search-box {
margin-bottom: 15px;
}
.diagnosis-list {
max-height: 520px;
overflow-y: auto;
.disease-categories {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.category-tag {
cursor: pointer;
padding: 5px 12px;
border-radius: 15px;
background: var(--el-fill-color-light);
font-size: 13px;
transition: all 0.3s;
}
.category-tag.active {
background: var(--el-color-primary);
color: white;
}
.relation-container {
text-align: center;
padding: 30px 0;
border: 2px dashed var(--el-border-color);
border-radius: 8px;
margin: 20px 0;
background: var(--el-fill-color-lighter);
}
.relation-icon {
margin-bottom: 15px;
color: var(--el-color-primary);
}
.relation-text {
font-size: 18px;
font-weight: 500;
color: var(--el-text-color-primary);
}
.syndrome-details {
padding: 15px;
background: var(--el-color-primary-light-9);
border-radius: 8px;
border: 1px solid var(--el-color-primary-light-5);
}
.detail-item {
margin-bottom: 12px;
}
.detail-label {
font-weight: 500;
color: var(--el-text-color-secondary);
margin-bottom: 3px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 15px;
padding-top: 20px;
border-top: 1px solid var(--el-border-color);
}
.empty-state {
text-align: center;
padding: 40px 0;
color: var(--el-text-color-secondary);
}
.diagnosis-history {
margin-top: 20px;
border-top: 1px solid var(--el-border-color);
padding-top: 20px;
}
.history-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;
color: var(--el-text-color-primary);
}
.history-item {
@@ -367,6 +451,17 @@ function close() {
border-radius: 0 4px 4px 0;
}
.diagnosis-list {
max-height: 520px;
overflow-y: auto;
}
.history-date {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 5px;
}
.history-diagnosis {
display: flex;
justify-content: space-between;
@@ -374,9 +469,16 @@ function close() {
margin-bottom: 5px;
}
.empty-state {
text-align: center;
padding: 40px 0;
.history-note {
font-size: 13px;
color: var(--el-text-color-secondary);
padding-top: 5px;
border-top: 1px dashed var(--el-border-color);
margin-top: 5px;
}
.empty-list {
padding: 20px 0;
text-align: center;
}
</style>

View File

@@ -1,60 +1,14 @@
<template>
<el-dialog
v-model="visible"
title="中医诊断"
top="6vh"
:width="width"
title="中医诊断"
:z-index="20"
append-to-body
destroy-on-close
@open="openAct"
@closed="closedAct"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item
label="中医诊断"
prop="conditionCode"
>
<el-select
v-model="formData.conditionCode"
placeholder="请选择中医诊断"
filterable
clearable
style="width: 100%"
@change="handleConditionChange"
>
<el-option
v-for="item in conditionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
label="中医证候"
prop="syndromeCode"
>
<el-select
v-model="formData.syndromeCode"
placeholder="请选择中医证候"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="item in syndromeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
中医诊断
<template #footer>
<el-button
size="fixed"
@@ -66,120 +20,122 @@
<el-button
size="fixed"
type="primary"
@click="handleSubmit"
@click="handleSubmit(signFormRef)"
>
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup>
<script setup>
import {onMounted, reactive, ref} from 'vue'
import { ElMessage } from 'element-plus'
import { getTcmCondition, getTcmSyndrome, saveTcmDiagnosis } from '../api'
import {dayjs} from 'element-plus'
// import { IInPatient } from '@/model/IInPatient'
const { proxy } = getCurrentInstance()
const currentInPatient = ref({})
const initCurrentInPatient = () => {
currentInPatient.value = {
feeType: '08',
sexName: '男',
age: '0',
}
}
/* 初始化数据 */
const init = () => {
initCurrentInPatient()
}
const conditionOptions = ref([])
const syndromeOptions = ref([])
const formData = ref({
conditionCode: '',
syndromeCode: '',
/* 入科 */
const signForm = ref({
visitCode: '', // 就诊流水号
height: 0, // 身高
weight: 0, // 体重
temperature: 0, // 体温
hertRate: 0, // 心率
pulse: 0, // 脉搏
highBloodPressure: 0, // 收缩压
endBloodPressure: 0, // 舒张压
loginDeptCode: '', // 当前登录科室
bingqing: '', //患者病情
inDeptDate: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'), //入院时间
signsId: '',
})
const rules = reactive({
conditionCode: [{ required: true, message: '请选择中医诊断', trigger: ['blur', 'change'] }],
syndromeCode: [{ required: true, message: '请选择中医证候', trigger: ['blur', 'change'] }],
admittedDoctor: [{ required: true, message: '请选择住院医生', trigger: ['blur', 'change'] }],
masterNurse: [{ required: true, message: '请选择责任护士', trigger: ['blur', 'change'] }],
})
const printWristband = ref(false)
const emits = defineEmits(['okAct'])
const props = defineProps({
patientInfo: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['ok-act'])
const visible = defineModel<boolean>('visible')
const width = '500px'
const visible = defineModel('visible')
const width = '920px'
/* 取消 */
const cancelAct = () => {
visible.value = false
}
function handleConditionChange() {
formData.value.syndromeCode = ''
loadSyndromeOptions(formData.value.conditionCode)
}
function loadConditionOptions() {
getTcmCondition().then((res) => {
if (res.data && res.data.records) {
conditionOptions.value = res.data.records.map((item) => ({
value: item.ybNo,
label: item.name,
}))
}
})
}
function loadSyndromeOptions(conditionCode) {
const params = conditionCode ? { conditionCode } : {}
getTcmSyndrome(params).then((res) => {
if (res.data && res.data.records) {
syndromeOptions.value = res.data.records.map((item) => ({
value: item.ybNo,
label: item.name,
}))
}
})
}
const formRef = ref()
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
/* 录入患者体征*/
const signFormRef = ref()
const handleSubmit = async (formEl) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const submitData = {
conditionCode: formData.value.conditionCode,
syndromeCode: formData.value.syndromeCode,
console.log('submit!')
try {
// 录入患者体征方法(signForm.value).then((res: any) => {
// ElMessage({
// message: '登记成功!',
// type: 'success',
// grouping: true,
// showClose: true,
// })
// emits('okAct')
// })
} catch (error) {
console.log(error)
}
if (props.patientInfo && props.patientInfo.patientId) {
submitData.patientId = props.patientInfo.patientId
submitData.encounterId = props.patientInfo.encounterId
}
submitData.diagnosisChildList = [{
conditionCode: formData.value.conditionCode,
syndromeCode: formData.value.syndromeCode,
}]
saveTcmDiagnosis(submitData).then((res) => {
if (res.code === 200) {
ElMessage.success('中医诊断保存成功')
emit('ok-act')
cancelAct()
} else {
ElMessage.error(res.msg || '保存失败')
}
}).catch(() => {
ElMessage.error('保存失败,请重试')
})
}
})
}
const openAct = () => {
formData.value = { conditionCode: '', syndromeCode: '' }
loadConditionOptions()
loadSyndromeOptions()
init()
}
const closedAct = () => {
visible.value = false
}
onMounted(() => {
loadConditionOptions()
})
onMounted(() => {})
</script>
<style lang="scss" scoped>
.transferIn-container {
width: 100%;
.admission-signs,
.admission-information {
width: 888px;
.unit {
display: inline-block;
margin-left: 10px;
color: #bbb;
font-weight: 400;
font-size: 14px;
font-family: '思源黑体 CN';
}
}
}
.print-wriBtn {
margin-left: 565px;
}
.w-p100 {
width: 100%;
}
.w-80 {
width: 80px;
}
.mb-90 {
margin-bottom: 90px !important;
}
</style>

View File

@@ -142,34 +142,6 @@
</el-form-item>
</template>
</el-table-column>
<el-table-column
label="诊断体系"
align="center"
prop="diagnosisSystem"
width="120"
>
<template #default="scope">
<el-form-item
:prop="`diagnosisList.${scope.$index}.diagnosisSystem`"
>
<el-select
v-model="scope.row.diagnosisSystem"
placeholder=" "
style="width: 100%"
@change="handleDiagnosisSystemChange(scope.row)"
>
<el-option
label="西医"
value="西医"
/>
<el-option
label="中医"
value="中医"
/>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column
label="诊断类别"
align="center"
@@ -187,7 +159,7 @@
style="width: 150px"
>
<el-option
v-for="item in diag_type"
v-for="item in med_type"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -215,7 +187,6 @@
>
<diagnosislist
:diagnosis-searchkey="diagnosisSearchkey"
:diagnosis-system="scope.row.diagnosisSystem || '西医'"
@select-diagnosis="handleSelsectDiagnosis"
/>
<template #reference>
@@ -237,74 +208,19 @@
align="center"
prop="diagnosisDoctor"
width="120"
>
<template #default="scope">
<el-form-item>
<span style="display: block; text-align: center; width: 100%;">{{ scope.row.diagnosisDoctor || '—' }}</span>
</el-form-item>
</template>
</el-table-column>
<el-table-column
align="center"
prop="tcmSyndromeName"
width="180"
>
<template #header>
<span>中医证候 <span style="color: #f56c6c;">*</span></span>
</template>
<template #default="scope">
<template v-if="scope.row.diagnosisSystem === '中医'">
<el-form-item
:prop="`diagnosisList.${scope.$index}.tcmSyndromeCode`"
:rules="scope.row.diagnosisSystem === '中医' ? [{ required: true, message: '请选择中医证候', trigger: 'change' }] : []"
>
<el-select
v-model="scope.row.tcmSyndromeCode"
placeholder="请选择中医证候"
filterable
clearable
style="width: 100%"
@focus="loadSyndromeOptions(scope.row.ybNo)"
@change="(val) => handleSyndromeSelect(val, scope.row)"
>
<el-option
v-for="item in syndromeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</template>
<el-form-item v-else>
<span>—</span>
</el-form-item>
</template>
</el-table-column>
/>
<el-table-column
label="诊断时间"
align="center"
prop="diagnosisTime"
width="150"
>
<template #default="scope">
<el-form-item>
<span style="display: block; text-align: center; width: 100%;">{{ scope.row.diagnosisTime || '—' }}</span>
</el-form-item>
</template>
</el-table-column>
/>
<el-table-column
label="诊断代码"
align="center"
prop="ybNo"
width="180"
>
<template #default="scope">
<el-form-item>
<span style="display: block; text-align: center; width: 100%;">{{ scope.row.ybNo || '—' }}</span>
</el-form-item>
</template>
</el-table-column>
/>
<el-table-column
label="诊断类型"
align="center"
@@ -312,32 +228,30 @@
width="120"
>
<template #default="scope">
<el-form-item>
<div style="display:flex;flex-direction:column;align-items:center;gap:5px;">
<el-checkbox
v-model="scope.row.maindiseFlag"
label="主诊断"
:true-label="1"
:false-label="0"
border
size="small"
@change="(value) => handleMaindise(value, scope.$index)"
<div style="display:flex;flex-direction:column;align-items:center;gap:5px;">
<el-checkbox
v-model="scope.row.maindiseFlag"
label="主诊断"
:true-label="1"
:false-label="0"
border
size="small"
@change="(value) => handleMaindise(value, scope.$index)"
/>
<el-select
v-model="scope.row.verificationStatusEnum"
placeholder=" "
style="width: 100%; padding-bottom: 5px; padding-left: 10px"
size="small"
>
<el-option
v-for="item in diagnosisOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<el-select
v-model="scope.row.verificationStatusEnum"
placeholder=" "
style="width: 100%; padding-bottom: 5px; padding-left: 10px"
size="small"
>
<el-option
v-for="item in diagnosisOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</el-form-item>
</el-select>
</div>
</template>
</el-table-column>
<el-table-column
@@ -346,15 +260,13 @@
width="130"
>
<template #default="scope">
<el-form-item>
<el-button
link
type="primary"
@click="handleDeleteDiagnosis(scope.row, scope.$index)"
>
删除
</el-button>
</el-form-item>
<el-button
link
type="primary"
@click="handleDeleteDiagnosis(scope.row, scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
@@ -369,14 +281,13 @@
<AddDiagnosisDialog
:open-add-diagnosis-dialog="openAddDiagnosisDialog"
:patient-info="props.patientInfo"
:update-zy="tcmDiagnosisListForEdit"
@close="closeDiagnosisDialog"
/>
</div>
</template>
<script setup>
import {getCurrentInstance, ref, watch} from 'vue'; // 添加 nextTick 导入
import {getCurrentInstance} from 'vue'; // 添加 nextTick 导入
import useUserStore from '@/store/modules/user';
import {
delEncounterDiagnosis,
@@ -387,10 +298,8 @@ import {
getEmrDetail,
getEncounterDiagnosis,
getTcmDiagnosis,
getTcmSyndrome,
isFoodDiseasesNew,
saveDiagnosis,
saveTcmDiagnosis,
} from '../api';
import {deleteTcmDiagnosis} from '@/views/doctorstation/components/api.js';
import diagnosisdialog from '../diagnosis/diagnosisdialog.vue';
@@ -409,7 +318,6 @@ const diagnosisOptions = ref([]);
const rowIndex = ref();
const diagnosis = ref();
const orgOrUser = ref();
const syndromeOptions = ref([]);
const form = ref({
diagnosisList: [],
});
@@ -423,15 +331,13 @@ const props = defineProps({
const emits = defineEmits(['diagnosisSave']);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
// 获取诊断类型字典(住院诊断类别)
const { diag_type } = proxy.useDict('diag_type');
const { med_type } = proxy.useDict('med_type');
const rules = ref({
name: [{ required: true, message: '请选择诊断', trigger: 'change' }],
medTypeCode: [{ required: true, message: '请选择诊断类型', trigger: 'change' }],
diagSrtNo: [{ required: true, message: '请输入诊断序号', trigger: 'change' }],
});
const diagnosisNetDatas = ref([]);
const tcmDiagnosisListForEdit = ref([]);
watch(
() => form.value.diagnosisList,
@@ -488,68 +394,46 @@ function getList() {
return;
}
// 先加载西医诊断,再加载中医诊断(避免竞态覆盖)
// 初始化中医诊断列表
const newList = [];
getEncounterDiagnosis(props.patientInfo.encounterId).then((res) => {
if (res.code == 200) {
// 过滤掉中医诊断typeName: '中医诊断'),中医数据由 getTcmDiagnosis 统一管理
const datas = (res.data || [])
.filter((item) => item.typeName !== '中医诊断')
.map((item) => {
let obj = {
...item,
diagnosisSystem: '西医',
tcmSyndromeCode: '',
tcmSyndromeName: '',
syndromeDefinitionId: '',
syndromeGroupNo: '',
showPopover: false,
};
if (obj.diagSrtNo == null) {
obj.diagSrtNo = 1;
}
return obj;
});
const datas = (res.data || []).map((item) => {
let obj = {
...item,
};
if (obj.diagSrtNo == null) {
obj.diagSrtNo = 1;
}
return obj;
});
form.value.diagnosisList = datas;
// form.value.diagnosisList = res.data;
emits('diagnosisSave', false);
}
});
getTcmDiagnosis({ encounterId: props.patientInfo.encounterId }).then((res) => {
console.log('getTcmDiagnosis=======>', JSON.stringify(res.data.illness));
// 西医数据就绪后再加载中医诊断并追加
getTcmDiagnosis({ encounterId: props.patientInfo.encounterId }).then((res) => {
console.log('getTcmDiagnosis=======>', JSON.stringify(res.data.illness));
if (res.code == 200) {
if (res.data.illness.length > 0) {
diagnosisNetDatas.value = res.data.illness;
const newList = [];
res.data.illness.forEach((item, index) => {
newList.push({
conditionId: item.conditionId || '',
encounterDiagnosisId: item.encounterDiagnosisId || '',
syndromeGroupNo: item.syndromeGroupNo || res.data.symptom[index]?.syndromeGroupNo || '',
name: item.name + '-' + (res.data.symptom[index]?.name || ''),
ybNo: item.ybNo,
definitionId: item.definitionId || '',
diagnosisSystem: '中医',
tcmSyndromeCode: res.data.symptom[index]?.ybNo || '',
tcmSyndromeName: res.data.symptom[index]?.name || '',
syndromeDefinitionId: res.data.symptom[index]?.definitionId || '',
diagSrtNo: item.diagSrtNo,
medTypeCode: item.medTypeCode,
maindiseFlag: item.maindiseFlag,
verificationStatusEnum: item.verificationStatusEnum,
diagnosisDesc: item.diagnosisDesc || '',
iptDiseTypeCode: item.iptDiseTypeCode,
showPopover: false,
diagnosisDoctor: item.diagnosisDoctor || props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: item.diagnosisTime || new Date().toLocaleString('zh-CN')
});
if (res.code == 200) {
if (res.data.illness.length > 0) {
diagnosisNetDatas.value = res.data.illness;
res.data.illness.forEach((item, index) => {
newList.push({
name: item.name + '-' + (res.data.symptom[index]?.name || ''),
ybNo: item.ybNo,
medTypeCode: item.medTypeCode,
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: new Date().toLocaleString('zh-CN')
});
// 将新数据添加到现有列表现有列表
form.value.diagnosisList.push(...newList);
// 重新排序整个列表
form.value.diagnosisList.sort((a, b) => {
});
// 将新数据添加到现有列表中
form.value.diagnosisList.push(...newList);
// 重新排序整个列表
form.value.diagnosisList.sort((a, b) => {
const aNo = typeof a.diagSrtNo === 'number' ? a.diagSrtNo : 9999;
const bNo = typeof b.diagSrtNo === 'number' ? b.diagSrtNo : 9999;
return aNo - bNo;
@@ -558,9 +442,7 @@ function getList() {
emits('diagnosisSave', false);
}
});
});
getTree();
}
@@ -711,11 +593,6 @@ function addDiagnosisItem() {
form.value.diagnosisList.push({
showPopover: false,
name: undefined,
diagnosisSystem: '西医',
tcmSyndromeCode: '',
tcmSyndromeName: '',
syndromeDefinitionId: '',
syndromeGroupNo: '',
verificationStatusEnum: 4,
medTypeCode: undefined,
diagSrtNo: form.value.diagnosisList.length + 1,
@@ -733,57 +610,8 @@ function addDiagnosisItem() {
}
}
// 诊断体系切换
function handleDiagnosisSystemChange(row) {
if (row.diagnosisSystem === '西医') {
row.tcmSyndromeCode = '';
row.tcmSyndromeName = '';
row.syndromeDefinitionId = '';
row.syndromeGroupNo = '';
}
row.name = '';
row.ybNo = '';
row.definitionId = '';
row.showPopover = false;
}
// 加载中医证候选项(按诊断名称关联过滤)
function loadSyndromeOptions(conditionCode) {
const params = conditionCode ? { conditionCode } : {};
getTcmSyndrome(params).then((res) => {
if (res.data && res.data.records) {
syndromeOptions.value = res.data.records.map((item) => ({
value: item.ybNo,
label: item.name,
id: item.id,
}));
} else {
syndromeOptions.value = [];
}
});
}
// 中医证候选中赋值
function handleSyndromeSelect(val, row) {
if (val) {
const selected = syndromeOptions.value.find((item) => item.value === val);
row.tcmSyndromeName = selected ? selected.label : '';
row.syndromeDefinitionId = selected ? selected.id : '';
} else {
row.tcmSyndromeName = '';
row.syndromeDefinitionId = '';
}
}
// 添加中医诊断
function handleAddTcmDiagonsis() {
tcmDiagnosisListForEdit.value = form.value.diagnosisList.filter(
(item) => item.diagnosisSystem === '中医'
).map((item) => ({
...item,
updateId: item.conditionId ? `${item.conditionId}-${item.syndromeGroupNo || ''}` : '' ,
illnessDefinitionId: item.definitionId || '' ,
}));
openAddDiagnosisDialog.value = true;
}
@@ -794,27 +622,40 @@ function handleAddTcmDiagonsis() {
* 删除诊断
*/
function handleDeleteDiagnosis(row, index) {
// 新行(未保存):直接从列表中移除
if (!row.conditionId && !row.encounterDiagnosisId) {
form.value.diagnosisList.splice(index, 1);
return;
}
// 已保存的中医诊断name含'-'且syndromeGroupNo有值
if (row.syndromeGroupNo) {
deleteTcmDiagnosis(row.syndromeGroupNo).then(() => {
getList();
getTree();
});
return;
}
// 已保存的西医诊断
//中医诊断用-拼接 例如:疳气-表里俱实证
const nameArr = row.name?.split('-') || [];
if (row.conditionId) {
delEncounterDiagnosis(row.conditionId).then(() => {
getList();
getTree();
});
if (nameArr.length > 1) {
deleteTcmDiagnosis(row.syndromeGroupNo).then(() => {
getList();
getTree();
});
} else {
delEncounterDiagnosis(row.conditionId).then(() => {
getList();
getTree();
});
}
} else {
console.log('row============>', JSON.stringify(row));
console.log('item============>', index);
if (nameArr.length > 1) {
let obj = null;
for (let index = 0; index < diagnosisNetDatas.value.length; index++) {
const item = diagnosisNetDatas.value[index];
console.log('item.name============>', item.name);
console.log('row.name============>', row.name);
if (item.ybNo == row.ybNo) {
obj = item;
}
}
deleteTcmDiagnosis(obj.syndromeGroupNo).then(() => {
getList();
getTree();
});
} else {
form.value.diagnosisList.splice(index, 1);
}
}
}
@@ -872,19 +713,6 @@ function handleSaveDiagnosis() {
return;
}
// 校验中医诊断证候完整性
for (let i = 0; i < form.value.diagnosisList.length; i++) {
const item = form.value.diagnosisList[i];
if (!item.name) {
ElMessage.warning(`第${i + 1}行诊断名称不能为空`);
return;
}
if (item.diagnosisSystem === '中医' && !item.tcmSyndromeCode) {
ElMessage.error('中医诊断不完整,请录入对应的证候!');
return;
}
}
// 设置保存标志避免触发watch监听器
isSaving.value = true;
@@ -897,78 +725,29 @@ function handleSaveDiagnosis() {
// 步骤2重新分配连续的序号从1开始
sortedList.forEach((item, index) => {
item.diagSrtNo = index + 1;
item.diagSrtNo = index + 1; // 这里是关键!把”诊断排序”改成新顺序
});
// 步骤3拆分为西医诊断和中医诊断
const westernList = sortedList.filter((item) => item.diagnosisSystem !== '中医');
const tcmList = sortedList.filter((item) => item.diagnosisSystem === '中医');
// 步骤3提交排序后的数据
saveDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: sortedList,
}).then((res) => {
if (res.code === 200) {
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
const savePromises = [];
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 保存西医诊断
if (westernList.length > 0) {
savePromises.push(
saveDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: westernList,
})
);
}
// 保存中医诊断
tcmList.forEach((item) => {
const syndromeGroupNo = item.conditionId
? `${item.conditionId}-${item.tcmSyndromeCode || Date.now()}`
: `${Date.now()}-${item.tcmSyndromeCode || '0'}`;
savePromises.push(
saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: [
// 病illness
{
conditionId: item.conditionId || null,
name: item.name,
ybNo: item.ybNo,
definitionId: item.definitionId || null,
diagSrtNo: item.diagSrtNo,
medTypeCode: item.medTypeCode,
maindiseFlag: item.maindiseFlag,
verificationStatusEnum: item.verificationStatusEnum,
diagnosisDesc: item.diagnosisDesc || '',
diagnosisDoctor: item.diagnosisDoctor || '',
diagnosisTime: item.diagnosisTime || '',
iptDiseTypeCode: item.iptDiseTypeCode,
syndromeGroupNo: syndromeGroupNo,
},
// 证syndrome
{
name: item.tcmSyndromeName,
ybNo: item.tcmSyndromeCode,
definitionId: item.syndromeDefinitionId || null,
diagSrtNo: null,
syndromeGroupNo: syndromeGroupNo,
},
],
})
);
});
Promise.all(savePromises).then(() => {
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) {
window.open(res2.data, '_blank');
}
});
}
}).finally(() => {
setTimeout(() => {
isSaving.value = false;

View File

@@ -28,55 +28,38 @@
</template>
<script setup>
import {getDiagnosisDefinitionList, getTcmCondition} from '../api';
import {getDiagnosisDefinitionList} from '../api';
const props = defineProps({
diagnosisSearchkey: {
type: String,
default: '',
},
diagnosisSystem: {
type: String,
default: '西医',
},
});
const emit = defineEmits(['selectDiagnosis']);
const total = ref(0);
const queryParams = ref({
pageSize: 1000,
pageNo: 1,
// typeCode: 1,
});
const diagnosisDefinitionList = ref([]);
watch(
() => [props.diagnosisSearchkey, props.diagnosisSystem],
() => {
() => props.diagnosisSearchkey,
(newValue) => {
queryParams.value.searchKey = newValue;
getList();
},
{ immediate: true }
{ immdiate: true }
);
getList();
function getList() {
if (props.diagnosisSystem === '中医') {
getTcmCondition({ searchKey: props.diagnosisSearchkey || undefined }).then((res) => {
if (res.data && res.data.records) {
diagnosisDefinitionList.value = res.data.records.map((item) => ({
name: item.name,
ybNo: item.ybNo,
typeName: '中医诊断',
id: item.id,
}));
}
total.value = res.data?.total || 0;
});
} else {
queryParams.value.searchKey = props.diagnosisSearchkey || undefined;
getDiagnosisDefinitionList(queryParams.value).then((res) => {
diagnosisDefinitionList.value = res.data.records;
total.value = res.data.total;
});
}
getDiagnosisDefinitionList(queryParams.value).then((res) => {
diagnosisDefinitionList.value = res.data.records;
total.value = res.data.total;
});
}
function clickRow(row) {

View File

@@ -1,6 +1,6 @@
<template>
<div class="diagnose-container">
<!-- 常用诊断个人诊断科室诊断历史诊断 -->
<!-- 常用诊断个人诊断科室诊断历史诊断 -->
<diagnose-folder
:folder="mockData"
:level="0"
@@ -10,44 +10,29 @@
<el-space>
<el-button
type="primary"
@click="addNewDiagnosis"
@click="addNewWestern"
>
新增诊断
开立诊断
</el-button>
<el-button type="primary">
既往诊断
</el-button>
<!-- 患者诊断 -->
<el-button
type="primary"
type="danger"
@click="addNewChinese"
>
中医诊断
</el-button>
<el-button
type="danger"
:disabled="!selectedRows.length"
@click="handleDelete"
>
删除诊断
</el-button>
<el-button
type="primary"
:loading="saveLoading"
@click="handleSaveDiagnosis"
>
保存诊断
</el-button>
</el-space>
</div>
<div class="diagnoseData-container">
<el-table
ref="diagnoseTableRef"
:data="diagnoseData"
border
row-key="id"
style="width: 100%; height: 100%"
highlight-current-row
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
@@ -55,531 +40,166 @@
width="40"
/>
<el-table-column
label="序号"
type="index"
width="50"
fixed="left"
prop="date"
label="诊断类型"
width="180"
sortable
/>
<el-table-column
label="诊断体系"
prop="diagnosisSystem"
width="120"
>
<template #default="scope">
<el-select
v-model="scope.row.diagnosisSystem"
placeholder=" "
style="width: 100%"
@change="handleDiagnosisSystemChange(scope.row)"
>
<el-option
label="西医"
value="西医"
/>
<el-option
label="中医"
value="中医"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="诊断类别"
prop="classification"
width="120"
>
<template #default="scope">
<el-select
v-model="scope.row.classification"
placeholder=" "
style="width: 100%"
>
<el-option
v-for="item in diag_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="诊断名称"
prop="name"
label="诊断名称"
width="180"
>
<template #default="scope">
<div
class="diagnosis-text"
@click="handleDiagnosisNameClick(scope.row, scope.$index)"
>
<span class="diagnosis-text-content">{{ scope.row.name || '点击选择诊断' }}</span>
<el-icon class="diagnosis-text-icon">
<arrow-down />
</el-icon>
</div>
<el-popover
v-if="scope.row.showPopover"
placement="bottom"
:width="400"
trigger="manual"
>
<template #reference>
<span />
</template>
<div class="diagnosis-popover-container">
<div class="diagnosis-popover-header">
<span class="diagnosis-popover-title">选择诊断</span>
<el-link
type="primary"
class="diagnosis-popover-close"
@click="closeDiagnosisPopover(scope.row)"
>
关闭
</el-link>
</div>
<div class="diagnosis-popover-body">
<diagnosislist
:diagnosis-searchkey="diagnosisSearchkey"
@select-diagnosis="(row) => handleSelectDiagnosis(row, scope.row, scope.$index)"
/>
</div>
</div>
</el-popover>
</template>
</el-table-column>
/>
<el-table-column
label="中医证候"
prop="tcmSyndromeName"
width="180"
>
<template #default="scope">
<template v-if="scope.row.diagnosisSystem === '中医'">
<div
class="diagnosis-text"
@click="handleTcmSyndromeClick(scope.row, scope.$index)"
>
<span class="diagnosis-text-content">{{ scope.row.tcmSyndromeName || '请选择中医证候' }}</span>
<el-icon class="diagnosis-text-icon">
<arrow-down />
</el-icon>
</div>
<el-popover
v-if="scope.row.showSyndromePopover"
placement="bottom"
:width="400"
trigger="manual"
>
<template #reference>
<span />
</template>
<div class="diagnosis-popover-container">
<div class="diagnosis-popover-header">
<span class="diagnosis-popover-title">选择中医证候</span>
<el-link
type="primary"
class="diagnosis-popover-close"
@click="closeSyndromePopover(scope.row)"
>
关闭
</el-link>
</div>
<div class="diagnosis-popover-body">
<el-input
v-model="syndromeSearchkey"
placeholder="搜索证候名称"
clearable
style="margin-bottom: 8px"
@input="handleSyndromeSearch"
/>
<el-table
:data="filteredSyndromeList"
highlight-current-row
max-height="300"
@row-click="(row) => handleSelectSyndrome(row, scope.row)"
>
<el-table-column
label="证候名称"
prop="name"
align="center"
/>
<el-table-column
label="医保编码"
prop="ybNo"
align="center"
/>
</el-table>
</div>
</div>
</el-popover>
</template>
<span v-else>—</span>
</template>
</el-table-column>
prop="address"
label="主诊"
/>
<el-table-column
prop="address"
label="复诊"
/>
<el-table-column
prop="address"
label="疑似"
/>
<el-table-column
prop="address"
label="传染"
/>
<el-table-column
prop="address"
label="入院病情"
prop="admissionCondition"
width="120"
width="180"
/>
<el-table-column
prop="address"
label="转归"
prop="outcome"
width="120"
width="180"
/>
<el-table-column
prop="address"
label="转归日期"
prop="outcomeDate"
width="140"
width="180"
/>
<el-table-column
prop="address"
label="诊断科室"
prop="deptName"
width="140"
width="180"
/>
<el-table-column
prop="address"
label="诊断医师"
prop="diagnosisDoctor"
width="140"
width="180"
/>
<el-table-column
prop="address"
label="诊断日期"
prop="diagnosisTime"
width="140"
width="180"
/>
<el-table-column
fixed="right"
label="操作"
width="120"
>
<template #default="scope">
<template #default="props">
<el-space>
<el-tooltip
content="删除"
placement="bottom"
>
<el-icon @click="deleteRow(scope.row, scope.$index)">
<el-icon @click="deleteDiagnose(row)">
<Delete />
</el-icon>
</el-tooltip>
<el-tooltip
v-if="scope.$index !== diagnoseData.length - 1"
v-if="props.$index !== diagnoseData.length - 1"
content="下移"
placement="bottom"
>
<el-icon @click="moveDown(scope.row, scope.$index)">
<el-icon @click="download(props.row)">
<Download />
</el-icon>
</el-tooltip>
<el-tooltip
v-if="scope.$index !== 0"
v-if="props.$index !== 0"
content="上移"
placement="bottom"
>
<el-icon @click="moveUp(scope.row, scope.$index)">
<el-icon @click="upload(props.row)">
<Upload />
</el-icon>
</el-tooltip>
<el-tooltip
v-if="props.$index !== 0"
content="置顶"
placement="bottom"
>
<el-icon @click="top(props.row)">
<Top />
</el-icon>
</el-tooltip>
<el-tooltip
v-if="props.$index !== diagnoseData.length - 1"
content="置底"
placement="bottom"
>
<el-icon @click="bottom(props.row)">
<Bottom />
</el-icon>
</el-tooltip>
</el-space>
</template>
</el-table-column>
</el-table>
</div>
</div>
<WesternMedicineDialog v-model:visible="westernMedicineDialogVisible" />
<ChineseMedicineDialog
v-model:visible="chineseMedicineDialogVisible"
:patient-info="patientInfo"
@ok-act="loadDiagnosisData"
/>
<WesternMedicineDialog v-model:visible="WesternMedicineDialogVisible" />
<ChineseMedicineDialog v-model:visible="ChineseMedicineDialogVisible" />
</div>
</template>
<script setup>
import {onMounted, reactive, ref, computed} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowDown, Delete, Download, Upload } from '@element-plus/icons-vue'
import {onBeforeMount, onMounted, reactive, ref} from 'vue'
// const { proxy } = getCurrentInstance()
// const emits = defineEmits([])
// const props = defineProps({})
// import DiagnoseFolder from './diagnoseFolder.vue'
import WesternMedicineDialog from './westernMedicineDialog.vue'
import ChineseMedicineDialog from './chineseMedicineDialog.vue'
import Diagnosislist from './diagnosislist.vue'
import {
saveDiagnosis,
delEncounterDiagnosis,
getEncounterDiagnosis,
getTcmSyndrome,
} from '../api'
const { proxy } = getCurrentInstance()
const props = defineProps({
patientInfo: {
type: Object,
default: () => ({}),
const diagnoseData = ref([
{
id: 1,
sort: 1,
name: '新冠',
},
})
{
id: 2,
sort: 2,
name: '新冠as',
},
{
id: 3,
sort: 3,
name: '新冠12',
},
{
id: 4,
sort: 4,
name: '新冠2121',
},
{
id: 5,
sort: 5,
name: '新冠12',
},
{
id: 6,
sort: 6,
name: '新冠21',
},
])
const diagnoseData = ref([])
const selectedRows = ref([])
const saveLoading = ref(false)
const diagnoseTableRef = ref()
const diagnosisSearchkey = ref('')
const syndromeSearchkey = ref('')
const syndromeList = ref([])
// 获取诊断类型字典(住院诊断类别)
const { diag_type } = proxy.useDict('diag_type')
const filteredSyndromeList = computed(() => {
if (!syndromeSearchkey.value) {
return syndromeList.value
}
const keyword = syndromeSearchkey.value.toLowerCase()
return syndromeList.value.filter(item =>
(item.name && item.name.toLowerCase().includes(keyword)) ||
(item.ybNo && item.ybNo.toLowerCase().includes(keyword))
)
})
function getCurrentDate() {
const date = new Date()
const year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
month = month < 10 ? '0' + month : month
day = day < 10 ? '0' + day : day
return `${year}-${month}-${day}`
}
function addNewDiagnosis() {
const maxSortNo = diagnoseData.value.length > 0
? Math.max(...diagnoseData.value.map(item => item.sortNo || 0))
: 0
diagnoseData.value.push({
id: Date.now(),
sortNo: maxSortNo + 1,
diagnosisSystem: '西医',
classification: '主诊断',
name: '',
ybNo: '',
definitionId: '',
tcmSyndromeCode: '',
tcmSyndromeName: '',
admissionCondition: '',
outcome: '',
outcomeDate: '',
deptName: '',
diagnosisDoctor: proxy.$store?.state?.user?.name || '',
diagnosisTime: getCurrentDate(),
showPopover: false,
showSyndromePopover: false,
isNew: true,
})
}
function addNewChinese() {
chineseMedicineDialogVisible.value = true
}
function handleDiagnosisSystemChange(row) {
if (row.diagnosisSystem === '西医') {
row.tcmSyndromeCode = ''
row.tcmSyndromeName = ''
}
row.name = ''
row.ybNo = ''
row.showPopover = false
row.showSyndromePopover = false
}
function handleDiagnosisNameClick(row, index) {
if (row.diagnosisSystem === '中医') {
row.showPopover = false
return
}
diagnoseData.value.forEach((item, idx) => {
if (idx !== index) {
item.showPopover = false
}
})
row.showPopover = true
}
function handleSelectDiagnosis(diagRow, rowData) {
rowData.name = diagRow.name
rowData.ybNo = diagRow.ybNo
rowData.definitionId = diagRow.id
rowData.showPopover = false
}
function closeDiagnosisPopover(row) {
row.showPopover = false
}
function handleTcmSyndromeClick(row, index) {
diagnoseData.value.forEach((item, idx) => {
if (idx !== index) {
item.showSyndromePopover = false
}
})
loadSyndromeList()
row.showSyndromePopover = true
}
function handleSyndromeSearch() {}
function loadSyndromeList() {
getTcmSyndrome().then((res) => {
if (res.data && res.data.records) {
syndromeList.value = res.data.records
}
})
}
function handleSelectSyndrome(syndromeRow, rowData) {
rowData.tcmSyndromeCode = syndromeRow.ybNo
rowData.tcmSyndromeName = syndromeRow.name
rowData.showSyndromePopover = false
}
function closeSyndromePopover(row) {
row.showSyndromePopover = false
}
function handleSelectionChange(rows) {
selectedRows.value = rows
}
function deleteRow(row, index) {
diagnoseData.value.splice(index, 1)
}
function moveDown(row, index) {
if (index >= diagnoseData.value.length - 1) return
const temp = diagnoseData.value[index]
diagnoseData.value[index] = diagnoseData.value[index + 1]
diagnoseData.value[index + 1] = temp
diagnoseData.value = [...diagnoseData.value]
}
function moveUp(row, index) {
if (index <= 0) return
const temp = diagnoseData.value[index]
diagnoseData.value[index] = diagnoseData.value[index - 1]
diagnoseData.value[index - 1] = temp
diagnoseData.value = [...diagnoseData.value]
}
function handleDelete() {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择要删除的诊断')
return
}
ElMessageBox.confirm('确定删除选中的诊断吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
const deleteIds = selectedRows.value
.filter(item => item.conditionId)
.map(item => item.conditionId)
const newRows = selectedRows.value.filter(item => !item.conditionId)
newRows.forEach(item => {
const idx = diagnoseData.value.findIndex(d => d.id === item.id)
if (idx > -1) {
diagnoseData.value.splice(idx, 1)
}
})
deleteIds.forEach(id => {
delEncounterDiagnosis(id).then(() => {
const idx = diagnoseData.value.findIndex(d => d.conditionId === id)
if (idx > -1) {
diagnoseData.value.splice(idx, 1)
}
})
})
selectedRows.value = []
ElMessage.success('删除成功')
}).catch(() => {})
}
async function handleSaveDiagnosis() {
if (!diagnoseData.value.length) {
ElMessage.warning('没有需要保存的诊断')
return
}
for (let i = 0; i < diagnoseData.value.length; i++) {
const item = diagnoseData.value[i]
if (!item.name) {
ElMessage.warning(`第${i + 1}行诊断名称不能为空`)
return
}
if (item.diagnosisSystem === '中医' && !item.tcmSyndromeCode) {
ElMessage.error('中医诊断不完整,请录入对应的证候!')
return
}
}
saveLoading.value = true
try {
const diagnosisList = diagnoseData.value.map((item, index) => ({
conditionId: item.conditionId || '',
ybNo: item.ybNo || '',
name: item.name,
definitionId: item.definitionId || '',
classification: item.classification || '主诊断',
diagnosisSystem: item.diagnosisSystem || '西医',
tcmSyndromeCode: item.tcmSyndromeCode || '',
tcmSyndromeName: item.tcmSyndromeName || '',
admissionCondition: item.admissionCondition || '',
outcome: item.outcome || '',
outcomeDate: item.outcomeDate || '',
diagnosisDoctor: item.diagnosisDoctor || '',
diagnosisTime: item.diagnosisTime || getCurrentDate(),
diagSrtNo: index + 1,
}))
const saveData = {
patientId: props.patientInfo?.patientId || '',
encounterId: props.patientInfo?.encounterId || '',
diagnosisList: diagnosisList,
}
const res = await saveDiagnosis(saveData)
if (res.code === 200) {
ElMessage.success('诊断保存成功')
loadDiagnosisData()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (error) {
ElMessage.error('保存失败,请重试')
} finally {
saveLoading.value = false
}
}
function loadDiagnosisData() {
if (!props.patientInfo?.encounterId) return
getEncounterDiagnosis(props.patientInfo.encounterId).then((res) => {
if (res.data) {
const westernDiagnoses = (res.data || []).filter(item => item.typeName !== '中医诊断')
diagnoseData.value = westernDiagnoses.map((item, index) => ({
...item,
diagnosisSystem: item.diagnosisSystem || '西医',
classification: item.classification || '主诊断',
tcmSyndromeCode: item.tcmSyndromeCode || '',
tcmSyndromeName: item.tcmSyndromeName || '',
showPopover: false,
showSyndromePopover: false,
diagSrtNo: index + 1,
}))
}
})
}
// 模拟数据(常用/科室/个人/历史诊断树)
// 模拟数据
const mockData = ref([
{
name: '常用',
@@ -587,18 +207,28 @@ const mockData = ref([
{
name: '文件夹 1',
children: [
{ name: '霍乱' },
{ name: '新型冠状病毒' },
{
name: '霍乱',
},
{
name: '新型冠状病毒新型冠状病毒新型冠状病毒',
},
],
},
{
name: '文件夹 2',
children: [
{ name: '普外科' },
{ name: '骨科' },
{
name: '普外科',
},
{
name: '骨科',
},
],
},
{ name: '新型冠状病毒' },
{
name: '新型冠状病毒',
},
],
},
{
@@ -607,18 +237,28 @@ const mockData = ref([
{
name: '内科',
children: [
{ name: '呼吸内科' },
{ name: '消化内科' },
{
name: '呼吸内科',
},
{
name: '消化内科',
},
],
},
{
name: '外科',
children: [
{ name: '普外科' },
{ name: '骨科' },
{
name: '普外科',
},
{
name: '骨科',
},
],
},
{ name: '儿科' },
{
name: '儿科',
},
],
},
{
@@ -627,27 +267,79 @@ const mockData = ref([
{
name: '内科',
children: [
{ name: '呼吸内科' },
{ name: '消化内科' },
{
name: '呼吸内科',
},
{
name: '消化内科',
},
],
},
{
name: '外科',
children: [
{
name: '普外科',
},
{
name: '骨科',
},
],
},
{
name: '儿科',
},
],
},
{
name: '历史',
children: [
{ name: '心率失常' },
{
name: '心率失常',
},
{
name: '心率失常',
},
{
name: '心率失常',
},
],
},
])
const state = reactive({})
onMounted(() => {
if (props.patientInfo?.encounterId) {
loadDiagnosisData()
}
})
defineExpose({ state, loadDiagnosisData })
onBeforeMount(() => {})
onMounted(() => {})
defineExpose({ state })
// const deleteDiagnose = (row: any) => {
// // TODO 删除
// console.log(row)
// }
// const download = (row: any) => {
// // TODO 删除
// }
// const upload = (row: any) => {
// // TODO 删除
// }
// const top = (row: any) => {
// // TODO 删除
// }
// const bottom = (row: any) => {
// // TODO 删除
// }
const addNewWestern = () => {
WesternMedicineDialogVisible.value = true
}
const addNewChinese = () => {
ChineseMedicineDialogVisible.value = true
}
const WesternMedicineDialogVisible = ref(false)
const ChineseMedicineDialogVisible = ref(false)
</script>
<style lang="scss" scoped>
.diagnose-container {
@@ -673,79 +365,4 @@ defineExpose({ state, loadDiagnosisData })
}
}
}
.diagnosis-text {
min-height: 32px;
line-height: 1.4;
padding: 6px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
text-align: left;
word-break: break-all;
white-space: pre-wrap;
max-width: 200px;
transition: border-color 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.diagnosis-text:hover {
border-color: #409eff;
}
.diagnosis-text-content {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.diagnosis-text-icon {
color: #909399;
font-size: 12px;
}
.diagnosis-text:hover .diagnosis-text-icon {
color: #409eff;
}
.diagnosis-popover-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.diagnosis-popover-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
background-color: #f5f7fa;
}
.diagnosis-popover-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.diagnosis-popover-close {
font-size: 12px;
}
.diagnosis-popover-body {
padding: 0;
}
.diagnosis-text:empty::before {
content: '点击选择诊断';
color: #a8abb2;
}
</style>

View File

@@ -200,9 +200,6 @@
/>
</el-select>
</el-form-item>
<el-form-item label="备注:" prop="remark">
<el-input v-model="row.remark" maxlength="50" placeholder="最多50字" style="width: 200px" />
</el-form-item>
</div>
</div>
<div
@@ -297,6 +294,9 @@
</el-select>
</template>
</div>
<el-form-item label="备注:" prop="remark">
<el-input v-model="row.remark" maxlength="50" placeholder="最多50字" style="width: 200px" />
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSave">确定</el-button>
<el-button @click="handleCancel">取消</el-button>

View File

@@ -147,6 +147,8 @@ onBeforeMount(() => {});
onMounted(() => {});
const applicationFormNameRef = ref();
const submitApplicationForm = () => {
console.log(applicationFormNameRef.value);
if (applicationFormNameRef.value?.submit) {
applicationFormNameRef.value.submit();
}

View File

@@ -31,10 +31,9 @@
<span class="total-count"> {{ totalCount }} </span>
</div>
<el-transfer
:model-value="transferState.selected"
v-model="transferValue"
:data="transferData"
:titles="['未选择', '已选择']"
@change="onTransferChange"
/>
</div>
<div class="bloodTransfusion-form">
@@ -183,10 +182,44 @@
style="width: 100%"
>
<el-option
v-for="item in specimenDictOptions"
:key="item.value"
:label="item.label"
:value="item.value"
label="血液"
value="血液"
/>
<el-option
label="尿液"
value="尿液"
/>
<el-option
label="粪便"
value="粪便"
/>
<el-option
label="痰液"
value="痰液"
/>
<el-option
label="咽拭子"
value="咽拭子"
/>
<el-option
label="脑脊液"
value="脑脊液"
/>
<el-option
label="胸腹水"
value="胸腹水"
/>
<el-option
label="关节液"
value="关节液"
/>
<el-option
label="分泌物"
value="分泌物"
/>
<el-option
label="其他"
value="其他"
/>
</el-select>
</el-form-item>
@@ -214,12 +247,11 @@
</div>
</template>
<script setup name="LaboratoryTests">
import {getCurrentInstance, nextTick, onMounted, reactive, ref, watch, watchEffect, computed} from 'vue';
import {getCurrentInstance, nextTick, onMounted, reactive, ref, watch, computed} from 'vue';
import {patientInfo} from '../../../store/patient.js';
import {getExaminationPage, saveInspection} from './api';
import {ActivityCategory} from '@/utils/medicalConstants';
import {getDepartmentList} from '@/api/public.js';
import {getDicts} from '@/api/system/dict/data';
import {getEncounterDiagnosis} from '../../api.js';
import {ElMessage} from 'element-plus';
@@ -252,10 +284,6 @@ const searchKey = ref('');
const totalCount = ref(0);
const skipDeptAutoFill = ref(false);
// 标本类型字典
const specimenDictMap = ref({}); // dictValue → dictLabel 映射
const specimenDictOptions = ref([]); // 下拉选项列表
// 将已加载的全部数据转为 transfer 组件所需的格式
const buildTransferData = (records) => {
return records.map((item) => {
@@ -319,28 +347,7 @@ const handleSearch = () => {
};
// 编辑初始化标志:避免 applyEditTransferSelection 设置 transferValue 时触发 projectWithDepartment 覆盖 descJson 中的科室值
const isInitializing = ref(false);
// el-transfer v-model 在某些环境下不触发响应式更新,
// 使用 reactive 数组 + @change 事件双重保障
const transferState = reactive({ selected: [] });
const transferValue = ref([]);
// 单向同步:程序设置 transferValue如编辑回显→ transferState.selected
watch(transferValue, (val) => {
if (JSON.stringify(val) !== JSON.stringify(transferState.selected)) {
transferState.selected = [...val];
}
}, { deep: true });
// 格式化当前时间为 yyyy-MM-dd HH:mm:ss
const formatCurrentDateTime = () => {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const min = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
};
const form = reactive({
// categoryType: '', // 项目类别
targetDepartment: '', // 发往科室
@@ -351,30 +358,12 @@ const form = reactive({
relatedResult: '', // 相关结果
attention: '', // 注意事项
applicationType: 0, // 申请类型 0-普通 1-急诊
specimenName: '', // 标本类型(由选择诊疗项目后自动带出)
executeTime: formatCurrentDateTime(), // 执行时间,默认当前系统时间
specimenName: '血液', // 标本类型
executeTime: null, // 执行时间
primaryDiagnosisList: [], //主诊断目录
otherDiagnosisList: [], //其他断目录
});
const validateExecuteTime = (rule, value, callback) => {
if (!value) {
callback(new Error('请选择执行时间'));
return;
}
const now = new Date();
const selectedTime = new Date(value);
if (selectedTime < now) {
callback(new Error('执行时间不可早于当前时间'));
return;
}
callback();
};
const rules = reactive({
executeTime: [
{ required: true, message: '请选择执行时间', trigger: 'change' },
{ validator: validateExecuteTime, trigger: 'change' },
],
});
const rules = reactive({});
const normalizeOrgTreeIds = (nodes) => {
if (!Array.isArray(nodes)) return [];
@@ -397,30 +386,10 @@ const applyTargetDepartmentEcho = () => {
}
};
/** 加载所需标本字典dictType = specimen_code */
const loadSpecimenDict = async () => {
try {
const res = await getDicts('specimen_code');
if (res?.code === 200 && Array.isArray(res.data)) {
const map = {};
const options = [];
res.data.forEach((item) => {
map[item.dictValue] = item.dictLabel;
options.push({ value: item.dictValue, label: item.dictLabel });
});
specimenDictMap.value = map;
specimenDictOptions.value = options;
}
} catch (e) {
console.warn('加载所需标本字典失败:', e);
}
};
onMounted(() => {
getLocationInfo();
getDiagnosisList();
getList();
loadSpecimenDict();
});
/**
* type(1watch监听类型 2:点击保存类型)
@@ -500,29 +469,12 @@ const projectWithDepartment = (selectProjectIds, type) => {
}
return isRelease;
};
/** el-transfer @change: 穿梭框数据变化(右侧完整列表) */
const onTransferChange = (newRightValue) => {
if (!Array.isArray(newRightValue)) return;
transferState.selected = [...newRightValue];
newRightValue.forEach((id) => {
if (!selectedItemsCache.value.has(id)) {
const item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
if (item) selectedItemsCache.value.set(id, item);
}
});
if (!isInitializing.value && !skipDeptAutoFill.value) {
projectWithDepartment(newRightValue, 1);
autoFillSpecimenType(newRightValue);
}
};
// 监听 transferValue 变化el-select multiple v-model 可靠)
// 监听选择项目变化
watch(
() => transferValue.value,
(newValue) => {
console.log('[标本联动] watch 触发, transferValue:', newValue);
if (skipDeptAutoFill.value || isInitializing.value) return;
if (!newValue || newValue.length === 0) return;
if (skipDeptAutoFill.value) return;
if (isInitializing.value) return;
newValue.forEach((id) => {
if (!selectedItemsCache.value.has(id)) {
const item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
@@ -530,45 +482,9 @@ watch(
}
});
projectWithDepartment(newValue, 1);
autoFillSpecimenType(newValue);
}
);
/** 根据选中的检验项目自动带出标本类型(直接使用字典码,与 el-option :value 对齐) */
const autoFillSpecimenType = (selectedIds) => {
if (!selectedIds || selectedIds.length === 0) return;
const specimens = [];
selectedIds.forEach((id) => {
let item = selectedItemsCache.value.get(id);
if (!item) {
item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
}
if (item?.specimenCode) {
const code = String(item.specimenCode);
if (code && !specimens.includes(code)) {
specimens.push(code);
}
}
});
// 所有选中项目标本类型一致时才自动填充
if (specimens.length === 1) {
form.specimenName = specimens[0];
}
};
/**
* 从可能的历史数据中解析标本字典码
* 旧数据可能存储的是显示名称(如 "血液"),需转换为字典码(如 "1"
*/
const resolveSpecimenCode = (value) => {
if (!value) return '';
if (specimenDictMap.value[value]) return value; // 已经是有效的字典码
for (const opt of specimenDictOptions.value) {
if (opt.label === value) return String(opt.value); // 标签 → 码值
}
return String(value); // 兜底:直接转字符串
};
/** 编辑弹窗:根据申请单明细把右侧「已选择」与 transferValue 对齐(依赖 applicationListAll 已加载) */
const applyEditTransferSelection = () => {
const newData = props.editData
@@ -609,10 +525,6 @@ const applyEditTransferSelection = () => {
skipDeptAutoFill.value = false
})
isInitializing.value = false
// 编辑初始化后显式填充标本类型watch 被 skipDeptAutoFill 守卫跳过了)
if (uniq.length > 0) {
autoFillSpecimenType(uniq)
}
if (newData.requestFormDetailList.length && uniq.length === 0) {
console.warn(
'[LaboratoryTests] 申请单明细未能在项目字典中匹配到项,请核对 activityId / 项目名称',
@@ -632,12 +544,7 @@ watch(
const obj = JSON.parse(newData.descJson)
Object.keys(form).forEach((key) => {
if (obj[key] !== undefined) {
// 标本类型:兼容旧数据中存的是显示名称(如 "血液"),转为字典码(如 "1"
if (key === 'specimenName') {
form[key] = resolveSpecimenCode(obj[key])
} else {
form[key] = obj[key]
}
form[key] = obj[key]
}
})
applyTargetDepartmentEcho()
@@ -684,17 +591,13 @@ watch(
);
const submit = () => {
// 使用 transferState.selected来自 el-transfer @change兜底 transferValue
const selected = transferState.selected.length > 0 ? transferState.selected : transferValue.value;
if (selected.length == 0) {
if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单');
}
// 提交前自动带出标本类型(必须在 projectWithDepartment 之前,否则科室校验失败直接 return 不会执行)
autoFillSpecimenType(selected);
if (!projectWithDepartment(selected, 2)) {
if (!projectWithDepartment(transferValue.value, 2)) {
return;
}
let applicationListAllFilter = selected.map((id) => {
let applicationListAllFilter = transferValue.value.map((id) => {
let item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
if (!item) {
item = selectedItemsCache.value.get(id);
@@ -724,11 +627,7 @@ const submit = () => {
organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID
requestFormId: isEditMode.value ? props.editData.requestFormId : '', // 申请单ID编辑模式传入新增为空
name: '检验申请单',
descJson: JSON.stringify({
...form,
// 标本类型显示名称(供申请单查看页使用,避免查看页依赖字典加载)
specimenNameLabel: specimenDictMap.value[form.specimenName] || form.specimenName,
}),
descJson: JSON.stringify(form),
categoryEnum: '21', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
};
saveInspection(params).then((res) => {
@@ -782,14 +681,7 @@ function getDiagnosisList() {
}
});
}
// 暴露给父组件:允许父组件在调用 submit 前读取选中项并执行联动
const getSelectedItems = () => transferValue.value;
const getSpecimenMap = () => specimenDictMap.value;
const getApplicationListAll = () => applicationListAll.value;
const fillSpecimenBySelected = () => { autoFillSpecimenType(transferValue.value); };
defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList,
getSelectedItems, getSpecimenMap, getApplicationListAll, fillSpecimenBySelected });
defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList });
</script>
<style lang="scss" scoped>
.LaboratoryTests-container {

View File

@@ -287,7 +287,7 @@
<el-table-column label="药房/科室" align="center" prop="" width="240">
<template #default="scope">
<span v-if="!scope.row.isEdit">
{{ scope.row.positionName || scope.row.orgName }}
{{ scope.row.positionName }}
</span>
</template>
</el-table-column>
@@ -342,13 +342,6 @@
</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" width="150" show-overflow-tooltip>
<template #default="scope">
<span v-if="!scope.row.isEdit">
{{ scope.row.remark || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="诊断" align="center" prop="diagnosisName" width="150">
<template #default="scope">
<span v-if="!scope.row.isEdit">

View File

@@ -2,14 +2,14 @@
<div style="height: calc(100vh - 126px)">
<div
style="
min-height: 51px;
height: 51px;
border-bottom: 2px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<div style="display: flex; align-items: center; flex-wrap: nowrap; flex-shrink: 0">
<div>
<span class="descriptions-item-label">截止时间</span>
<el-date-picker
v-model="deadline"
@@ -22,7 +22,6 @@
<el-radio-group
v-model="therapyEnum"
class="ml20"
style="flex-shrink: 0"
@change="handleRadioChange"
>
<el-radio :value="undefined">
@@ -37,7 +36,6 @@
</el-radio-group>
<el-button
class="ml20"
style="flex-shrink: 0"
type="primary"
plain
@click="handleGetPrescription"
@@ -45,7 +43,7 @@
查询
</el-button>
</div>
<div style="display: flex; align-items: center; flex-wrap: nowrap; flex-shrink: 0">
<div>
<span class="descriptions-item-label">实际执行时间</span>
<el-date-picker
v-model="exeDate"
@@ -58,12 +56,10 @@
<span class="descriptions-item-label">全选</span>
<el-switch
v-model="chooseAll"
style="flex-shrink: 0"
@change="handelSwicthChange"
/>
<el-button
class="ml20"
style="flex-shrink: 0"
type="primary"
:disabled="props.exeStatus == 6"
@click="handleExecute"
@@ -72,7 +68,6 @@
</el-button>
<el-button
class="ml20"
style="flex-shrink: 0"
type="primary"
:disabled="props.exeStatus == 6"
@click="handleNoExecute"
@@ -80,8 +75,7 @@
不执行
</el-button>
<el-button
class="ml20"
style="flex-shrink: 0"
class="ml20 mr20"
type="danger"
:disabled="props.exeStatus != 6"
plain

View File

@@ -111,7 +111,6 @@ function handleClick(tabName) {
break;
case 'cancel':
exeStatus.value = 9;
requestStatus.value = RequestStatus.CANCELLED;
break;
}

View File

@@ -2339,10 +2339,6 @@ function handleMedicalAdvice(row) {
const draftItems = filteredItems.filter(item => item.statusEnum === 1)
const activeItems = filteredItems.filter(item => item.statusEnum === 2)
if (activeItems.length > 0) {
temporarySigned.value = true
}
// 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死
const maxItems = 100
if (draftItems.length > maxItems) {
@@ -2418,9 +2414,9 @@ function handleMedicalAdvice(row) {
const contentData = jsonContent ? JSON.parse(jsonContent) : {};
const medicineName = contentData.adviceName || contentData.advice_name || item.adviceName || item.advice_name || '';
const spec = contentData.volume || contentData.specification || item.volume || item.specification || '';
const specMatch = spec.match(/([\d.]+)\s*([a-zA-Z一-龥]+)/)
const specValue = specMatch ? parseFloat(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : ''
const specMatch = spec.match(/(\d+)(\D+)/)
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
const dosage = specValue * (contentData.quantity || item.quantity || 1)
let usageCode = contentData.methodCode || 'iv'
@@ -2438,8 +2434,8 @@ function handleMedicalAdvice(row) {
unit: specUnit,
usage: usageCode,
usageLabel,
frequency: '立即',
executeTime: '',
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: medicineName,
@@ -2453,8 +2449,8 @@ function handleMedicalAdvice(row) {
id: index + 1,
adviceName: item.adviceName || item.advice_name || '',
dosage: 1, unit: 'ml', usage: 'iv', usageLabel: '静脉注射',
frequency: '立即',
executeTime: '',
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: item.adviceName || item.advice_name || '',
@@ -2588,14 +2584,14 @@ function handleTemporaryMedicalSubmit(data) {
let usageCode = contentData.methodCode || 'iv'
return {
id: index + 1, adviceName: medicineName, dosage, unit: specUnit,
usage: usageCode, frequency: '立即',
executeTime: '',
usage: usageCode, frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: { ...item, medicineName, specification: spec, quantity: contentData.quantity || item.quantity || 1, encounterId: row.visitId }
}
} catch (e) {
return {
id: index + 1, adviceName: item.adviceName || '', dosage: 1, unit: 'ml',
usage: 'iv', frequency: '立即', executeTime: '',
usage: 'iv', frequency: '临时', executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: { ...item, medicineName: item.adviceName || '', specification: item.volume || '', quantity: item.quantity || 1, encounterId: row.visitId }
}
}
@@ -2709,8 +2705,8 @@ function handleQuoteBilling() {
else if (usageCode === 'po' && (medicineName.includes('片') || medicineName.includes('胶囊'))) { usageLabel = '口服' }
return {
id: index + 1, adviceName: medicineName, dosage, unit: specUnit,
usage: usageCode, usageLabel, frequency: '立即',
executeTime: '',
usage: usageCode, usageLabel, frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: medicineName,
@@ -2723,7 +2719,7 @@ function handleQuoteBilling() {
return {
id: index + 1, adviceName: item.adviceName || item.advice_name || '',
dosage: 1, unit: 'ml', usage: 'iv', usageLabel: '静脉注射',
frequency: '立即', executeTime: '',
frequency: '临时', executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: item.adviceName || item.advice_name || '',

View File

@@ -173,8 +173,6 @@
border
style="width: 100%;"
fit
highlight-current-row
@row-click="handleAdviceRowClick"
>
<el-table-column
label="序号"
@@ -259,7 +257,7 @@
<span
class="info-value"
:class="{ 'unsigned': !isSigned }"
>{{ isSigned ? signatureDoctor : '未签名' }}</span>
>{{ isSigned ? currentUser.name : '未签名' }}</span>
</div>
<div class="signature-info">
<span class="info-label">签名时间</span>
@@ -400,7 +398,6 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import useUserStore from '@/store/modules/user'
import { checkPassword } from '@/api/surgicalschedule'
import { savePrescription } from '@/views/clinicmanagement/bargain/component/api.js'
import { parseTime } from '@/utils/openhis'
// 定义props
const props = defineProps({
@@ -510,11 +507,16 @@ const displayAdvicesList = computed(() => {
return advicesExpanded.value ? all : all.slice(0, PAGE_SIZE)
})
const isSigned = ref(false)
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)
// 🔧 修复 Bug #446: 同步父组件 isSignedProp 的变化到本地 isSigned
// ref(props.isSignedProp) 只在初始化时读取一次,父组件后续更新不会自动同步
watch(() => props.isSignedProp, (newVal) => {
isSigned.value = newVal
})
const signatureDoctor = ref(userStore.nickName || userStore.name || '未知用户')
const signatureTime = ref('')
const showSignDialog = ref(false)
const signPassword = ref('')
const showEditDialog = ref(false)
@@ -529,7 +531,7 @@ const editForm = ref({
// 计算属性
const currentUser = computed(() => ({
name: userStore.nickName || userStore.name || '未知用户',
name: userStore.name || '未知用户',
id: userStore.id
}))
@@ -552,10 +554,10 @@ const totalAmount = computed(() => {
// 将计费药品转换为临时医嘱数据
const convertedAdvices = computed(() => {
return props.billingMedicines.map((medicine, index) => {
// 解析规格中的数值和单位(支持小数,去除 ×、:、/、* 等多余字符)
const specMatch = medicine.specification ? medicine.specification.match(/([\d.]+)\s*([a-zA-Z一-龥]+)/) : null
const specValue = specMatch ? parseFloat(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : ''
// 解析规格中的数值和单位
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
// 计算剂量 = 规格数值 × 数量
const dosage = specValue * (medicine.quantity || 1)
@@ -581,8 +583,8 @@ const convertedAdvices = computed(() => {
unit: specUnit,
usage: usageCode, // 🔧 修复:使用后端字典的正确编码
usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '立即',
executeTime: '',
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: medicine
}
})
@@ -638,24 +640,6 @@ const handleSign = () => {
showSignDialog.value = true
}
// 点击已生成列表行 → 回显该行的签名信息
const handleAdviceRowClick = (row) => {
const om = row?.originalMedicine
if (!om) return
const contentJson = om.contentJson || om.content_json
if (!contentJson) return
try {
const cd = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson
if (cd.signDoctorName) {
signatureDoctor.value = cd.signDoctorName
}
if (cd.signDate) {
signatureTime.value = cd.signDate
}
isSigned.value = true
} catch (e) {}
}
// 编辑医嘱
const handleEditAdvice = (index) => {
const advice = displayAdvices.value[index]
@@ -678,7 +662,7 @@ const handleEditAdvice = (index) => {
}
// 保存编辑
const handleSaveEdit = async () => {
const handleSaveEdit = () => {
if (!editForm.value.dosage && editForm.value.dosage !== 0) {
ElMessage.warning('请填写剂量')
return
@@ -736,8 +720,8 @@ const handleSaveEdit = async () => {
// 如果用户修改了剂量,重新计算数量
if (originalMedicine.specification) {
const specMatch = originalMedicine.specification.match(/([\d.]+)\s*([a-zA-Z一-龥]+)/)
const specValue = specMatch ? parseFloat(specMatch[1]) : 1
const specMatch = originalMedicine.specification.match(/(\d+)(\D+)/)
const specValue = specMatch ? parseInt(specMatch[1]) : 1
if (specValue > 0) {
const newQuantity = editForm.value.dosage / specValue
contentData.quantity = newQuantity
@@ -747,9 +731,12 @@ const handleSaveEdit = async () => {
updatedAdvice.quantity = newQuantity
}
}
// 🔧 修复:原地修改 contentJson,保存使用最新数据
originalMedicine.contentJson = JSON.stringify(contentData)
// 更新 contentJson
updatedAdvice.originalMedicine = {
...originalMedicine,
contentJson: JSON.stringify(contentData)
}
} catch (e) {
console.error('解析 originalMedicine.contentJson 失败', e)
}
@@ -763,49 +750,7 @@ const handleSaveEdit = async () => {
emit('update:temporary-advices', updatedAdvices)
showEditDialog.value = false
// 🔧 修复 Bug #604: 编辑保存后直接提交到服务器
const editMedicine = updatedAdvice.originalMedicine
if (editMedicine) {
let contentJsonData = {}
try { contentJsonData = JSON.parse(editMedicine.contentJson || '{}') } catch (e) {}
const quantity = editMedicine.quantity || contentJsonData.quantity || 1
const unitPrice = editMedicine.unitPrice || contentJsonData.unitPrice || 0
contentJsonData.dose = editForm.value.dosage
contentJsonData.doseUnitCode = editForm.value.unit
contentJsonData.methodCode = updatedAdvice.usage
contentJsonData.quantity = quantity
contentJsonData.totalPrice = unitPrice * quantity
contentJsonData.adviceName = updatedAdvice.adviceName
const saveItem = {
...contentJsonData,
dbOpType: editMedicine.requestId ? '2' : '1',
adviceType: editMedicine.adviceType || 1,
requestId: editMedicine.requestId,
chargeItemId: editMedicine.chargeItemId,
contentJson: JSON.stringify(contentJsonData),
quantity,
unitCode: editMedicine.unitCode || editForm.value.unit,
unitPrice,
totalPrice: unitPrice * quantity,
adviceName: updatedAdvice.adviceName,
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.visitId,
orgId: props.patientInfo.orgId,
methodCode: updatedAdvice.usage,
dose: editForm.value.dosage,
doseUnitCode: editForm.value.unit,
generateSourceEnum: 6,
sourceBillNo: props.patientInfo?.operCode || ''
}
try {
await savePrescription({ organizationId: props.patientInfo.orgId || 1, adviceSaveList: [saveItem] }, '2')
ElMessage.success('医嘱修改已保存到服务器')
} catch (e) {
ElMessage.error('保存失败,请重试')
}
}
ElMessage.success('编辑成功(已暂存本地,请点击"一键签名并生成医嘱"按钮提交到服务器)')
}
// 取消编辑
@@ -817,7 +762,7 @@ const handleCancelEdit = () => {
dosage: '',
unit: '',
usage: '',
frequency: '立即'
frequency: '临时'
}
}
@@ -832,10 +777,10 @@ const confirmSign = async () => {
const response = await checkPassword({
password: signPassword.value
})
if (response.code === 200 && response.data) {
isSigned.value = true
signatureDoctor.value = userStore.nickName || userStore.name
signatureTime.value = parseTime(new Date())
signatureTime.value = new Date().toLocaleString('zh-CN')
showSignDialog.value = false
signPassword.value = ''
ElMessage.success('签名成功')
@@ -854,8 +799,10 @@ const confirmSign = async () => {
const handleSignAndSubmit = () => {
if (isSigned.value) {
// 如果已经签名,直接提交
handleSubmit()
} else {
// 如果未签名,打开签名弹窗
handleSign()
}
}
@@ -960,8 +907,6 @@ const handleSubmit = async () => {
contentJsonData.dose = advice.dosage;
contentJsonData.doseUnitCode = advice.unit;
contentJsonData.rateCode = advice.frequency;
contentJsonData.signDoctorName = signatureDoctor.value
contentJsonData.signDate = signatureTime.value
// 重新序列化contentJson
const updatedContentJson = JSON.stringify(contentJsonData);
@@ -1048,7 +993,7 @@ const handleSubmit = async () => {
billingMedicines: props.billingMedicines,
temporaryAdvices: itemsToSign,
signature: {
doctorName: signatureDoctor.value,
doctorName: currentUser.value.name,
signatureTime: signatureTime.value
}
}

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #466: Bug #466 待确认标题
* 自动生成: 2026-06-01 09:36:17
*/
test.describe('🐛 Bug#466', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#466 Bug #466 待确认标题 @bug466 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-466-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #467: Bug #467 待确认标题
* 自动生成: 2026-06-01 09:36:17
*/
test.describe('🐛 Bug#467', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#467 Bug #467 待确认标题 @bug467 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-467-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #610: Bug #610 待确认标题
* 自动生成: 2026-06-01 09:36:17
*/
test.describe('🐛 Bug#610', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#610 Bug #610 待确认标题 @bug610 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-610-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #611: Bug #611 待确认标题
* 自动生成: 2026-06-01 09:36:17
*/
test.describe('🐛 Bug#611', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#611 Bug #611 待确认标题 @bug611 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-611-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #613: Bug #613 待确认标题
* 自动生成: 2026-06-01 09:36:17
*/
test.describe('🐛 Bug#613', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#613 Bug #613 待确认标题 @bug613 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-613-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #614: Bug #614 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#614', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#614 Bug #614 待确认标题 @bug614 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-614-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #615: Bug #615 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#615', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#615 Bug #615 待确认标题 @bug615 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-615-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #616: Bug #616 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#616', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#616 Bug #616 待确认标题 @bug616 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-616-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #625: Bug #625 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#625', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#625 Bug #625 待确认标题 @bug625 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-625-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #626: Bug #626 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#626', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#626 Bug #626 待确认标题 @bug626 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-626-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #627: Bug #627 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#627', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#627 Bug #627 待确认标题 @bug627 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-627-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #628: Bug #628 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#628', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#628 Bug #628 待确认标题 @bug628 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-628-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,47 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #629: Bug #629 待确认标题
* 自动生成: 2026-06-01 09:36:18
*/
test.describe('🐛 Bug#629', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#629 Bug #629 待确认标题 @bug629 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-629-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -1,83 +0,0 @@
import { test, expect } from '@playwright/test';
/**
* Bug #630: [门诊医生站] 点击选择现诊患者列表报错
* 禅道信息:
* - 登录doctor1 / 123456租户=中联医院(tenantId=1)
* - 步骤:进入门诊医生站 → 点击左侧现诊患者 → 观察右侧加载
*/
test.describe('🐛 Bug#630 门诊医生站现诊患者列表', () => {
test('#630 点击现诊患者不应报错 @bug630 @regression', async ({ page }) => {
// 1. 登录
await page.goto('http://localhost:81/');
const loginResp = await page.request.post('http://localhost:18082/openhis/login', {
data: { username: 'doctor1', password: '123456', tenantId: '1', code: '', uuid: '' }
});
const loginData = await loginResp.json();
expect(loginData.code).toBe(200);
await page.context().addCookies([{
name: 'Admin-Token', value: loginData.token, domain: 'localhost', path: '/'
}]);
// 2. 进入首页
await page.goto('http://localhost:81/index');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
// 3. 通过侧边栏导航到门诊医生站
await page.locator('text=门诊医生工作站').first().click();
await page.waitForTimeout(1000);
await page.locator('text=门诊医生站').first().click();
await page.waitForTimeout(5000);
// 4. 验证"现诊患者"标签存在
const patientLabel = page.locator('text=现诊患者');
await expect(patientLabel).toBeVisible({ timeout: 10000 });
console.log('✅ 现诊患者标签可见');
// 5. 截图:门诊医生站页面
await page.screenshot({ path: 'test-results/bug-630-step1.png', fullPage: true });
// 6. 查找患者列表项(可能是表格行或列表项)
const patientSelectors = [
'.el-table__body tr',
'.patient-item',
'.current-patient',
'[class*="patient-list"] li',
'.list-item',
];
let clickedPatient = false;
for (const selector of patientSelectors) {
const items = page.locator(selector);
const count = await items.count();
if (count > 0) {
console.log(`找到 ${count} 个患者 (${selector})`);
try {
await items.first().click({ timeout: 5000 });
clickedPatient = true;
console.log('✅ 已点击患者');
break;
} catch {
console.log(`点击失败 (${selector})`);
}
}
}
if (!clickedPatient) {
console.log('⚠️ 没有患者数据,测试环境可能无数据');
}
// 7. 等待右侧加载
await page.waitForTimeout(5000);
await page.screenshot({ path: 'test-results/bug-630-step2.png', fullPage: true });
// 8. 验证没有错误弹窗
const errorPopups = page.locator('.el-message--error');
const errorCount = await errorPopups.count();
console.log('错误弹窗:', errorCount);
await page.screenshot({ path: 'test-results/bug-630-final.png', fullPage: true });
expect(errorCount).toBe(0);
});
});

View File

@@ -1,98 +0,0 @@
#!/bin/bash
# 为指定 Bug 生成 Playwright 测试用例
# 用法: ./generate-bug-test.sh <bug_id> <bug_title> [bug_steps]
BUG_ID="$1"
BUG_TITLE="$2"
BUG_STEPS="$3"
if [ -z "$BUG_ID" ] || [ -z "$BUG_TITLE" ]; then
echo "用法: $0 <bug_id> <bug_title> [bug_steps]"
exit 1
fi
SPEC_DIR="$(dirname "$0")/../specs"
SPEC_FILE="${SPEC_DIR}/bug-${BUG_ID}.spec.ts"
# 如果测试已存在,跳过
if [ -f "$SPEC_FILE" ]; then
echo "SKIP: ${SPEC_FILE} 已存在"
exit 0
fi
mkdir -p "$SPEC_DIR"
# 从标题推断模块
infer_route() {
local t="$1"
if echo "$t" | grep -qi "门诊医生\|门诊诊前\|门诊挂号"; then echo "/doctorstation"; return; fi
if echo "$t" | grep -qi "住院医生\|临床医嘱\|医嘱录入"; then echo "/inpatientDoctor"; return; fi
if echo "$t" | grep -qi "住院护士\|补费\|发退药\|医嘱执行"; then echo "/inpatientNurse"; return; fi
if echo "$t" | grep -qi "分诊\|排队\|候诊"; then echo "/triageandqueuemanage"; return; fi
if echo "$t" | grep -qi "挂号\|预约\|签到"; then echo "/registration"; return; fi
if echo "$t" | grep -qi "手术\|计费"; then echo "/operatingroom"; return; fi
if echo "$t" | grep -qi "诊断\|中医"; then echo "/inpatientDoctor"; return; fi
if echo "$t" | grep -qi "病历\|EMR"; then echo "/doctorstation"; return; fi
if echo "$t" | grep -qi "目录\|诊疗"; then echo "/catalog"; return; fi
if echo "$t" | grep -qi "药房\|发药\|库存"; then echo "/pharmacy"; return; fi
echo "/"
}
ROUTE=$(infer_route "$BUG_TITLE")
STEPS_COMMENT=""
if [ -n "$BUG_STEPS" ]; then
STEPS_COMMENT="// 复现步骤:
// $(echo "$BUG_STEPS" | head -5)"
fi
cat > "$SPEC_FILE" << SPECEOF
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #${BUG_ID}: ${BUG_TITLE}
* 自动生成: $(date '+%Y-%m-%d %H:%M:%S')
*/
test.describe('🐛 Bug#${BUG_ID}', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#${BUG_ID} ${BUG_TITLE} @bug${BUG_ID} @regression', async ({ page }) => {
await page.goto('${ROUTE}');
await page.waitForLoadState('networkidle');
${STEPS_COMMENT}
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-${BUG_ID}-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});
SPECEOF
echo "OK: ${SPEC_FILE}"

View File

@@ -1,191 +0,0 @@
/**
* Bug 回归测试用例生成器
*
* 根据 Bug 标题、描述、复现步骤自动生成 Playwright 测试用例。
* 每个 Bug 生成独立的 spec 文件tests/e2e/specs/bug-{id}.spec.ts
*/
export interface BugInfo {
id: string;
title: string;
description?: string;
steps?: string;
module?: string;
severity?: string;
}
/**
* 从 Bug 标题推断所属模块和页面路径
*/
function inferModule(title: string): { page: string; route: string; description: string } {
const t = title.toLowerCase();
if (t.includes('门诊医生') || t.includes('门诊诊前') || t.includes('门诊挂号')) {
return { page: '门诊医生站', route: '/doctorstation', description: '门诊医生工作站' };
}
if (t.includes('住院医生') || t.includes('临床医嘱') || t.includes('医嘱录入')) {
return { page: '住院医生站', route: '/inpatientDoctor', description: '住院医生工作站' };
}
if (t.includes('住院护士') || t.includes('补费') || t.includes('发退药') || t.includes('医嘱执行')) {
return { page: '住院护士站', route: '/inpatientNurse', description: '住院护士工作站' };
}
if (t.includes('分诊') || t.includes('排队') || t.includes('候诊')) {
return { page: '分诊台', route: '/triageandqueuemanage', description: '分诊排队管理' };
}
if (t.includes('挂号') || t.includes('预约') || t.includes('签到')) {
return { page: '挂号', route: '/registration', description: '门诊挂号' };
}
if (t.includes('手术') || t.includes('计费')) {
return { page: '手术管理', route: '/operatingroom', description: '手术管理/计费' };
}
if (t.includes('诊断') || t.includes('中医')) {
return { page: '诊断录入', route: '/inpatientDoctor', description: '诊断录入模块' };
}
if (t.includes('病历') || t.includes('EMR') || t.includes('emr')) {
return { page: '病历', route: '/doctorstation', description: '电子病历' };
}
if (t.includes('目录') || t.includes('诊疗')) {
return { page: '目录管理', route: '/catalog', description: '诊疗目录管理' };
}
if (t.includes('药房') || t.includes('发药') || t.includes('库存')) {
return { page: '药房管理', route: '/pharmacy', description: '药房管理' };
}
return { page: '未知模块', route: '/', description: '通用模块' };
}
/**
* 从 Bug 标题推断需要测试的关键操作
*/
function inferTestActions(title: string): string[] {
const actions: string[] = [];
const t = title.toLowerCase();
if (t.includes('报错') || t.includes('错误') || t.includes('异常')) {
actions.push('检查页面无 JS 错误');
actions.push('检查控制台无报错');
}
if (t.includes('显示') || t.includes('缺失') || t.includes('不规范')) {
actions.push('检查元素正确显示');
actions.push('检查数据完整性');
}
if (t.includes('弹窗') || t.includes('弹框')) {
actions.push('检查弹窗正常弹出');
actions.push('检查弹窗内容正确');
}
if (t.includes('保存') || t.includes('提交') || t.includes('写入')) {
actions.push('检查保存操作成功');
actions.push('检查数据持久化');
}
if (t.includes('列表') || t.includes('查询')) {
actions.push('检查列表数据加载');
actions.push('检查分页功能');
}
if (t.includes('按钮') || t.includes('操作')) {
actions.push('检查按钮可点击');
actions.push('检查操作响应');
}
if (t.includes('下拉') || t.includes('选择') || t.includes('字典')) {
actions.push('检查下拉选项加载');
actions.push('检查选项值正确');
}
if (t.includes('退回') || t.includes('撤回') || t.includes('取消')) {
actions.push('检查退回流程');
actions.push('检查状态变更');
}
// 至少有一个基础检查
if (actions.length === 0) {
actions.push('检查页面正常加载');
actions.push('检查无明显异常');
}
return actions;
}
/**
* 生成 Playwright 测试用例代码
*/
export function generateBugTestSpec(bug: BugInfo): string {
const mod = inferModule(bug.title);
const actions = inferTestActions(bug.title);
const stepsComment = bug.steps
? `\n // 复现步骤:\n // ${bug.steps.split('\n').join('\n // ')}`
: '';
return `import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #${bug.id}: ${bug.title}
* 模块: ${mod.description}
* 自动生成时间: ${new Date().toISOString()}
* 严重程度: ${bug.severity || '未知'}
*/
test.describe('🐛 Bug#${bug.id} ${mod.description}', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#${bug.id} ${bug.title} @bug${bug.id} @regression', async ({ page }) => {
// 导航到目标页面
await page.goto('${mod.route}');
await page.waitForLoadState('networkidle');
${stepsComment}
// ── 检查项 ──
// 1. 页面正常加载
await expect(page).not.toHaveURL(/.*login.*/);
// 2. 检查页面无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 3. 执行具体检查
${actions.map(a => ` // ${a}
await page.waitForTimeout(500);`).join('\n')}
// 4. 断言:无 JS 错误
expect(jsErrors).toEqual([]);
// 5. 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-${bug.id}-result.png',
fullPage: true
});
});
});
`;
}
/**
* 将测试用例写入文件
*/
export function writeBugTestSpec(bug: BugInfo): string {
const spec = generateBugTestSpec(bug);
const fs = require('fs');
const path = require('path');
const specDir = path.join(__dirname, '..', 'specs');
const filePath = path.join(specDir, `bug-${bug.id}.spec.ts`);
// 不覆盖已有测试
if (fs.existsSync(filePath)) {
return filePath;
}
fs.mkdirSync(specDir, { recursive: true });
fs.writeFileSync(filePath, spec, 'utf-8');
return filePath;
}

View File

@@ -40,7 +40,7 @@ export default defineConfig(({ mode, command }) => {
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: process.env.VITE_API_PROXY || 'http://localhost:18080/openhis',
target: 'http://localhost:18080/openhis',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, ''),
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long