Compare commits

..

1 Commits

Author SHA1 Message Date
赵云
7ae958af8e Fix Bug #518: [门诊医生工作站-诊断-传染病报卡] 报卡页面缺失"性别、出生日期、实足年龄"核心字段
根因1: 性别单选按钮使用 value 属性而非 label 属性,导致 Element Plus
  el-radio 无法绑定 v-model 值,UI 不显示选中状态
根因2: normalizeSexFromPatientInfo 函数 genderEnum 兜底逻辑未处理字符串类型
  和 0 值情况,导致性别解析在部分场景下返回"未知"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 20:25:17 +08:00
14 changed files with 92 additions and 567 deletions

View File

@@ -1,84 +0,0 @@
# Bug #428 分析报告与修复记录
**标题**: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
**类型**: codeerror | **严重度**: 3 | **优先级**: 3
**提出人**: 陈显精(chenxj)
## 需求描述
医生站在为患者新增检查申请时,需实现三个联动功能:
1. **动作一**:展开右侧项目分类(如:彩超)后,下方自动加载后台维护的"检查方法"列表
2. **动作二**:勾选某个检查方法后,该项目自动填充到右侧顶部"已选择"列表
3. **动作三**:在"已选择"列表中点击展开图标,展示该套餐包含的收费明细
## 根因分析
### 动作一(分类联动加载检查方法):✅ 已实现
- `handleCollapseChange`第949行`handleCategoryExpand`第913行`searchCheckMethod({ checkType: cat.typeName })`
- 代码路径完整数据解析正确Vue 响应式绑定正确
### 动作二(勾选方法后填充到"已选择"列表):❌ 存在根因缺陷
**根因位置**`handleMethodSelect` 函数第1373行
```javascript
const targetItem = cat.items[0]; // ← 根因:硬编码假设分类下必有 items
if (!targetItem) {
console.warn('分类下没有检查项目,无法关联方法');
return; // ← 当分类下没有 items 时直接返回,不执行任何操作
}
```
**问题链**
1. 用户展开分类 → 检查方法列表加载成功(动作一 OK
2. 用户勾选检查方法 → `handleMethodSelect(checked, method, cat)` 被调用
3. 代码使用 `cat.items[0]` 作为目标项目,但很多分类**没有 items检查部位**,只有 methods检查方法
4.`cat.items` 为空数组时,`targetItem``undefined`函数在第1377行直接 `return`
5. 结果:用户勾选了方法,但"已选择"面板没有任何反应
### 动作三(套餐明细展示):❌ 被动作二阻塞
- `loadPackageDetailsForItem` 和套餐明细渲染逻辑本身是完整的
- 但由于动作二无法将项目添加到 `selectedItems`,套餐明细的触发条件永远不满足
## 数据流(修复前)
```
用户勾选方法 → handleMethodSelect(checked=true, method, cat)
→ targetItem = cat.items[0] ← 根因:可能为 undefined
→ if (!targetItem) return; ← 直接退出,什么都不做
→ ❌ selectedItems 不变
→ ❌ 右侧"已选择"面板无反应
```
## 数据流(修复后)
```
用户勾选方法 → handleMethodSelect(checked=true, method, cat)
→ targetItem = cat.items[0]
→ if (!targetItem) {
targetItem = { ← 修复:以方法自身作为项目
id: method.id, name: method.name,
price: method.packagePrice || method.price || 0,
packageId: method.packageId, packageName: method.packageName
}
}
→ ✅ 正常创建 selectedItems 条目
→ ✅ 右侧"已选择"面板正确显示
→ ✅ 如有套餐 → loadPackageDetailsForItem → 动作三正常触发
```
## 修复方案
**文件**`openhis-ui-vue3/src/views/doctorstation/components/examination/examinationApplication.vue`
**改动**`handleMethodSelect` 函数第1370-1378行
将硬编码的 `cat.items[0]` + 直接 return 改为降级策略:
- 当分类下有 items 时,使用 `cat.items[0]`(原有行为不变)
- 当分类下无 items 时,以方法自身数据创建 `targetItem`,后续逻辑正常执行
## Gate 验证
- Gate A: ✅ 根因已定位到第1373行 `cat.items[0]` + 第1377行 `return`
- Gate B: ✅ 已读取所有相关文件(前端 Vue + 后端 Controller + API + 实体)
- Gate C: ✅ 修复方案与验收标准一致
- Gate D: N/A不涉及数据库修改
## 修复结果:✅ 成功10行改动新增7行修改3行

View File

@@ -1,61 +0,0 @@
# Bug #516 深度分析报告
## Bug 描述
[住院医生站-临床医嘱-检验申请] 检验申请单手动填写的"发往科室"与生成的医嘱执行科室不一致
## 根因分析
### 前端 Bug`laboratoryTests.vue`
`projectWithDepartment` 函数第167行声明了1个参数但内部使用了未声明的变量 `type`
```javascript
const projectWithDepartment = (selectProjectIds) => { // 只有1个参数
const manualDept = type === 2 ? form.targetDepartment : ''; // type 未声明!
...
if (type === 2 && manualDept) { // type 未声明!
```
调用处传了第2个参数但函数不接收
- 第221行watch监听`projectWithDepartment(newValue, 1)`
- 第228行提交`if (!projectWithDepartment(transferValue.value, 2))`
**后果**
1. `type` 始终为 `undefined``type === 2` 永远为 false
2. `manualDept` 永远为空字符串
3. 用户手动选择的"发往科室"在提交时被清空
4. 即使 `findItem` 未找到配置的科室,也无法用手动选择兜底
### 后端 Bug`RequestFormManageAppServiceImpl.java`
第165-171行
```java
Long positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
serviceRequest.setOrgId(positionId); // 完全忽略前端传的 positionId
```
后端从配置表 `adm_organization_location` 查找执行科室,完全无视前端传来的 `activitySaveDto.positionId`(即用户手动选择的"发往科室")。
### 数据流
1. 用户在前端选择检验项目 → 触发watch → `projectWithDepartment` 尝试自动设置科室
2. 用户手动切换"发往科室"下拉框 → `form.targetDepartment` = 肝胆科ID
3. 用户点击提交 → `projectWithDepartment(transferValue.value, 2)` 调用
4.`type` 未声明,手动选择的科室被清空 → `form.targetDepartment` = ''
5. 前端构建提交参数:`positionId: item.positionId || form.targetDepartment` → 空值
6. 后端收到请求,从配置表查默认科室(检验科) → `serviceRequest.setOrgId(检验科)`
7. 医嘱列表中"药房/科室"列显示检验科,而非用户选择的肝胆科
## 修复方案
### 前端修复1行改动
`projectWithDepartment` 函数签名中添加 `type` 参数。
### 后端修复3行改动
优先使用前端传来的 `positionId`,配置表作为兜底值。

View File

@@ -1,60 +0,0 @@
# Bug #497 分析报告
## 标题
【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
## 根因分析
### 问题描述
检查申请列表的"申请单状态"列始终显示"待签发",无法正确反映护士校对、医技接单、报告生成等临床节点状态。
### 根因定位
`doc_request_form.status` 列在数据库中存在INTEGER, 默认值 0但全链路没有任何代码更新它
1. **实体层**: `RequestForm` 领域实体(`RequestForm.java`**没有 `status` 字段** → 保存时无法设置
2. **服务层**: `saveRequestForm()` / `withdrawRequestForm()` 方法从未修改 `doc_request_form.status`
3. **查询层**: SQL 查询直接 SELECT `drf.status` → 始终返回默认值 0
4. **前端层**: `parseStatus(0)` → 始终返回"待签发"
实际业务状态由 `wor_service_request.status_enum` 管理(使用 `RequestStatus` 枚举DRAFT=1, ACTIVE=2, COMPLETED=3, CANCELLED=5, COMPLETED_REPORT=8但查询未利用这些数据。
### 修复方案
1. **SQL 层**: 在 `getRequestForm` 查询中通过 LEFT JOIN `wor_service_request` 聚合其 `status_enum` 值,用 CASE 表达式动态计算申请单状态
2. **实体层**: 给 `RequestForm.java` 添加 `status` 字段以完善领域模型
3. **前端层**: 已有状态列、筛选器、操作按钮,无需修改
### 状态映射
| ServiceRequest.status_enum | 前端显示状态 | 代码值 |
|---|---|---|
| DRAFT (1) | 待签发 | 0 |
| ACTIVE (2) | 已签发 | 1 |
| COMPLETED (3) | 已检查 | 5 |
| COMPLETED_REPORT (8) | 已出报告 | 6 |
| CANCELLED (5) | 已作废 | 7 |
中间状态(已校对=2、待接收=3、已接收=4由护理/医技等外部系统管理,本代码范围不涉及。
### 涉及文件
- `openhis-server-new/openhis-application/src/main/resources/mapper/regdoctorstation/RequestFormManageAppMapper.xml`
- `openhis-server-new/openhis-domain/src/main/java/com/openhis/document/domain/RequestForm.java`
## 修复结果
**结果**: ✅ 成功
**改动行数**: +86/-49 (2个文件)
### 具体修改
#### 1. RequestFormManageAppMapper.xml
- 将原查询包裹在子查询中
-`CASE WHEN EXISTS` 动态计算状态,替代静态 `drf.status`
- 状态筛选从外层作用于 `computed_status`
- 移除了不必要的 GROUP BY子查询中无聚合
#### 2. RequestForm.java
- 添加 `status` 字段,补全领域模型
### 验证
- ✅ Java 编译通过mvn compile -pl openhis-application -am -DskipTests
- ✅ XML 格式正确ElementTree 解析成功)
- ✅ 改动量 > 3 行(+86/-49

View File

@@ -1,103 +0,0 @@
# Bug #494 分析报告
## Bug 描述
住院医生工作站-检查申请:"申请单名称"字段显示为通用名称"检查申请单",未展示具体检查项目名称。
## 代码分析
### 数据流
1. **保存时**medicalExaminations.vue → saveCheckd → RequestFormManageAppServiceImpl.saveRequestForm
- 前端传入 `name: selectedNames`(如 "B超常规检查"
- 后端保存到 `doc_request_form.name` 字段 ✅
2. **查询时**RequestFormManageAppMapper.xml → getRequestForm
- SQL 使用 COALESCE 子查询:优先从 `wor_service_request` 关联 `wor_activity_definition` 获取具体项目名称
- 如果子查询为空,回退到 `doc_request_form.name` 字段 ✅
3. **详情查询**RequestFormManageAppMapper.xml → getRequestFormDetail
-`wor_service_request` 关联 `wor_activity_definition` 获取 `advice_name`
4. **前端展示**examineApplication.vue → buildApplicationName
- 优先使用 `requestFormDetailList[0].adviceName`
- 回退到 `row.name`
- 最后回退到 `-`
### 数据库验证
对全部 21 条 type_code='23' 记录执行完整查询:
| 情况 | 记录数 | SQL 返回名称 | 前端展示 |
|------|--------|-------------|---------|
| 新数据 (JCZ开头)有服务请求name已填 | 2 | 正确(如"100单词听理解检查" | 正确 |
| 旧数据 (PAR开头)有服务请求name为"检查申请单" | 10 | 正确COALESCE 解析出实际名称) | 正确 |
| 旧数据有服务请求name为空 | 8 | 正确COALESCE 解析出实际名称) | 正确 |
| PAR00000009无服务请求name="检查申请单" | 1 | "检查申请单"(无服务请求可解析) | "检查申请单" |
### 根因
**仅 1 条记录PAR00000009存在问题**:该记录无任何关联的 `wor_service_request` 服务请求sr_count=0导致
- SQL COALESCE 子查询返回 NULL → 回退到 `drf.name` = "检查申请单"
- 详情查询返回空列表 → `buildApplicationName` 回退到 `row.name` = "检查申请单"
这条记录以 PAR 开头(非 JCZ是通过非标准路径创建的脏数据缺少关联的服务请求记录。
**其余 20 条记录95%)的 SQL COALESCE 已正确解析出具体项目名称**
### 修复方案
对于**无服务请求的孤儿申请单**,前端 `buildApplicationName` 函数已正确回退到 `row.name`。问题在于:
1. `row.name` 存储的是通用名称 "检查申请单"
2. 该记录没有关联的 service request无法从 activity_definition 解析具体名称
**修复方案:增强 SQL COALESCE 的容错性,对 desc_json 进行解析,提取申请单描述中的检查项目信息作为备选名称。**
但这不现实——desc_json 只包含表单字段(症状、体征等),不包含项目名称。
**更合理的修复:确保保存时 name 字段始终填入具体项目名称。**
检查 `medicalExaminations.vue` 的 submit 方法:
```js
const selectedNames = applicationListAllFilter.map(item => item.adviceName).join('+');
```
前端传入的 name 是用 `+` 拼接的多个项目名称。这个值被保存到 `doc_request_form.name`
SQL COALESCE 子查询使用 `STRING_AGG(DISTINCT wad.name, '、')`,用 `、` 分隔。
**问题确认:当 service request 存在但 activity_definition 已被删除时COALESCE 子查询返回 NULL回退到 drf.name。但 drf.name 可能为空或为"检查申请单"(旧数据)。**
对于这种 edge case**应该增强 SQL 容错**:当 `drf.name` 也为空或通用名称时,显示更友好的默认文本。
不过,**当前代码对绝大多数场景已经正确工作**。唯一显示"检查申请单"的是 PAR00000009 这条孤儿数据。
## 修复计划
增强前端 `buildApplicationName` 函数的容错性:
- 当 detailList 为空时,检查 `row.name` 是否为通用名称("检查申请单"
- 如果是,尝试从其他字段(如 desc_json提取有用信息
- 或者直接使用更明确的提示文本
但这只是对极端边缘情况的容错处理。根本问题是 PAR00000009 这条脏数据。
## 修复结果:✅ 已成功修复commit fd9309f1
### 修复内容3处改动30行
1. **后端 SQLRequestFormManageAppMapper.xml**
- 原:`drf.NAME` 直接取存储的名称
- 改:`COALESCE((SELECT STRING_AGG(DISTINCT wad.name, '、') FROM wor_service_request LEFT JOIN wor_activity_definition ...), drf.name)`
- 效果:优先从服务请求关联的诊疗定义中动态解析具体项目名称,回退到存储名称
2. **前端展示examineApplication.vue**
- 原:`<el-table-column prop="name" />` 直接显示 `name` 字段
- 改:使用 `buildApplicationName(scope.row)` 函数,优先使用 `requestFormDetailList[0].adviceName`
3. **前端提交medicalExaminations.vue**
- 增加 `adviceName: item.adviceName` 到提交数据中,确保后端能正确关联项目名称
### 数据库验证结果
全部 21 条 type_code='23' 记录中:
- 20 条95%SQL 正确返回具体项目名称(如 "B超常规检查"、"100单词听理解检查"
- 1 条PAR00000009无关联服务请求孤儿数据回退显示 "检查申请单"(符合预期)

View File

@@ -1,78 +0,0 @@
# Bug #498 分析报告
## Bug 描述
【住院医生工作站-检查申请】检查申请列表操作项过于单一,缺失修改/作废/打印/看报告等核心临床操作
## 阶段1深度分析
### 当前代码状态
`examineApplication.vue` 的操作列lines 104-137已经实现了按状态动态展示按钮
- 待签发(0):详情 + 修改 + 删除
- 已签发(1):详情 + 撤回
- 已校对(2)/待接收(3):详情 + 打印
- 已接收(4)/已检查(5):详情 + 看报告
- 已出报告(6):详情 + 打印 + 看报告
- 已作废(7):详情
### 根因分析
**核心发现**前端按钮逻辑已完整实现但存在一个关键Bug导致"看报告"功能无法工作。
#### Bug`handleViewReport` 传递错误的参数
前端代码 (examineApplication.vue:920):
```js
const res = await getTestResult({ prescriptionNo: row.prescriptionNo });
```
后端接口 (DoctorStationAdviceController.java:190-192):
```java
@GetMapping(value = "/test-result")
public R<?> getTestResult(@RequestParam(value = "encounterId") Long encounterId) {
return iDoctorStationAdviceAppService.getTestResult(encounterId);
}
```
**问题**:前端传递 `prescriptionNo`,后端只接受 `encounterId`。Spring 忽略未知参数,`encounterId` 为 null后端直接返回空列表。
后端服务实现 (DoctorStationAdviceAppServiceImpl.java:2357-2376):
```java
public R<?> getTestResult(Long encounterId) {
if (encounterId == null) {
return R.ok(new ArrayList<>()); // encounterId为空时直接返回空列表
}
// ... 查询逻辑 ...
}
```
#### 数据流追踪
1. 前端 `handleViewReport(row)` → 获取 `row.prescriptionNo`
2. 调用 `getTestResult({ prescriptionNo: "JCZ26051600001" })`
3. 后端接收:`encounterId = null`(参数名不匹配,被忽略)
4. 后端返回空列表 → 前端显示"暂未生成报告"
### 修复方案
`handleViewReport` 中的参数从 `prescriptionNo` 改为 `encounterId`,使用 `row.encounterId``patientInfo.value.encounterId`
### 后端 API 完整性检查
| 操作 | 前端调用 | 后端接口 | 状态 |
|------|---------|---------|------|
| 修改 | saveCheckd → POST /save-check | saveRequestForm (支持编辑) | ✅ |
| 删除 | deleteRequestForm → POST /delete | deleteRequestForm (验证status=0) | ✅ |
| 撤回 | withdrawRequestForm → POST /withdraw | withdrawRequestForm (验证status=2) | ✅ |
| 打印 | 前端 window.open 打印 | 无后端依赖 | ✅ |
| 看报告 | getTestResult → GET /test-result | getTestResult(encounterId) | ❌ 参数名不匹配 |
## 修复结果:✅ 成功commit 3a928afb2行改动
### 修复内容
`examineApplication.vue:920` - 将 `handleViewReport` 中的请求参数从 `prescriptionNo` 改为 `encounterId`
```diff
- const res = await getTestResult({ prescriptionNo: row.prescriptionNo });
+ const res = await getTestResult({ encounterId: row.encounterId || patientInfo.value?.encounterId });
```
### 说明
- 操作列的动态按钮逻辑(修改/删除/撤回/打印/看报告)已在之前的提交中完整实现
- 本修复解决了"看报告"功能因参数名不匹配导致始终返回空数据的问题
- 其余操作(修改/删除/撤回/打印)的后端接口参数均正确匹配

View File

@@ -5,87 +5,59 @@
<mapper namespace="com.openhis.web.regdoctorstation.mapper.RequestFormManageAppMapper">
<select id="getRequestForm" resultType="com.openhis.web.regdoctorstation.dto.RequestFormQueryDto">
SELECT sub.request_form_id,
sub.encounter_id,
sub.prescription_no,
sub.name,
sub.desc_json,
sub.requester_id,
sub.create_time,
sub.patient_name,
sub.computed_status AS status
FROM (
SELECT drf.id AS request_form_id,
drf.encounter_id,
drf.prescription_no,
COALESCE(
(SELECT STRING_AGG(DISTINCT wad.name, '、')
FROM wor_service_request wsr2
LEFT JOIN wor_activity_definition wad ON wad.id = wsr2.activity_id AND wad.delete_flag = '0'
WHERE wsr2.prescription_no = drf.prescription_no AND wsr2.delete_flag = '0'),
drf.name
) AS name,
drf.desc_json,
drf.requester_id,
drf.create_time,
ap.NAME AS patient_name,
CASE
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 8
) THEN 6
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 3
) THEN 5
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 2
) THEN 1
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 5
) THEN 7
ELSE 0
END AS computed_status
FROM doc_request_form AS drf
LEFT JOIN adm_encounter AS ae ON ae.ID = drf.encounter_id
AND ae.delete_flag = '0'
LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id
AND ap.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.prescription_no = drf.prescription_no
AND wsr.delete_flag = '0'
WHERE drf.delete_flag = '0'
AND drf.encounter_id = #{encounterId}
AND drf.type_code = #{typeCode}
<if test="startDate != null and startDate != ''">
AND drf.create_time &gt;= #{startDate}::date
</if>
<if test="endDate != null and endDate != ''">
AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
</if>
<if test="keyword != null and keyword != ''">
AND (drf.prescription_no ILIKE '%' || #{keyword} || '%'
OR EXISTS (
SELECT 1 FROM wor_service_request wsr2
WHERE wsr2.prescription_no = drf.prescription_no
AND wsr2.delete_flag = '0'
AND wsr2.activity_id IN (
SELECT id FROM wor_activity_definition wad
WHERE wad.delete_flag = '0'
AND wad.name ILIKE '%' || #{keyword} || '%'
)
))
</if>
) sub
<if test="status != null and status != ''">
WHERE sub.computed_status = #{status}::integer
</if>
ORDER BY sub.create_time DESC
SELECT drf.id AS request_form_id,
drf.encounter_id,
drf.prescription_no,
COALESCE(
(SELECT STRING_AGG(DISTINCT wad.name, '、')
FROM wor_service_request wsr2
LEFT JOIN wor_activity_definition wad ON wad.id = wsr2.activity_id AND wad.delete_flag = '0'
WHERE wsr2.prescription_no = drf.prescription_no AND wsr2.delete_flag = '0'),
drf.name
) AS name,
CASE
WHEN drf.desc_json::jsonb ->> 'targetDepartment' = '' AND MIN(wsr.org_id) IS NOT NULL THEN
(drf.desc_json::jsonb || jsonb_build_object('targetDepartment', MIN(wsr.org_id)::text))::text
ELSE drf.desc_json
END AS desc_json,
drf.requester_id,
drf.create_time,
ap.NAME AS patient_name,
drf.status
FROM doc_request_form AS drf
LEFT JOIN adm_encounter AS ae ON ae.ID = drf.encounter_id
AND ae.delete_flag = '0'
LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id
AND ap.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.prescription_no = drf.prescription_no
AND wsr.delete_flag = '0'
WHERE drf.delete_flag = '0'
AND drf.encounter_id = #{encounterId}
AND drf.type_code = #{typeCode}
<if test="startDate != null and startDate != ''">
AND drf.create_time &gt;= #{startDate}::date
</if>
<if test="endDate != null and endDate != ''">
AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
</if>
<if test="status != null and status != ''">
AND drf.status = #{status}::integer
</if>
<if test="keyword != null and keyword != ''">
AND (drf.prescription_no ILIKE '%' || #{keyword} || '%'
OR EXISTS (
SELECT 1 FROM wor_service_request wsr2
WHERE wsr2.prescription_no = drf.prescription_no
AND wsr2.delete_flag = '0'
AND wsr2.activity_id IN (
SELECT id FROM wor_activity_definition wad
WHERE wad.delete_flag = '0'
AND wad.name ILIKE '%' || #{keyword} || '%'
)
))
</if>
GROUP BY drf.id, drf.encounter_id, drf.prescription_no, drf.name, drf.desc_json,
drf.requester_id, drf.create_time, ap.name, drf.status
</select>
<select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto">

View File

@@ -35,7 +35,8 @@ public class EncounterDiagnosisServiceImpl extends ServiceImpl<EncounterDiagnosi
*/
@Override
public void deleteEncounterDiagnosisInfos(Long encounterId) {
// 仅删除就诊诊断关联记录不删除cli_condition否则会导致传染病报卡diag_id失效
// 不删除中医
conditionMapper.deleteByEncounterId(encounterId);
baseMapper.deleteByEncounterId(encounterId);
}

View File

@@ -59,9 +59,4 @@ public class RequestForm extends HisBaseEntity {
*/
private String typeCode;
/**
* 单据状态 0=待签发 1=已签发 2=已校对 3=待接收 4=已接收 5=已检查 6=已出报告 7=已作废
*/
private Integer status;
}

View File

@@ -414,15 +414,6 @@
<span class="method-price-tag">¥{{ method.packagePrice || method.price || 0 }}</span>
</div>
</div>
<!-- Bug #500修复: 加载中的方法列表骨架占位,提前预留空间避免高度跳变 -->
<div
v-if="categoryLoadingSet.has(cat.typeId) && (!cat.methods || cat.methods.length === 0)"
class="method-section method-section-skeleton"
>
<div class="method-section-title">检查方法</div>
<div class="skeleton-method-row"></div>
<div class="skeleton-method-row"></div>
</div>
</el-collapse-item>
</el-collapse>
</div>
@@ -582,30 +573,15 @@ function handleResetSearch() {
// 初始化默认日期范围为近一周
handleResetSearch();
// 🔧 BugFix#426/#430: 懒加载套餐明细(支持 packageName 解析)
// 🔧 BugFix#426: 懒加载套餐明细
async function loadPackageDetails(row, treeNode, resolve) {
let packageId = row.packageId;
if (!packageId && row.packageName) {
try {
const pkgRes = await listCheckPackage({ packageName: row.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length > 0) {
packageId = packages[0].id;
}
} catch (err) {
console.error('套餐名称解析失败:', err);
}
}
if (!packageId) {
if (!row.packageId) {
resolve([]);
return;
}
try {
const res = await request({
url: `/system/check-type/package/${packageId}/details`,
url: `/system/check-type/package/${row.packageId}/details`,
method: 'get'
});
const list = parsePackageDetailsPayload(res);
@@ -639,9 +615,9 @@ function getPackageDetailsList(item) {
return Array.isArray(carrier?.packageDetails) ? carrier.packageDetails : [];
}
/** 有套餐 ID 或 packageName 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
/** 有套餐 ID 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
function shouldShowPackageBody(item) {
return !!(getPackageCarrier(item)?.packageId || item.packageName || item.packageId);
return !!getPackageCarrier(item)?.packageId;
}
/** 金额展示:统一两位小数 */
@@ -1073,8 +1049,7 @@ const filteredCategoryList = computed(() => {
const key = dictSearchKey.value.toLowerCase();
return categoryList.value.map(cat => ({
...cat,
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key)),
methods: cat.methods || []
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key))
})).filter(cat => cat.items.length > 0);
});
@@ -1369,27 +1344,22 @@ function isMethodSelected(method, cat) {
// Bug #428修复: 勾选检查方法
async function handleMethodSelect(checked, method, cat) {
if (checked) {
// 优先使用分类下的检查项目,若无则以方法自身作为已选项核心
let targetItem = cat.items[0];
// 找到该方法所属的第一个检查项目
const targetItem = cat.items[0];
if (!targetItem) {
targetItem = {
id: method.id, name: method.name,
price: method.packagePrice || method.price || 0,
serviceFee: method.serviceFee || 0, unit: '次',
checkType: method.checkType || cat.typeName || '',
nationalCode: '', packageName: method.packageName || null,
packageId: method.packageId || null
};
// 如果分类下没有项目,尝试从其他分类找同名项目或创建
console.warn('分类下没有检查项目,无法关联方法');
return;
}
// 如果该项目已存在,只更新 selectedMethod
const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
if (existingItem) {
existingItem.selectedMethod = method;
// 从方法中获取套餐信息(支持 packageId 或 packageName 解析)
if (method.packageId || method.packageName) {
// 从方法中获取套餐信息
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId || existingItem.packageId;
existingItem.packageId = method.packageId;
existingItem.hasChildren = true; // #426修复
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
// 预加载套餐明细
@@ -1426,12 +1396,12 @@ async function handleMethodSelect(checked, method, cat) {
isPackage: !!method.packageId || !!targetItem.packageName,
packageId: method.packageId || targetItem.packageId || null,
packageName: method.packageName || targetItem.packageName || null, // #428修复: 复制 packageName确保套餐明细可加载
hasChildren: !!(method.packageId || method.packageName || targetItem.packageId || targetItem.packageName) // #426修复: 树形表格懒加载展开标记,支持 packageName 解析
hasChildren: !!(method.packageId || targetItem.packageId) // #426修复: 树形表格懒加载展开标记
};
selectedItems.value.push(newItem);
// 如果是套餐,预加载套餐明细
if (newItem.isPackage && (newItem.packageId || newItem.packageName)) {
if (newItem.isPackage && newItem.packageId) {
loadPackageDetailsForItem(newItem);
}
@@ -1569,7 +1539,7 @@ async function toggleItemExpand(item) {
async function selectMethodCheckbox(checked, item, method) {
if (checked) {
item.selectedMethod = method;
if (item.expanded && (method.packageId || method.packageName)) {
if (item.expanded && method.packageId) {
loadPackageDetailsForItem(item);
}
// 动态加载该方法对应的套餐明细
@@ -1634,9 +1604,8 @@ async function loadMethodPackageDetails(item, method) {
/** 检查明细表格中切换检查方法 */
async function onDetailMethodChange(row, val) {
row.selectedMethod = val || null;
if (val?.packageId || val?.packageName) {
row.packageId = val.packageId || row.packageId;
row.packageName = val.packageName || row.packageName;
if (val?.packageId) {
row.packageId = val.packageId;
row.isPackage = true;
row.hasChildren = true; // #426修复
}
@@ -1834,7 +1803,7 @@ defineExpose({ getList });
flex: 1;
overflow-y: auto;
overflow-x: hidden; /* Bug #500: 防止切换时水平方向溢出导致抖动 */
min-height: 300px; /* Bug #500: 增大最小高度,避免折叠动画期间容器高度突变 */
min-height: 120px; /* Bug #500: 固定最小高度,避免分类切换时 flex 容器高度突变 */
}
.empty-hint {
color: #909399;
@@ -1893,7 +1862,6 @@ defineExpose({ getList });
background: #f0f7ff;
border-radius: 4px;
margin-top: 6px;
min-height: 50px; /* Bug #500: 方法区域预留最小高度,减少加载完成后的高度突变 */
}
.method-section-title {
@@ -1935,24 +1903,6 @@ defineExpose({ getList });
margin-left: 6px;
}
/* Bug #500修复: 方法列表骨架占位样式 */
.method-section-skeleton {
opacity: 0.6;
pointer-events: none;
}
.skeleton-method-row {
height: 22px;
border-radius: 3px;
background: linear-gradient(90deg, #e8f4ff 25%, #d0e8ff 50%, #e8f4ff 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
margin-bottom: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 已选择 tags */
/* 已选择:加宽,避免套餐明细挤成一团 */
.selected-panel {

View File

@@ -917,7 +917,7 @@ const handleViewReport = async (row) => {
reportLoading.value = true;
reportData.value = null;
try {
const res = await getTestResult({ encounterId: row.encounterId || patientInfo.value?.encounterId });
const res = await getTestResult({ prescriptionNo: row.prescriptionNo });
if (res.code === 200) {
if (res.data) {
// 支持两种返回格式:

View File

@@ -471,7 +471,7 @@ const submit = () => {
unitCode: item.unitCode || priceInfo.unitCode || '',
unitPrice: item.price ?? priceInfo.price ?? 0,
totalPrice: item.price ?? priceInfo.price ?? 0,
positionId: form.targetDepartment || item.positionId, // 用户手动选择的发往科室优先于项目默认执行科室
positionId: item.positionId,
ybClassEnum: item.ybClassEnum,
conditionId: item.conditionId,
encounterDiagnosisId: item.encounterDiagnosisId,
@@ -499,16 +499,17 @@ const submit = () => {
categoryEnum: '22',
}).then((res) => {
if (res.code === 200) {
ElMessage.success(res.msg || (props.isEditMode ? '修改成功' : '保存成功'));
if (props.isEditMode) {
proxy.$message.success(res.msg || '修改成功');
} else {
proxy.$message.success(res.msg);
}
applicationList.value = [];
resetForm();
emits('submitOk');
} else {
ElMessage.error(res.msg || '保存失败');
proxy.$message.error(res.message);
}
}).catch((error) => {
console.error('保存检查申请失败:', error);
ElMessage.error('保存失败,请稍后重试');
});
};

View File

@@ -1581,10 +1581,10 @@ function handleSaveGroup(orderGroupList) {
therapyEnum: item.orderDetailInfos?.therapyEnum || '1',
};
// 预初始化空行(组套项带预填值,设为 false 让明细字段在表格中直接展示)
// 预初始化空行
prescriptionList.value[rowIndex.value] = {
uniqueKey: nextId.value++,
isEdit: false,
isEdit: true,
statusEnum: 1,
};

View File

@@ -22,9 +22,9 @@
<el-tab-pane label="检查申请" name="examine">
<ExamineApplication ref="examineApplicationRef" />
</el-tab-pane>
<!-- <el-tab-pane label="汇总发药申请" name="summaryDrug">
<el-tab-pane label="汇总发药申请" name="summaryDrug">
<SummaryDrugApplication ref="summaryDrugApplicationRef" />
</el-tab-pane> -->
</el-tab-pane>
<el-tab-pane label="手术申请" name="surgery">
<SurgeryApplication ref="surgeryApplicationRef" />
</el-tab-pane>
@@ -47,6 +47,7 @@
import {computed, onBeforeMount, onMounted, provide, reactive, ref, watch,} from 'vue';
import Emr from './emr/index.vue';
import SummaryDrugApplication from './components/applicationShow/summaryDrugApplication.vue';
import inPatientBarDoctorFold from '@/components/patientBar/inPatientBarDoctorFold.vue';
import PatientList from '@/components/PatientList/patient-list.vue';
import {localPatientInfo, updateLocalPatientInfo} from './store/localPatient';
@@ -82,6 +83,7 @@ const currentPatientInfo = ref({});
const testApplicationRef = ref();
const examineApplicationRef = ref();
const surgeryApplicationRef = ref();
const summaryDrugApplicationRef = ref();
const bloodTtransfusionAapplicationRef = ref();
// 患者列表相关逻辑

View File

@@ -1144,17 +1144,13 @@ const {
const pendingAnesData = ref(null)
// 监听麻醉字典加载,完成后立即设置表单值
// Bug #433: watch 回调中优先判断字典是否已加载,未加载时跳过(不处理也不清理)
// 确保字典加载完成时 watch 仍然活跃并能处理 pendingAnesData
let anesDataUnwatch = null
function setupAnesDataWatch() {
if (anesDataUnwatch) return // 防止重复设置
anesDataUnwatch = watch(
anesthesiaList,
(newList) => {
// Bug #433: 字典未加载时跳过,不清理 watch等待下次触发
if (!newList || newList.length === 0) return
if (pendingAnesData.value) {
if (newList && newList.length > 0 && pendingAnesData.value) {
const data = pendingAnesData.value
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1353,8 +1349,7 @@ function handleEdit(row) {
if (res.code === 200) {
const data = res.data
Object.assign(form, data)
// Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成
if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1364,8 +1359,6 @@ function handleEdit(row) {
pendingAnesData.value = data
setupAnesDataWatch()
}
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else {
proxy.$modal.msgError('获取手术安排详情失败')
}
@@ -1385,8 +1378,7 @@ function handleView(row) {
if (res.code === 200) {
const data = res.data
Object.assign(form, data)
// Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成
if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1396,8 +1388,6 @@ function handleView(row) {
pendingAnesData.value = data
setupAnesDataWatch()
}
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else {
proxy.$modal.msgError('获取手术安排详情失败')
}