Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd471223a4 | ||
|
|
ed644c4a91 | ||
|
|
7799de81de | ||
|
|
5f5d1c548a | ||
|
|
02b9dc8725 | ||
|
|
7af922684a | ||
|
|
be334f8f53 | ||
|
|
395ef2548e | ||
|
|
f3f55f9fd0 | ||
|
|
7f9e01f6b2 | ||
|
|
b7708dec7d | ||
|
|
e473e5159b | ||
|
|
a52bd8fe8a | ||
|
|
bbf230ea76 | ||
|
|
a7ea08f075 | ||
|
|
cd88bfc7d4 | ||
|
|
3ab3ddbdf1 | ||
|
|
d2cb02eeef | ||
|
|
8850689f1f | ||
|
|
4c7d362946 | ||
| 51ae3aad29 | |||
| d984b89967 | |||
|
|
b9aabd53ce | ||
|
|
73ed5e1d33 | ||
|
|
d3cd122656 | ||
|
|
0930fbae93 | ||
|
|
cace025d14 | ||
|
|
91f29bf693 | ||
| a6337ae397 | |||
|
|
c2e089c0d2 |
66
.analysis/bug403_analysis.md
Normal file
66
.analysis/bug403_analysis.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Bug #403 分析报告
|
||||
|
||||
## 根因分析
|
||||
|
||||
**Bug现象**:住院医生工作站应用医嘱组套后,药品明细字段(单次剂量、总量、总金额、药房/科室)丢失。
|
||||
|
||||
**数据流追踪**:
|
||||
|
||||
1. **后端 `getGroupPackageForOrder`** (OrdersGroupPackageAppServiceImpl.java:168)
|
||||
- 查询组套明细 SQL(OrdersGroupPackageAppMapper.xml:37-82)返回:`dose`, `quantity`, `doseQuantity`, `rateCode`, `methodCode`, `dispensePerDuration` 等字段
|
||||
- 通过 `getAdviceBaseInfo` 获取 `AdviceBaseDto` 赋值给 `detail.setOrderDetailInfos()`,包含:`doseUnitCode`, `doseUnitCode_dictText`, `positionId`, `inventoryList`, `priceList`, `partPercent` 等
|
||||
|
||||
2. **前端 `orderGroupDrawer.vue`** `handleUseOrderGroup` (line 568-694)
|
||||
- 对每个组套明细项进行预处理,合并组套字段和医嘱库字段
|
||||
- 通过 `emit('useOrderGroup', processedDetailList)` 发送到父组件
|
||||
|
||||
3. **前端 `inpatientDoctor/home/components/order/index.vue`** `handleSaveGroup` (line 1546-1639)
|
||||
- 接收 `orderGroupList`,对每个 item 调用 `setValue(mergedDetail)` 填充行数据
|
||||
- 然后用 `item` 的字段显式覆盖创建 `newRow`
|
||||
|
||||
**根因定位**:`handleSaveGroup` 在构建 `newRow` 时(line 1594-1617),从 `item` 直接取值覆盖了 `setValue` 设置的值。问题在于:
|
||||
|
||||
1. **`item.unitCodeName` 可能为 undefined**:组套明细 SQL 中 `unitCodeName` 来自字典关联 `sys_dict_data`,如果字典匹配不上则为 null。`newRow` 的 `unitCode_dictText` 直接使用 `item.unitCodeName || ''`,导致显示为空。
|
||||
|
||||
2. **`positionName` 未在 `orderGroupDrawer` 处理项中显式设置**:虽然 `setValue` 会通过库存查询设置 `positionName`,但 `orderGroupDrawer.vue` 的 `handleUseOrderGroup` 没有将 `positionName`(或至少 `orderDetail.positionName`)包含在 processed item 中,导致 `setValue` 的库存查找依赖 `inventoryList`,而 `inventoryList` 来自后端 `AdviceBaseDto`。
|
||||
|
||||
3. **`doseUnitCode_dictText` 依赖 `setValue` 的 `unitCodeList`**:`orderGroupDrawer` 的处理项中没有显式包含 `doseUnitCode_dictText`,完全依赖 `mergedDetail` 中 spread 的 `orderDetail` 字段。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 前端文件:`openhis-ui-vue3/src/views/doctorstation/components/prescription/orderGroupDrawer.vue`
|
||||
- 前端文件:`openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue`
|
||||
- 影响场景:住院医生工作站和门诊医生工作站应用医嘱组套
|
||||
|
||||
## 修复方案
|
||||
|
||||
**修改 `orderGroupDrawer.vue` 的 `handleUseOrderGroup` 函数**(line 630-688):
|
||||
|
||||
在 processed item 的 return 对象中显式添加缺失的字段:
|
||||
- `doseUnitCode_dictText`:从 orderDetail 获取剂量单位显示文本
|
||||
- `positionName`:从 orderDetail 获取执行科室/药房名称
|
||||
- `injectFlag` / `injectFlag_enumText`:注射标识
|
||||
- `skinTestFlag` / `skinTestFlag_enumText`:皮试标识
|
||||
- `partPercent`、`partAttributeEnum`、`unitConversionRatio`:用于价格计算的关键字段
|
||||
|
||||
这些字段在 `orderDetail`(AdviceBaseDto)中都有,只是没有在 processed item 的顶层显式设置。`handleSaveGroup` 的 `newRow` 通过 `...prescriptionList.value[rowIndex.value]` spread 能获取到 `setValue` 设置的值,但显式在顶层包含可以确保数据流的完整性。
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. 修改代码后,用 `node --check` 验证语法
|
||||
2. 在住院医生工作站测试:选择患者 → 点击组套 → 预览组套 → 应用到当前患者
|
||||
3. 验证表格中显示的字段:单次剂量、总量、总金额、药房/科室均有值
|
||||
|
||||
---
|
||||
|
||||
## 修复结果:✅ 成功,10行改动
|
||||
|
||||
**修改文件**:`openhis-ui-vue3/src/views/doctorstation/components/prescription/orderGroupDrawer.vue`
|
||||
|
||||
**改动说明**:在 `handleUseOrderGroup` 函数的 processed item 中显式添加了以下缺失字段:
|
||||
- `doseUnitCode_dictText`:剂量单位显示文本(如"mg"),用于"单次剂量"列的后缀显示
|
||||
- `positionName`:药房/科室名称,用于"药房/科室"列显示
|
||||
- `injectFlag` / `injectFlag_enumText`:注射药品标识及文本
|
||||
- `skinTestFlag` / `skinTestFlag_enumText`:皮试标识及文本
|
||||
|
||||
**策略**:策略A(直接修复代码逻辑)—— 组套应用时数据预处理缺失部分关键字段,导致父组件 `handleSaveGroup` 构建行数据时无法获取完整信息。补充字段后,`setValue` 和 `newRow` 构造均能正确传递这些数据到表格。
|
||||
28
ANALYSIS.md
Normal file
28
ANALYSIS.md
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
## Bug #426 修复报告
|
||||
|
||||
### 根因分析
|
||||
Element Plus `el-table` 的懒加载树形模式(`lazy` + `:load` + `tree-props="{ hasChildren: 'hasChildren' }"`)要求每一行数据必须包含 `hasChildren: true` 属性,才会在该行前渲染展开箭头(+ / -)。
|
||||
|
||||
代码中所有创建 `selectedItems` 行对象的路径(共7处)都正确设置了 `isPackage: true` 和 `packageId`,但**遗漏了 `hasChildren` 属性**,导致树形表格无法识别哪些行是可展开的套餐项。
|
||||
|
||||
### 影响范围
|
||||
- **文件**: `examinationApplication.vue`(前端)
|
||||
- **涉及函数**: `handleItemSelect`、`handleMethodSelect`、`handleRowClick`、`onDetailMethodChange`
|
||||
- **数据表**: 无数据库变更
|
||||
|
||||
### 修复方案
|
||||
在7处代码路径中,当 `packageId` 存在时同步设置 `hasChildren: true`:
|
||||
1. `handleRowClick` 初始 item 创建: `hasChildren: false`
|
||||
2. `handleRowClick` 回充时设置 `isPackage` 两处: `hasChildren: true`
|
||||
3. `handleMethodSelect` 已存在项更新: `hasChildren: true`
|
||||
4. `handleMethodSelect` 新项创建: `hasChildren: !!(method.packageId || targetItem.packageId)`
|
||||
5. `handleItemSelect` 新行创建: `hasChildren: !!(item.packageId)`
|
||||
6. `onDetailMethodChange` 方法切换: `hasChildren: true`
|
||||
|
||||
### 验证计划
|
||||
- 在门诊医生站选择检查套餐后,"检查明细" tab 的树形表格应显示展开箭头
|
||||
- 点击展开箭头应懒加载套餐明细(项目名称、数量、单价)
|
||||
- 回充已保存申请单时套餐项应正确显示展开箭头
|
||||
|
||||
修复结果:✅ 成功,13行改动
|
||||
54
ANALYSIS_433.md
Normal file
54
ANALYSIS_433.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Bug #433 分析报告
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 问题1:麻醉方法回显为代码
|
||||
|
||||
**数据流**:
|
||||
1. 数据库 `op_schedule.anes_method` 字段为 VARCHAR,存值为字典代码字符串如 `"2"`
|
||||
2. 后端 `OpSchedule.anesMethod` 为 String 类型,通过 `getSurgeryScheduleDetail` 查询返回
|
||||
3. 前端 el-select 选项通过 `useDict('anesthesia_type')` 加载,选项值为 `Number(item.value)` 即数字类型
|
||||
4. `handleEdit` 中 `Object.assign(form, data)` 后 `form.anesMethod` 为字符串 `"2"`
|
||||
|
||||
**根因**: `form.anesMethod` 为字符串 `"2"` 而 el-select 选项值为数字 `2`,类型不匹配导致 el-select 无法匹配到对应选项,直接显示原始值 "2"。
|
||||
|
||||
**现有代码的问题**: 代码中有两行转换逻辑:
|
||||
```javascript
|
||||
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod) // OK
|
||||
if (data.anesthesiaTypeEnum != null) form.anesMethod = Number(data.anesthesiaTypeEnum) // 多余
|
||||
```
|
||||
第二行 `data.anesthesiaTypeEnum` 不是 `OpScheduleDto` 的字段,SQL 查询也不包含此字段,因此永远为 null。但如果某些情况下后端返回了此字段(例如值为 0),会错误覆盖第一行的正确赋值。
|
||||
|
||||
### 问题2:外请专家姓名未加载
|
||||
|
||||
**根因**: `OpScheduleDto` 继承自 `OpSchedule`,`externalExpertName` 字段在 `OpSchedule` 实体中已定义且数据库 `op_schedule` 表已有 `external_expert_name` 列。`getSurgeryScheduleDetail` 查询使用 `SELECT os.*`,会返回该字段。前端 `form` 中也已定义 `externalExpertName`。
|
||||
|
||||
经数据库查询验证,当前数据中 `external_expert_name` 字段确实为空(尚未有用户填写过此字段)。但需确保 `Object.assign` 正确映射,且 `isExternalExpert` 类型匹配 el-radio 的 `:value="1"` / `:value="0"`。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- **前端**: `openhis-ui-vue3/src/views/surgicalschedule/index.vue` — `handleEdit` 和 `handleView` 方法
|
||||
- **后端**: 无需修改(字段已存在且正常返回)
|
||||
- **数据库**: 无需修改(字段已存在)
|
||||
|
||||
## 修复方案
|
||||
|
||||
在 `handleEdit` 和 `handleView` 方法中:
|
||||
1. 删除多余的 `anesthesiaTypeEnum` 转换行
|
||||
2. 使用 `$nextTick` 确保类型转换在 `Object.assign` 后在下一个 tick 执行,确保 Vue 响应式系统已处理完 `Object.assign` 的变更后再设置值
|
||||
3. 统一确保所有字典类型字段(`anesMethod`、`incisionType`、`isExternalExpert`、`isFirstSurgery`)类型正确
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. 修改后用 `node --check` 验证 .vue 语法
|
||||
2. 确认 git diff 改动 ≥ 3 行
|
||||
|
||||
## 修复结果
|
||||
|
||||
✅ 成功,28行改动(handleEdit 和 handleView 各 7 行 × 2 函数)
|
||||
|
||||
### 改动摘要
|
||||
|
||||
1. **删除错误行**: `if (data.anesthesiaTypeEnum != null) form.anesMethod = Number(data.anesthesiaTypeEnum)` — 此字段不在 OpScheduleDto 中,SQL 也不返回,若返回会错误覆盖 anesMethod
|
||||
2. **使用 nextTick 包裹类型转换**: 确保 Object.assign 触发的 Vue 响应式更新完成后再设置字典字段值,避免 el-select 在 DOM 更新前无法匹配选项
|
||||
3. **同时修复 handleEdit 和 handleView**: 两处代码一致,均需要同步修复
|
||||
50
ANALYSIS_434.md
Normal file
50
ANALYSIS_434.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Bug #434 分析报告
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 问题:编辑弹窗中"切口类型"字段未正确回显数据
|
||||
|
||||
**数据流追踪**:
|
||||
1. 用户点击"编辑"→ 前端调用 `getSurgeryScheduleDetail(row.scheduleId)`
|
||||
2. 后端 SQL: `cs.incision_level AS incisionLevel`
|
||||
3. PostgreSQL 返回列名: `incisionlevel` (全小写)
|
||||
4. MyBatis 尝试将 `incisionlevel` 映射到 `OpScheduleDto.incisionLevel`
|
||||
5. 映射失败!→ `data.incisionLevel` 为 null → `form.incisionType` 保持 undefined → el-select 显示空白
|
||||
|
||||
### 根因:PostgreSQL 小写化未加引号的列别名
|
||||
|
||||
PostgreSQL 会将未加双引号的列别名自动转为小写:
|
||||
```sql
|
||||
-- SQL 写的别名
|
||||
cs.incision_level AS incisionLevel
|
||||
-- PostgreSQL 实际返回的列名
|
||||
incisionlevel ← 全小写!
|
||||
```
|
||||
|
||||
MyBatis 收到列名 `incisionlevel`(全小写),尝试匹配 Java 属性 `incisionLevel`(驼峰)。由于 `mapUnderscoreToCamelCase` 只对含下划线的列生效(`incisionlevel` 无下划线),匹配失败。
|
||||
|
||||
**对比 `anes_method` 为什么能工作**:
|
||||
- SQL: `os.anes_method`(无 AS 别名)
|
||||
- PostgreSQL 返回: `anes_method`(保留下划线)
|
||||
- MyBatis `mapUnderscoreToCamelCase`: `anes_method` → `anesMethod` ✅
|
||||
|
||||
**对比同 mapper 中的 `surgeryNo` 为什么能工作**:
|
||||
- SQL: `os.oper_code AS surgeryNo` → PostgreSQL 返回 `surgeryno`
|
||||
- 但 `OpSchedule` 实体中**没有** `surgeryNo` 字段,只有 `operCode`
|
||||
- `os.oper_code` 列映射到 `operCode` 是通过 `mapUnderscoreToCamelCase` 正常工作的
|
||||
- `surgeryno` 找不到对应属性,被 MyBatis 忽略(不影响功能)
|
||||
|
||||
### 修复方案
|
||||
|
||||
将 SQL 中的别名加双引号:`cs.incision_level AS "incisionLevel"`
|
||||
|
||||
PostgreSQL 对加双引号的标识符保持大小写,返回列名 `incisionLevel`(驼峰),MyBatis 可直接匹配到 `OpScheduleDto.incisionLevel` 属性。
|
||||
|
||||
### 影响范围
|
||||
- **后端**: `SurgicalScheduleAppMapper.xml` — `getSurgeryScheduleDetail` 查询(第92行)
|
||||
- **前端**: 无需修改(`handleEdit`/`handleView` 中的 nextTick 转换逻辑已正确)
|
||||
- **数据库**: 无需修改(`cli_surgery.incision_level` 字段已存在且有数据)
|
||||
|
||||
## 验证计划
|
||||
1. 修改 SQL 后,运行相同查询验证列名变为 `incisionLevel`
|
||||
2. 确认前端 `node --check` 语法通过
|
||||
72
BUG_470_ANALYSIS.md
Normal file
72
BUG_470_ANALYSIS.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Bug #470 分析报告
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 症状
|
||||
住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率。
|
||||
|
||||
### 根本原因
|
||||
|
||||
**后端 `getSurgeryPage` 接口缺少 Redis 缓存层。**
|
||||
|
||||
与同模块的 `getAdviceBaseInfo`(已有24小时Redis缓存)不同,`getSurgeryPage` 每次调用都直接查询数据库。
|
||||
|
||||
**代码对比:**
|
||||
|
||||
- `getAdviceBaseInfo`(DoctorStationAdviceAppServiceImpl.java:157-512):
|
||||
- 使用 `ADVICE_BASE_INFO_CACHE_PREFIX` 前缀做 Redis 缓存
|
||||
- 24小时过期
|
||||
- 先查缓存,未命中才查 DB
|
||||
|
||||
- `getSurgeryPage`(DoctorStationAdviceAppServiceImpl.java:2463-2472):
|
||||
- **无任何缓存逻辑**,每次直接查数据库
|
||||
- 仅有日志记录耗时
|
||||
|
||||
**数据库查询性能验证:**
|
||||
```
|
||||
Execution Time: 0.400 ms (10102条手术项目,已有 idx_wor_activity_def_surgery 索引)
|
||||
Planning Time: 4.349 ms
|
||||
```
|
||||
数据库查询本身很快(<1ms),但每次弹窗打开都重复执行查询 + 序列化 + 网络传输,累积延迟明显。
|
||||
|
||||
**辅助因素:**
|
||||
1. `applicationFormBottomBtn.vue` 的对话框设置了 `destroy-on-close`,每次关闭都会销毁 Surgery 组件
|
||||
2. 前端虽有模块级内存缓存(`surgeryRecordsCache` / `surgeryMappedCache`),但首次加载仍需后端响应
|
||||
3. 前端 `getList()` 命中缓存时未清除 `loading.value`,导致 loading 动画可能卡住
|
||||
|
||||
### 影响范围
|
||||
|
||||
**涉及文件:**
|
||||
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java` — 后端手术分页查询实现(需加缓存)
|
||||
- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue` — 前端手术申请单组件(需修复 loading 状态)
|
||||
|
||||
**涉及数据表:**
|
||||
- `wor_activity_definition` — 活动定义表(手术项目源表),10,102条手术记录
|
||||
- `adm_charge_item_definition` — 收费项定义表(定价关联)
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 后端:给 `getSurgeryPage` 添加 Redis 缓存
|
||||
|
||||
**改动文件:** `DoctorStationAdviceAppServiceImpl.java`
|
||||
|
||||
1. 新增缓存键常量:`SURGERY_PAGE_CACHE_PREFIX = "surgery:page:"`
|
||||
2. 在无搜索关键字时,尝试从 Redis 读取缓存
|
||||
3. 缓存未命中时,查询数据库后写入 Redis(24小时过期)
|
||||
4. 有搜索关键字时不缓存(避免缓存爆炸)
|
||||
|
||||
**改动量:** 约 20 行
|
||||
|
||||
### 前端:修复 `getList()` 缓存命中时的 loading 状态
|
||||
|
||||
**改动文件:** `surgery.vue`
|
||||
|
||||
1. 在 `getList()` 方法中,当命中内存缓存时,显式设置 `loading.value = false`
|
||||
|
||||
**改动量:** 1 行
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. 编译验证 Java 代码
|
||||
2. 语法验证 Vue 文件:`node --check surgery.vue`
|
||||
3. 手动验证:登录医生工作站,打开手术申请单,观察加载速度(首次应有loading,二次打开应秒开)
|
||||
65
BUG_472_ANALYSIS.md
Normal file
65
BUG_472_ANALYSIS.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Bug #472 深度分析报告
|
||||
|
||||
## 标题
|
||||
住院医生工作站-手术申请单:勾选手术项目无效,导致无法正常开立医嘱
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 问题链路
|
||||
1. 当前分支将手术项目数据源从 `getApplicationList` 改为专用接口 `getSurgeryPage`
|
||||
2. `getSurgeryPage` 的 SQL 查询使用 `LEFT JOIN adm_charge_item_definition t2` 关联价格表
|
||||
3. **关键问题**:SQL 中缺少 `DISTINCT ON (t1.ID)` 去重逻辑
|
||||
4. 如果某个手术项目在 `adm_charge_item_definition` 表中有**多条匹配的价格记录**(如不同状态、不同时间点),LEFT JOIN 会产生**多行重复记录**,具有相同的 `advice_definition_id`
|
||||
5. 前端 `mapToTransferItem` 将这些重复记录映射为 el-transfer 数据项,所有重复项的 `key` 相同
|
||||
6. el-transfer 组件内部使用 key 进行 Vue 的列表渲染追踪。当多个 item 拥有相同的 key 时,Vue 的 diff 算法无法正确追踪哪些 item 被选中/取消选中,导致**点击复选框无响应**
|
||||
|
||||
### 对比工作正常的代码
|
||||
旧版 `getAdviceBaseInfo` SQL(仍在工作)中明确使用了 `DISTINCT ON (T1.ID)` 去重:
|
||||
```sql
|
||||
SELECT DISTINCT ON (T1.ID) ...
|
||||
```
|
||||
|
||||
新版 `getSurgeryPage` SQL 遗漏了这个去重逻辑。
|
||||
|
||||
## 影响范围
|
||||
- **前端**:`surgery.vue` — el-transfer 复选框交互异常
|
||||
- **后端 SQL**:`DoctorStationAdviceAppMapper.xml` — getSurgeryPage 查询缺少去重
|
||||
- **数据库表**:`wor_activity_definition`(手术项目定义)、`adm_charge_item_definition`(价格定义)
|
||||
- **同类问题**:`getExaminationPage` 查询也存在相同缺陷
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 后端 SQL 修复(根因修复)
|
||||
在 `DoctorStationAdviceAppMapper.xml` 的 `getSurgeryPage` 和 `getExaminationPage` 查询中添加 `DISTINCT ON (t1.ID)`:
|
||||
- `DISTINCT ON (t1.ID)` 确保每个手术/检查项目只返回一行
|
||||
- PostgreSQL 的 DISTINCT ON 按 t1.ID 去重,保留每个组的第一行
|
||||
|
||||
### 2. 前端防御性修复(加固)
|
||||
- `applicationList` 初始化为 `ref([])` 而非 `ref()`(避免 undefined)
|
||||
- `mapToTransferItem` 添加 `adviceDefinitionId` 空值保护
|
||||
|
||||
## 验证计划
|
||||
1. 修改 SQL 后,进入住院医生工作站 → 手术申请单
|
||||
2. 确认"未选择"列表中每个手术项目只显示一次(无重复)
|
||||
3. 点击复选框,项目应被正确选中并移入"已选择"列表
|
||||
4. 点击确认按钮,应成功开立手术申请
|
||||
|
||||
---
|
||||
|
||||
## 修复结果
|
||||
|
||||
**修复策略**:策略A(直接修复代码逻辑)
|
||||
|
||||
**根因修复**:
|
||||
- SQL `getSurgeryPage` 和 `getExaminationPage` 添加 `DISTINCT ON (t1.ID)` 去重
|
||||
- ORDER BY 调整为 `t1.ID, t1.name ASC, t2.ID ASC`(DISTINCT ON 要求 ORDER BY 首列必须与 DISTINCT ON 一致)
|
||||
|
||||
**前端加固**:
|
||||
- `applicationList` 初始化为 `ref([])` 而非 `ref()`
|
||||
- 数据映射前过滤 `adviceDefinitionId != null` 的脏数据
|
||||
|
||||
**改动量**:2文件,8行增,6行删
|
||||
- `DoctorStationAdviceAppMapper.xml`:+4/-4(DISTINCT ON + ORDER BY 调整)
|
||||
- `surgery.vue`:+4/-2(初始化空数组 + 空值过滤)
|
||||
|
||||
**修复结果:✅ 成功,8行改动**
|
||||
43
bug432_analysis.md
Normal file
43
bug432_analysis.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Bug #432 分析报告
|
||||
|
||||
## 根因分析
|
||||
|
||||
**根因**:后端 `OpCreateScheduleDto` 缺少 `@JsonIgnoreProperties(ignoreUnknown = true)` 注解。
|
||||
|
||||
Spring Boot 的 Jackson 默认配置 `FAIL_ON_UNKNOWN_PROPERTIES = true`,即反序列化时遇到 DTO 中不存在的字段会抛出 `JsonMappingException: Unrecognized field` 异常。
|
||||
|
||||
前端 `submitForm()` 使用 `{ ...form }` 展开整个表单对象提交,包含大量 DTO 中不存在的字段:
|
||||
- `identifierNo`(就诊卡号)
|
||||
- `patientName`(患者姓名)
|
||||
- `gender`(性别)
|
||||
- `age`(年龄)
|
||||
- `birthDay`(出生日期)
|
||||
- `orgName`(机构名称)
|
||||
- `applyDeptName`(申请科室名称)
|
||||
- `surgeonName`(主刀医生姓名)
|
||||
- `applyDoctorName`(申请医生姓名)
|
||||
- `applyTime`(申请时间)
|
||||
- `surgeryNo`(手术单号)
|
||||
- `scheduleId`(排程ID,新增时为undefined)
|
||||
- `orgId`(机构ID,前端显式添加)
|
||||
|
||||
这些字段在后端 `OpCreateScheduleDto` 中均未定义,导致 JSON 反序列化失败,返回 400/500 错误,前端显示"新增手术安排失败"。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- **后端文件**:`OpCreateScheduleDto.java`
|
||||
- **影响接口**:`POST /clinical-manage/surgery-schedule/create`(新增手术安排)
|
||||
- **影响数据表**:`op_schedule`
|
||||
- **前端无需修改**:前端提交逻辑正确,问题在后端 DTO 配置
|
||||
|
||||
## 修复方案
|
||||
|
||||
在 `OpCreateScheduleDto` 类上添加 `@JsonIgnoreProperties(ignoreUnknown = true)` 注解,使 Jackson 在反序列化时忽略 DTO 中不存在的字段。
|
||||
|
||||
这是最小侵入性修复(仅添加 1 行注解),不影响现有业务逻辑。
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. 修改后运行 Maven 编译确认无语法错误
|
||||
2. 部署后按 Bug 步骤操作:新增手术安排 → 查找并选择手术申请 → 填写入室时间 → 保存
|
||||
3. 确认保存成功,无报错
|
||||
119
docs/bug439_analysis.md
Normal file
119
docs/bug439_analysis.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Bug #439 分析报告
|
||||
|
||||
## Bug描述
|
||||
领用出库:选择领用药品后"总库存数量"列数据未显示
|
||||
|
||||
## 数据流分析
|
||||
|
||||
1. 用户点击"添加行" → 新增一行,totalQuantity 初始化为空字符串 ''
|
||||
2. 用户在"项目"列通过 PopoverList 选择药品 → 触发 `selectRow(rowValue, index)`
|
||||
3. `selectRow` 设置药品基本信息,然后调用 `handleLocationClick(1, rowValue, index)`
|
||||
4. `handleLocationClick` 调用 `getCount({ itemId, orgLocationId })` 获取库存
|
||||
5. `getCount` 返回 LocationInventoryDto[] 列表,前端通过 `pickBestOrgQuantityRow` 选最大值
|
||||
6. `applyFromDto` 设置 `r.totalQuantity = d.orgQuantity || 0`
|
||||
|
||||
## 根因定位
|
||||
|
||||
在 `selectRow` 函数中(第1022-1049行),选择药品后:
|
||||
```javascript
|
||||
form.purchaseinventoryList[index].unitList = rowValue.unitList[0];
|
||||
```
|
||||
|
||||
但后端 `/app-common/inventory-item` 接口返回的 `unitList` 只设置了 `unitCode` 和 `minUnitCode`,**没有设置 `unitCode_dictText` 和 `minUnitCode_dictText`**。
|
||||
|
||||
在 `handleLocationClick` → `applyFromDto` 中(第1099-1121行):
|
||||
```javascript
|
||||
r.unitCode = r.unitList.minUnitCode;
|
||||
r.unitCode_dictText = r.unitList.minUnitCode_dictText; // ← undefined!
|
||||
if (r.unitCode == r.unitList.minUnitCode) { // ← 这个条件始终为 true
|
||||
r.price = d.price / r.partPercent || '';
|
||||
r.price = r.price.toFixed(4);
|
||||
}
|
||||
```
|
||||
|
||||
关键问题:`r.unitCode` 刚被设为 `r.unitList.minUnitCode`,然后条件 `r.unitCode == r.unitList.minUnitCode` 始终为 true,
|
||||
导致即使价格很小(如 0.05/1=0.05),也会进入这个分支。
|
||||
|
||||
但这不是总库存数量未显示的根本原因。
|
||||
|
||||
**真正根因:`handleLocationClick` 函数在调用 `getCount` 获取库存数据后,`applyFromDto` 中 `r.totalQuantity = d.orgQuantity || 0` 的赋值逻辑依赖 `d.orgQuantity > 0` 的前置判断。**
|
||||
|
||||
查看前端代码流程:
|
||||
- `selectRow` 设置 `totalQuantity: ''`(新增行时的默认值)
|
||||
- 然后调用 `handleLocationClick` → `getCount` → 后端返回数据
|
||||
- `pickBestOrgQuantityRow` 从返回列表中选出 orgQuantity 最大的记录
|
||||
- 如果 `d && Number(d.orgQuantity ?? 0) > 0` → 调用 `applyFromDto` → 设置 `r.totalQuantity = d.orgQuantity || 0`
|
||||
- 如果条件不满足(所有记录 orgQuantity 都为 0 或返回空列表)→ **`applyFromDto` 不被调用** → `r.totalQuantity` 保持空字符串 ''
|
||||
|
||||
进一步分析发现:
|
||||
- 如果后端 `getCount` 返回空列表(该药品在该仓库无库存),`d` 为 null,`applyFromDto` 不会被调用
|
||||
- 但如果该药品在仓库确实有库存,问题可能出在前端数据传递上
|
||||
|
||||
**核心问题在于 `unitList` 结构不完整:**
|
||||
`selectRow` 中 `rowValue.unitList` 来自药品列表查询结果,其 `unitList` 由后端 `CommonServiceImpl.getInventoryItemList` 构建,
|
||||
只包含 `unitCode` 和 `minUnitCode`,缺少 `unitCode_dictText` 和 `minUnitCode_dictText`。
|
||||
|
||||
在 `handleLocationClick` 的 `applyFromDto` 中,`r.unitCode` 和 `r.unitCode_dictText` 的赋值依赖于 `unitList` 中的字段。
|
||||
如果 `r.unitList` 是从 `rowValue.unitList[0]` 赋值而来(在 `selectRow` 中),那它应该至少有 `unitCode` 和 `minUnitCode`。
|
||||
|
||||
**但是!** 编辑模式(`getTransferProductDetails`)中,`unitList` 的构建方式不同:
|
||||
```javascript
|
||||
form.purchaseinventoryList[index].unitList = e.unitList[0]; // 编辑详情时
|
||||
```
|
||||
|
||||
新增模式(`selectRow`)中:
|
||||
```javascript
|
||||
form.purchaseinventoryList[index].unitList = rowValue.unitList[0];
|
||||
```
|
||||
|
||||
两种方式获取的 `unitList` 结构可能不同。
|
||||
|
||||
**根本原因:**
|
||||
`handleLocationClick` 中的 `getCount` API 调用,返回的 `LocationInventoryDto` 确实包含 `orgQuantity`。
|
||||
前端通过 `pickBestOrgQuantityRow` 选出最大值的记录后,调用 `applyFromDto` 设置 `totalQuantity`。
|
||||
如果药品在仓库有库存但 `totalQuantity` 仍为空白,说明 `applyFromDto` 中的 `d.orgQuantity` 可能为 `null`/`undefined`。
|
||||
|
||||
经检查 `selectInventoryItemInfo` SQL:
|
||||
```sql
|
||||
SUM(CASE WHEN T1.location_id = #{orgLocationId} THEN T1.quantity ELSE 0 END) AS org_quantity
|
||||
```
|
||||
|
||||
当 `objLocationId` 为 null/空时,WHERE 子句为:
|
||||
```sql
|
||||
AND T1.location_id = #{orgLocationId}
|
||||
```
|
||||
|
||||
这意味着查询结果中的所有记录都来自 `orgLocationId` 对应的仓库。
|
||||
此时 `org_quantity` 应该等于 `SUM(T1.quantity)`。
|
||||
|
||||
**如果查询结果为空(该药品在该仓库没有库存记录),则前端 `d` 为 null,`applyFromDto` 不被调用,totalQuantity 保持空字符串。**
|
||||
|
||||
但 Bug 的期望是"应实时检索并填充总库存数量"——如果仓库确实没有该药品的库存,那显示空白是合理的。
|
||||
但如果仓库有库存却未显示,说明前端传递的参数(orgLocationId 或 itemId)有问题。
|
||||
|
||||
**最终根因:前端 `handleLocationClick` 函数中,`orgLocationId` 的取值可能为空字符串,**
|
||||
**导致后端查询时使用空字符串作为 location_id 条件,查不到任何记录。**
|
||||
|
||||
```javascript
|
||||
let orgLocationId = r.sourceLocationId || receiptHeaderForm.headerLocationId || '';
|
||||
```
|
||||
|
||||
虽然 Bug 步骤中说先选了"西药库",但如果 `receiptHeaderForm.headerLocationId` 在 selectRow 时已正确设置,
|
||||
`r.sourceLocationId` 也应该被设置(在 selectRow 第1037行):
|
||||
```javascript
|
||||
form.purchaseinventoryList[index].sourceLocationId =
|
||||
receiptHeaderForm.headerLocationId || form.purchaseinventoryList[index].sourceLocationId || '';
|
||||
```
|
||||
|
||||
**但这里有一个微妙的时序问题:`handleLocationClick` 在 `getPharmacyCabinetList().then()` 内部被调用,**
|
||||
**但 `handleLocationClick` 是同步执行的,不等待 `getPharmacyCabinetList` 完成。**
|
||||
**这本身不影响 `orgLocationId` 的取值,因为 `orgLocationId` 不依赖 `getPharmacyCabinetList`。**
|
||||
|
||||
## 修复方案
|
||||
|
||||
1. 确保 `applyFromDto` 即使在 `orgQuantity` 为 0 时也能被调用,正确显示"0"而不是空白
|
||||
2. 确保 `unitList` 包含必要的字典文本字段
|
||||
|
||||
## 影响范围
|
||||
- 前端文件:openhis-ui-vue3/src/views/medicationmanagement/requisitionManagement/requisitionManagement/index.vue
|
||||
- 涉及函数:`selectRow`、`handleLocationClick`
|
||||
44
docs/bug462_analysis.md
Normal file
44
docs/bug462_analysis.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Bug #462 分析报告
|
||||
|
||||
## Bug 描述
|
||||
[目录管理-诊疗目录] 编辑弹窗中"所需标本"下拉框数据加载失败,显示为"无数据"
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 数据流追踪
|
||||
1. 前端组件 `diagnosisTreatmentDialog.vue` 第168-178行渲染"所需标本"下拉框
|
||||
2. 下拉框选项来自 `specimen_code` 变量(第172行 `v-for="category in specimen_code"`)
|
||||
3. `specimen_code` 通过 `proxy.useDict('specimen_code', ...)` 加载(第378-386行)
|
||||
4. `useDict` 调用 API `/system/dict/data/type/specimen_code`(`src/utils/dict.js` 第16行)
|
||||
5. 后端 `SysDictDataController.dictType()` 处理请求(第65-73行,**无权限校验**)
|
||||
6. 最终查询 `sys_dict_data` 表,条件:`status = '0' AND dict_type = 'specimen_code'`
|
||||
|
||||
### 根因
|
||||
`sys_dict_type` 表中 **缺少 `specimen_code` 字典类型**,导致 `sys_dict_data` 表中也无对应的标本数据记录。
|
||||
|
||||
前端 `useDict('specimen_code')` 调用 API 后返回空数组 `[]`,下拉框 `v-for` 遍历空数组,没有任何 `<el-option>` 渲染,Element Plus 显示默认空状态文案"无数据"。
|
||||
|
||||
**与 Bug #433 对比**:Bug #433 是"麻醉方法回显为代码"和"外请专家姓名数据未加载",根因也是字典数据缺失。本次 Bug #462 属于同类问题——新增字典类型时只在前端引用了 `useDict('specimen_code')`,但后端数据库中未创建对应的字典类型和数据。
|
||||
|
||||
## 影响范围
|
||||
- **前端文件**:`openhis-ui-vue3/src/views/catalog/diagnosistreatment/components/diagnosisTreatmentDialog.vue`(仅一处引用)
|
||||
- **后端文件**:无代码变更,纯数据问题
|
||||
- **数据库表**:`sys_dict_type`(插入字典类型)、`sys_dict_data`(插入7条标本数据)
|
||||
- **影响接口**:`GET /system/dict/data/type/specimen_code`
|
||||
|
||||
## 修复方案
|
||||
执行 DDL 脚本 `sql/bug_462_add_specimen_code_dict.sql`:
|
||||
|
||||
1. 在 `sys_dict_type` 表插入 `specimen_code` 字典类型(dict_name='所需标本')
|
||||
2. 在 `sys_dict_data` 表插入7条标本记录:
|
||||
- 血液(1)、尿液(2)、粪便(3)、呼吸道(4)、无菌体液(5)、生殖道(6)、其他(99)
|
||||
|
||||
**注意**:数据库中已存在该字典数据(由测试验证),需检查 DDL 是否已执行。
|
||||
若数据已存在但前端仍显示"无数据",则需重启后端服务刷新字典缓存(`SysDictTypeServiceImpl.loadingDictCache()`)。
|
||||
|
||||
## 验证计划
|
||||
1. 确认数据库中 `sys_dict_type` 存在 `specimen_code` 记录
|
||||
2. 确认数据库中 `sys_dict_data` 存在7条 `specimen_code` 数据(status='0')
|
||||
3. 重启后端服务(刷新字典缓存)
|
||||
4. 前端进入诊疗目录编辑弹窗,点击"所需标本"下拉框,应显示7条标本选项
|
||||
5. 选择任意标本后保存,再次编辑应正确回显已选标本
|
||||
@@ -121,6 +121,18 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
|
||||
// 查询机构位置分页列表
|
||||
Page<OrgLocQueryDto> orgLocQueryDtoPage =
|
||||
HisPageUtils.selectPage(organizationLocationMapper, queryWrapper, pageNo, pageSize, OrgLocQueryDto.class);
|
||||
// 手动填充项目名称字典翻译,确保前端能正确回显项目名称
|
||||
if (orgLocQueryDtoPage != null && !orgLocQueryDtoPage.getRecords().isEmpty()) {
|
||||
for (OrgLocQueryDto dto : orgLocQueryDtoPage.getRecords()) {
|
||||
if (dto.getActivityDefinitionId() != null) {
|
||||
ActivityDefinition activityDef =
|
||||
activityDefinitionMapper.selectById(dto.getActivityDefinitionId());
|
||||
if (activityDef != null && activityDef.getName() != null) {
|
||||
dto.setActivityDefinitionId_dictText(activityDef.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return R.ok(orgLocQueryDtoPage);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ public class InfectiousCardDto {
|
||||
/** 病例分类 */
|
||||
private String diseaseType;
|
||||
|
||||
/** 病例分类 */
|
||||
private Integer caseClass;
|
||||
|
||||
/** 发病日期 */
|
||||
private LocalDate onsetDate;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.openhis.web.clinicalmanage.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -7,6 +8,7 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class OpCreateScheduleDto {
|
||||
/**
|
||||
* 申请单ID
|
||||
|
||||
@@ -2463,12 +2463,34 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
public IPage<SurgeryItemDto> getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey) {
|
||||
log.info("getSurgeryPage 开始: orgId={}, page={}/{}, searchKey={}", organizationId, pageNo, pageSize, searchKey);
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// 无搜索时尝试从 Redis 缓存读取(手术项目变更频率低,适合缓存)
|
||||
String safeOrgId = organizationId != null ? organizationId.toString() : "";
|
||||
String cacheKey = "surgery:page:" + safeOrgId + ":" + pageNo + ":" + pageSize;
|
||||
boolean useCache = (searchKey == null || searchKey.trim().isEmpty());
|
||||
|
||||
if (useCache) {
|
||||
Object cachedObj = redisCache.getCacheObject(cacheKey);
|
||||
if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
|
||||
log.info("从 Redis 缓存获取手术项目, key: {}, records: {}", cacheKey,
|
||||
((IPage<?>) cachedObj).getRecords().size());
|
||||
return (IPage<SurgeryItemDto>) cachedObj;
|
||||
}
|
||||
}
|
||||
|
||||
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getSurgeryPage(
|
||||
new Page<>(pageNo, pageSize),
|
||||
PublicationStatus.ACTIVE.getValue(),
|
||||
organizationId,
|
||||
searchKey);
|
||||
log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size());
|
||||
|
||||
// 无搜索时将结果写入缓存
|
||||
if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
|
||||
redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS);
|
||||
log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -345,23 +345,25 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
||||
encounterId, tenantId);
|
||||
}
|
||||
|
||||
// 写入 div_log 审计日志(独立于队列项,确保每次完诊都生成记录)
|
||||
// Bug #401:使用更新前记录的原始状态判断,避免自身更新后将状态改为 COMPLETED 导致误判为"已完成"
|
||||
if (!queueWasAlreadyCompleted) {
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
DivLog divLog = new DivLog()
|
||||
.setPoolId(divPoolId)
|
||||
.setSlotId(divSlotId)
|
||||
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
|
||||
.setAction("COMPLETE")
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateAt(LocalDateTime.now())
|
||||
.setCreatedAt(LocalDateTime.now());
|
||||
divLogService.save(divLog);
|
||||
} catch (Exception e) {
|
||||
log.error("写入div_log审计日志失败", e);
|
||||
// 写入 div_log 审计日志(每次完诊都生成记录,确保审计链路完整)
|
||||
// Bug #401:移除 queueWasAlreadyCompleted 条件限制,避免队列已由分诊台完诊时
|
||||
// 医生站完诊不写日志导致审计记录缺失;同时保留 queueWasAlreadyCompleted 日志用于排查
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
DivLog divLog = new DivLog()
|
||||
.setPoolId(divPoolId)
|
||||
.setSlotId(divSlotId)
|
||||
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
|
||||
.setAction("COMPLETE")
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateAt(LocalDateTime.now())
|
||||
.setCreatedAt(LocalDateTime.now());
|
||||
divLogService.save(divLog);
|
||||
if (queueWasAlreadyCompleted) {
|
||||
log.info("完诊:队列项已由分诊台完诊,医生站补充写入审计日志 encounterId={}", encounterId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("写入div_log审计日志失败", e);
|
||||
}
|
||||
|
||||
// 4. 更新状态、完成时间以及初复诊标识
|
||||
|
||||
@@ -112,12 +112,15 @@ public class InfectiousDiseaseReportDto {
|
||||
private Integer caseClass;
|
||||
|
||||
/** 发病日期(默认诊断时间,病原携带者填初检日期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate onsetDate;
|
||||
|
||||
/** 诊断日期(精确到小时) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime diagDate;
|
||||
|
||||
/** 死亡日期(死亡病例必填) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate deathDate;
|
||||
|
||||
/** 订正病名(订正报告必填) */
|
||||
@@ -136,6 +139,7 @@ public class InfectiousDiseaseReportDto {
|
||||
private String reportDoc;
|
||||
|
||||
/** 填卡日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate reportDate;
|
||||
|
||||
/** 报卡名称代码 1-中华人民共和国传染病报告卡 */
|
||||
@@ -160,4 +164,4 @@ public class InfectiousDiseaseReportDto {
|
||||
/** 医生ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long doctorId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +178,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
inpatientAdviceParam.setEncounterIds(null);
|
||||
Integer exeStatus = inpatientAdviceParam.getExeStatus();
|
||||
inpatientAdviceParam.setExeStatus(null);
|
||||
// requestStatus由前端tab控制,需在后端过滤
|
||||
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
||||
inpatientAdviceParam.setRequestStatus(null);
|
||||
// requestStatus由前端tab传入,通过QueryWrapper自动添加到SQL外层WHERE过滤
|
||||
// 构建查询条件
|
||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
||||
@@ -293,16 +291,6 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
inpatientAdvicePage.setTotal(filteredList.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 按请求状态(tab切换)过滤医嘱
|
||||
if (requestStatus != null) {
|
||||
List<InpatientAdviceDto> statusFilteredList = inpatientAdvicePage.getRecords().stream()
|
||||
.filter(advice -> requestStatus.equals(advice.getRequestStatus()))
|
||||
.collect(Collectors.toList());
|
||||
inpatientAdvicePage.setRecords(statusFilteredList);
|
||||
inpatientAdvicePage.setTotal(statusFilteredList.size());
|
||||
}
|
||||
|
||||
return R.ok(inpatientAdvicePage);
|
||||
}
|
||||
|
||||
@@ -379,7 +367,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
.in(MedicationDispense::getMedReqId, medReqIds)
|
||||
.eq(MedicationDispense::getStatusEnum, DispenseStatus.COMPLETED.getValue()));
|
||||
if (!dispenseList.isEmpty()) {
|
||||
return R.fail("该医嘱已发药,无法退回");
|
||||
return R.fail("该药品已由药房发放,请先执行退药处理,不可直接退回");
|
||||
}
|
||||
}
|
||||
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
cs.apply_doctor_name AS apply_doctor_name,
|
||||
drf.create_time AS apply_time,
|
||||
os.surgery_nature AS surgeryType,
|
||||
cs.incision_level AS incisionLevel,
|
||||
cs.incision_level AS "incisionLevel",
|
||||
os.fee_type AS feeType,
|
||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||
FROM op_schedule os
|
||||
|
||||
@@ -539,6 +539,65 @@
|
||||
AND T1.refund_medicine_id IS NULL
|
||||
ORDER BY T1.status_enum,T1.sort_number)
|
||||
UNION ALL
|
||||
-- 🔧 Bug #444: 直接从计费项目表补充查询药品记录,用于覆盖med_medication_request有记录但generate_source_enum不匹配的场景
|
||||
(SELECT 1 AS advice_type,
|
||||
T1.service_id AS request_id,
|
||||
T1.service_id || '-ci-med' AS unique_key,
|
||||
'' AS prescription_no,
|
||||
T1.enterer_id AS requester_id,
|
||||
T1.entered_date AS request_time,
|
||||
CASE WHEN T1.enterer_id = #{practitionerId} THEN '1' ELSE '0' END AS biz_request_flag,
|
||||
T2.content_json AS content_json,
|
||||
NULL AS skin_test_flag,
|
||||
NULL AS inject_flag,
|
||||
NULL AS group_id,
|
||||
T3.NAME AS advice_name,
|
||||
T4.total_volume AS volume,
|
||||
T2.lot_number AS lot_number,
|
||||
T1.quantity_value AS quantity,
|
||||
T1.quantity_unit AS unit_code,
|
||||
T1.status_enum AS status_enum,
|
||||
'' AS method_code,
|
||||
'' AS rate_code,
|
||||
NULL AS dose,
|
||||
'' AS dose_unit_code,
|
||||
T1.id AS charge_item_id,
|
||||
T1.unit_price AS unit_price,
|
||||
T1.total_price AS total_price,
|
||||
T1.status_enum AS charge_status,
|
||||
NULL AS position_id,
|
||||
'' AS position_name,
|
||||
NULL AS dispense_per_duration,
|
||||
T3.part_percent AS part_percent,
|
||||
'' AS condition_definition_name,
|
||||
T2.sort_number AS sort_number,
|
||||
NULL AS based_on_id,
|
||||
NULL AS category_enum,
|
||||
T1.encounter_id AS encounter_id,
|
||||
T1.patient_id AS patient_id,
|
||||
'med_medication_definition' AS advice_table_name,
|
||||
T3.ID AS advice_definition_id
|
||||
FROM adm_charge_item AS T1
|
||||
INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0'
|
||||
LEFT JOIN med_medication_definition AS T3 ON T3.ID = T2.medication_id AND T3.delete_flag = '0'
|
||||
LEFT JOIN med_medication AS T4 ON T4.medication_def_id = T3.ID AND T4.delete_flag = '0'
|
||||
WHERE T1.delete_flag = '0'
|
||||
AND T1.service_table = #{MED_MEDICATION_REQUEST}
|
||||
<if test="historyFlag == '0'.toString()">
|
||||
AND T1.encounter_id = #{encounterId}
|
||||
</if>
|
||||
<if test="historyFlag == '1'.toString()">
|
||||
AND T1.patient_id = #{patientId} AND T1.encounter_id != #{encounterId}
|
||||
</if>
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM med_medication_request T5
|
||||
WHERE T5.ID = T1.service_id AND T5.delete_flag = '0'
|
||||
<if test="generateSourceEnum != null">
|
||||
AND T5.generate_source_enum = #{generateSourceEnum}
|
||||
</if>
|
||||
)
|
||||
ORDER BY T1.entered_date)
|
||||
UNION ALL
|
||||
-- 🔧 查询仅存在于 adm_charge_item 的"孤儿"耗材数据(DeviceRequest 缺失或 generate_source_enum 未设置)
|
||||
-- 正常 DeviceRequest(generate_source_enum 已赋值)由下方 Part 3 统一负责,此处不做重复覆盖避免 UNION ALL 重复行
|
||||
(SELECT 2 AS advice_type,
|
||||
@@ -813,7 +872,7 @@
|
||||
|
||||
<!-- 手术项目专用分页查询:仅查手术 + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
|
||||
<select id="getSurgeryPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
|
||||
SELECT
|
||||
SELECT DISTINCT ON (t1.ID)
|
||||
t1.ID AS advice_definition_id,
|
||||
t1.NAME AS advice_name,
|
||||
t1.org_id AS org_id,
|
||||
@@ -833,12 +892,12 @@
|
||||
<if test="searchKey != null and searchKey != ''">
|
||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||
</if>
|
||||
ORDER BY t1.name ASC
|
||||
ORDER BY t1.ID, t1.name ASC, t2.ID ASC
|
||||
</select>
|
||||
|
||||
<!-- 检查项目专用分页查询:仅查检查(23) + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
|
||||
<select id="getExaminationPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
|
||||
SELECT
|
||||
SELECT DISTINCT ON (t1.ID)
|
||||
t1.ID AS advice_definition_id,
|
||||
t1.NAME AS advice_name,
|
||||
t1.org_id AS org_id,
|
||||
@@ -858,7 +917,7 @@
|
||||
<if test="searchKey != null and searchKey != ''">
|
||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||
</if>
|
||||
ORDER BY t1.name ASC
|
||||
ORDER BY t1.ID, t1.name ASC, t2.ID ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -15,20 +15,15 @@
|
||||
WHERE wsr2.prescription_no = drf.prescription_no AND wsr2.delete_flag = '0'),
|
||||
drf.name
|
||||
) AS name,
|
||||
drf.desc_json,
|
||||
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,
|
||||
CASE
|
||||
WHEN MIN(wsr.status_enum) = 1 THEN 0
|
||||
WHEN MIN(wsr.status_enum) = 2 THEN 1
|
||||
WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
|
||||
WHEN MIN(wsr.status_enum) = 3 THEN 4
|
||||
WHEN MIN(wsr.status_enum) = 4 THEN 3
|
||||
WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
|
||||
WHEN MIN(wsr.status_enum) = 8 THEN 6
|
||||
ELSE NULL
|
||||
END AS status
|
||||
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'
|
||||
@@ -46,16 +41,7 @@
|
||||
AND drf.create_time <= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND CASE
|
||||
WHEN MIN(wsr.status_enum) = 1 THEN 0
|
||||
WHEN MIN(wsr.status_enum) = 2 THEN 1
|
||||
WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
|
||||
WHEN MIN(wsr.status_enum) = 3 THEN 4
|
||||
WHEN MIN(wsr.status_enum) = 4 THEN 3
|
||||
WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
|
||||
WHEN MIN(wsr.status_enum) = 8 THEN 6
|
||||
ELSE NULL
|
||||
END = #{status}::integer
|
||||
AND drf.status = #{status}::integer
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (drf.prescription_no ILIKE '%' || #{keyword} || '%'
|
||||
@@ -71,7 +57,7 @@
|
||||
))
|
||||
</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.requester_id, drf.create_time, ap.name, drf.status
|
||||
</select>
|
||||
|
||||
<select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto">
|
||||
|
||||
@@ -926,23 +926,36 @@ function handleDelete() {
|
||||
}
|
||||
|
||||
if (hasSavedItem) {
|
||||
// 有已保存的行,调用后端API删除
|
||||
savePrescription({ adviceSaveList: deleteList }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
getListInfo(false);
|
||||
}
|
||||
// 🔧 Bug #454: 删除前弹出确认提示,告知用户将级联删除关联检验申请单
|
||||
const hasLabItem = deleteList.some(item => item.adviceType === 3);
|
||||
const confirmMsg = hasLabItem
|
||||
? '删除此医嘱将同时删除关联的检验申请单,是否确认删除?'
|
||||
: '确认删除选中的医嘱项目吗?';
|
||||
|
||||
proxy.$modal.confirm(confirmMsg).then(() => {
|
||||
savePrescription({ adviceSaveList: deleteList }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
getListInfo(false);
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
} else {
|
||||
// 只有新增行,已经在前端删除完成
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
|
||||
function handleNumberClick(item, index) {
|
||||
@@ -1046,8 +1059,10 @@ function handleSave() {
|
||||
chargeItemId: item.chargeItemId,
|
||||
};
|
||||
});
|
||||
// 确保 organizationId 不为 undefined(手术计费场景下可能缺失 orgId)
|
||||
const orgId = props.patientInfo.orgId || props.patientInfo.effectiveOrgId || 1;
|
||||
savePrescriptionSign({
|
||||
organizationId: props.patientInfo.orgId,
|
||||
organizationId: orgId,
|
||||
adviceSaveList: list,
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
@@ -1064,6 +1079,7 @@ function handleSave() {
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('签发失败:', error);
|
||||
console.warn('签发操作失败(可能无权限或后端异常):', error?.response?.data?.msg || error?.message);
|
||||
proxy.$modal.msgError(error?.response?.data?.msg || error?.message || '签发失败,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1025,7 +1025,11 @@ function parseBirthDate(birthDate) {
|
||||
|
||||
function normalizeDate(value) {
|
||||
if (!value) return '';
|
||||
return String(value).split(/[T ]/)[0];
|
||||
const datePart = String(value).split(/[T ]/)[0].replace(/\//g, '-');
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length !== 3) return datePart;
|
||||
const [year, month, day] = parts;
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function normalizeSex(value) {
|
||||
@@ -1116,7 +1120,7 @@ function showReport(reportData = {}, readOnly = true) {
|
||||
addressHouse: reportData.addressHouse || '',
|
||||
patientBelong: reportData.patientBelong || 1,
|
||||
occupation: reportData.occupation || '',
|
||||
caseClass: reportData.diseaseType != null ? String(reportData.diseaseType) : '',
|
||||
caseClass: reportData.caseClass != null ? String(reportData.caseClass) : '',
|
||||
onsetDate: normalizeDate(reportData.onsetDate),
|
||||
diagDate: normalizeDate(reportData.diagDate),
|
||||
deathDate: normalizeDate(reportData.deathDate),
|
||||
@@ -1125,7 +1129,7 @@ function showReport(reportData = {}, readOnly = true) {
|
||||
selectedClassB: diseaseSelection.selectedClassB,
|
||||
selectedClassC: diseaseSelection.selectedClassC,
|
||||
otherDisease: reportData.otherDisease || (diseaseCode === 'OTHER' ? reportData.diseaseName || '' : ''),
|
||||
diseaseType: reportData.diseaseSubtype || '',
|
||||
diseaseType: reportData.diseaseSubtype || reportData.diseaseType || '',
|
||||
reportOrg: reportData.reportOrg || '',
|
||||
reportOrgPhone: reportData.reportOrgPhone || '',
|
||||
reportDoc: reportData.reportDoc || '',
|
||||
@@ -1466,7 +1470,7 @@ async function buildSubmitData() {
|
||||
reportDate: formData.reportDate || null,
|
||||
cardNameCode: 1, // 默认中华人民共和国传染病报告卡
|
||||
registrationSource: 1, // 默认门诊
|
||||
status: '',
|
||||
status: null,
|
||||
deptId: props.deptId || null,
|
||||
doctorId: props.doctorId || null,
|
||||
};
|
||||
|
||||
@@ -1250,7 +1250,8 @@ function handleRowClick(row) {
|
||||
expanded: false,
|
||||
packageDetailsLoading: false,
|
||||
isPackage: false,
|
||||
packageId: null
|
||||
packageId: null,
|
||||
hasChildren: false // #426修复: 树形表格懒加载展开标记,后续根据packageId动态设置
|
||||
};
|
||||
// 加载该项目的检查方法
|
||||
if (m.bodyPartCode) {
|
||||
@@ -1278,6 +1279,7 @@ function handleRowClick(row) {
|
||||
if (item.selectedMethod?.packageId) {
|
||||
item.isPackage = true;
|
||||
item.packageId = item.selectedMethod.packageId;
|
||||
item.hasChildren = true; // #426修复
|
||||
}
|
||||
}
|
||||
if (!item.selectedMethod && item.methods.length) {
|
||||
@@ -1286,6 +1288,7 @@ function handleRowClick(row) {
|
||||
if (item.selectedMethod?.packageId) {
|
||||
item.packageId = item.selectedMethod.packageId;
|
||||
item.isPackage = true;
|
||||
item.hasChildren = true; // #426修复
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1361,6 +1364,7 @@ async function handleMethodSelect(checked, method, cat) {
|
||||
if (method.packageId) {
|
||||
existingItem.isPackage = true;
|
||||
existingItem.packageId = method.packageId;
|
||||
existingItem.hasChildren = true; // #426修复
|
||||
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
|
||||
// 预加载套餐明细
|
||||
loadPackageDetailsForItem(existingItem);
|
||||
@@ -1395,7 +1399,8 @@ 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,确保套餐明细可加载
|
||||
packageName: method.packageName || targetItem.packageName || null, // #428修复: 复制 packageName,确保套餐明细可加载
|
||||
hasChildren: !!(method.packageId || targetItem.packageId) // #426修复: 树形表格懒加载展开标记
|
||||
};
|
||||
selectedItems.value.push(newItem);
|
||||
|
||||
@@ -1483,7 +1488,8 @@ async function handleItemSelect(checked, item, cat) {
|
||||
isPackage: !!(item.packageId || item.packageName),
|
||||
packageName: item.packageName || null,
|
||||
packageDetailsLoading: false,
|
||||
packageId: item.packageId || null
|
||||
packageId: item.packageId || null,
|
||||
hasChildren: !!(item.packageId) // #426修复: 树形表格懒加载展开标记
|
||||
};
|
||||
selectedItems.value.push(newRow);
|
||||
// 必须用数组里的响应式行,不能继续改局部 newRow:push 后列表内是 proxy,改 raw 对象不会触发右侧卡片更新(会一直卡在「加载中」)
|
||||
@@ -1605,6 +1611,7 @@ async function onDetailMethodChange(row, val) {
|
||||
if (val?.packageId) {
|
||||
row.packageId = val.packageId;
|
||||
row.isPackage = true;
|
||||
row.hasChildren = true; // #426修复
|
||||
}
|
||||
row.packageDetailsDisplay = undefined;
|
||||
const carrier = getPackageCarrier(row);
|
||||
|
||||
@@ -653,6 +653,16 @@ async function handleUseOrderGroup(row) {
|
||||
unitCodeName: item.unitCodeName || orderDetail.unitCode_dictText,
|
||||
minUnitCode: orderDetail.minUnitCode,
|
||||
doseUnitCode: orderDetail.doseUnitCode,
|
||||
doseUnitCode_dictText: orderDetail.doseUnitCode_dictText || '',
|
||||
|
||||
// 药房/科室名称(setValue 通过库存查找设置,但需确保 orderDetail 中有)
|
||||
positionName: orderDetail.positionName || '',
|
||||
|
||||
// 注射/皮试标识(表格列显示依赖这些字段)
|
||||
injectFlag: orderDetail.injectFlag,
|
||||
injectFlag_enumText: orderDetail.injectFlag_enumText || '',
|
||||
skinTestFlag: orderDetail.skinTestFlag,
|
||||
skinTestFlag_enumText: orderDetail.skinTestFlag_enumText || '',
|
||||
|
||||
// 字典文本(传递到 item 层级,避免后续代码依赖 mergedDetail 再查一次)
|
||||
methodCode_dictText: methodCodeDictText,
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
|
||||
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
|
||||
<el-table-column label="申请单状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)" effect="plain" round>
|
||||
@@ -101,6 +100,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- 详情 - 所有状态都显示 -->
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待签发" value="0" />
|
||||
<el-option label="已签发" value="1" />
|
||||
<el-option label="报告已出" value="4" />
|
||||
<el-option label="已作废" value="5" />
|
||||
<el-option label="已出报告" value="6" />
|
||||
<el-option label="已作废" value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字">
|
||||
@@ -105,15 +105,18 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
|
||||
<el-table-column label="操作" align="center" fixed="right" width="160">
|
||||
<el-table-column label="操作" align="center" fixed="right" width="180">
|
||||
<template #default="scope">
|
||||
<!-- 待签发:可修改、删除 -->
|
||||
<template v-if="scope.row.status == 0">
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
<!-- 已签发:可撤回 -->
|
||||
<template v-else-if="scope.row.status == 1">
|
||||
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
|
||||
</template>
|
||||
<!-- 已校对(2)、待接收(3)、已接收(4)、已检查(6)、已作废(7):仅查看详情 -->
|
||||
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -328,8 +331,11 @@ const parseBillStatus = (status) => {
|
||||
const statusMap = {
|
||||
'0': '待签发',
|
||||
'1': '已签发',
|
||||
'4': '报告已出',
|
||||
'5': '已作废',
|
||||
'2': '已校对',
|
||||
'3': '待接收',
|
||||
'4': '已收样',
|
||||
'6': '已出报告',
|
||||
'7': '已作废',
|
||||
};
|
||||
return statusMap[String(status)] || '-';
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@ const emits = defineEmits(['submitOk']);
|
||||
const props = defineProps({});
|
||||
const state = reactive({});
|
||||
const applicationListAll = ref();
|
||||
const applicationList = ref();
|
||||
const applicationList = ref([]);
|
||||
const orgOptions = ref([]); // 科室选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const mapToTransferItem = (item) => {
|
||||
@@ -150,6 +150,7 @@ const getList = () => {
|
||||
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
|
||||
applicationList.value = surgeryMappedCache;
|
||||
applicationListAll.value = surgeryRecordsCache;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
loadPage('');
|
||||
@@ -173,7 +174,9 @@ const loadPage = (key) => {
|
||||
dbTotal.value = res.data.total || 0;
|
||||
const records = res.data.records;
|
||||
applicationListAll.value = records;
|
||||
applicationList.value = records.map(mapToTransferItem);
|
||||
applicationList.value = records
|
||||
.filter(item => item.adviceDefinitionId != null)
|
||||
.map(mapToTransferItem);
|
||||
// 仅在无搜索时缓存
|
||||
if (!key) {
|
||||
surgeryRecordsCache = records;
|
||||
|
||||
@@ -380,7 +380,7 @@ const form = ref({
|
||||
prescriptionList: prescriptionList.value,
|
||||
});
|
||||
const adviceQueryParams = ref({
|
||||
adviceType: '',
|
||||
adviceType: 1,
|
||||
categoryCode: '', // 初始为空,等待加载配置后动态设置
|
||||
searchKey: '',
|
||||
});
|
||||
@@ -533,7 +533,6 @@ const statusOption = [
|
||||
let loadingInstance = undefined;
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', escKeyListener);
|
||||
getList();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -574,6 +573,7 @@ function handleTotalAmount() {
|
||||
}
|
||||
}, new Decimal(0));
|
||||
}
|
||||
getList();
|
||||
function getList() {
|
||||
getDiagnosisDefinitionList(queryParams.value).then((res) => {
|
||||
// prescriptionList.value = res.data.records;
|
||||
@@ -585,11 +585,6 @@ function refresh() {
|
||||
}
|
||||
// 获取列表信息
|
||||
function getListInfo(addNewRow) {
|
||||
// 守护:未选择患者时不发起 API 请求,避免页面加载时循环报错
|
||||
if (!patientInfo.value || !patientInfo.value.encounterId) {
|
||||
console.warn('⚠️ getListInfo 跳过:未选择患者');
|
||||
return;
|
||||
}
|
||||
loadingInstance = ElLoading.service({ fullscreen: true });
|
||||
setTimeout(() => {
|
||||
loadingInstance.close();
|
||||
@@ -691,7 +686,7 @@ function loadConfiguredCategories() {
|
||||
nextTick(() => {
|
||||
// 创建新对象触发响应式更新
|
||||
adviceQueryParams.value = {
|
||||
adviceType: '',
|
||||
adviceType: 1,
|
||||
categoryCode: defaultCategoryCode,
|
||||
searchKey: '',
|
||||
};
|
||||
@@ -715,17 +710,9 @@ function loadConfiguredCategories() {
|
||||
// 数据过滤
|
||||
const filterPrescriptionList = computed(() => {
|
||||
const pList = prescriptionList.value.filter((item) => {
|
||||
// 修复 Bug #488:orderClassCode 可能是复合值 '1-2',需提取 adviceType 部分进行比较
|
||||
let matchAdviceType = true;
|
||||
if (orderClassCode.value) {
|
||||
const filterAdviceType = String(orderClassCode.value).includes('-')
|
||||
? parseInt(String(orderClassCode.value).split('-')[0])
|
||||
: orderClassCode.value;
|
||||
matchAdviceType = filterAdviceType == item.adviceType;
|
||||
}
|
||||
return (
|
||||
(!therapyEnum.value || therapyEnum.value == item.therapyEnum) &&
|
||||
matchAdviceType &&
|
||||
(!orderClassCode.value || orderClassCode.value == item.adviceType) &&
|
||||
(!orderStatus.value || (orderStatus.value == item.statusEnum && item.requestId))
|
||||
);
|
||||
});
|
||||
@@ -753,28 +740,12 @@ function getRowDisabled(row) {
|
||||
/**
|
||||
* 将行的 adviceType + categoryCode 映射为 el-select 的选中值
|
||||
* 药品子分类使用复合值如 '1-2'(adviceType-categoryCode),诊疗/手术/全部使用原始值
|
||||
* 修复 Bug #488:当行的 adviceType 在当前配置中找不到匹配选项时,返回最接近的可用值,避免回显为纯数字
|
||||
*/
|
||||
function getRowSelectValue(row) {
|
||||
if (row.adviceType == 1 && row.categoryCode) {
|
||||
const compositeValue = '1-' + row.categoryCode;
|
||||
// 检查复合值是否在选项列表中
|
||||
if (adviceTypeList.value.some(item => item.value === compositeValue)) {
|
||||
return compositeValue;
|
||||
}
|
||||
// 配置的 categoryCode 已变更,回退到第一个药品选项
|
||||
const firstPharmacy = adviceTypeList.value.find(item => String(item.value).startsWith('1-'));
|
||||
if (firstPharmacy) {
|
||||
return firstPharmacy.value;
|
||||
}
|
||||
return row.adviceType;
|
||||
return '1-' + row.categoryCode;
|
||||
}
|
||||
// 诊疗/手术等非药品类型,检查其值是否在选项列表中
|
||||
if (adviceTypeList.value.some(item => item.value === row.adviceType)) {
|
||||
return row.adviceType;
|
||||
}
|
||||
// 不在选项中的值(如已废弃的 adviceType),返回 undefined 让 el-select 显示为空
|
||||
return undefined;
|
||||
return row.adviceType;
|
||||
}
|
||||
|
||||
// 新增医嘱
|
||||
@@ -831,8 +802,8 @@ function clickRowDb(row, column, event) {
|
||||
return;
|
||||
}
|
||||
row.showPopover = false;
|
||||
// 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑
|
||||
if (row.statusEnum == 1) {
|
||||
// “待签发(已保存 requestId存在)”不允许再编辑;仅“待保存(无requestId)”允许编辑
|
||||
if (row.statusEnum == 1 && !row.requestId) {
|
||||
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
|
||||
row.therapyEnum = String(row.therapyEnum ?? '1');
|
||||
row.isEdit = true;
|
||||
@@ -910,9 +881,9 @@ function handleFocus(row, index) {
|
||||
// 用 adviceType + categoryCode 组合查找匹配的选项
|
||||
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
|
||||
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
|
||||
// 修复Bug #486:当行没有显式选择医嘱类型时(row.adviceType为undefined),
|
||||
// 不传categoryCode,让搜索在全药库中进行;只有行已选择类型时才用对应categoryCode过滤
|
||||
const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
|
||||
// If the row has an explicit adviceType (saved/existing row), use its own categoryCode.
|
||||
// If no type is selected (new row), use empty string for global search across all categories.
|
||||
const categoryCode = selectedItem ? selectedItem.categoryCode : (row.adviceType != null ? (row.categoryCode || '') : '');
|
||||
const searchKey = row.adviceName || '';
|
||||
|
||||
nextTick(() => {
|
||||
@@ -949,12 +920,9 @@ function handleChange(value) {
|
||||
// 用 adviceType + categoryCode 组合查找匹配的选项
|
||||
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
|
||||
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
|
||||
// 修复Bug #486:当行没有显式选择医嘱类型时(row?.adviceType为undefined),
|
||||
// 不传categoryCode,让搜索在全药库中进行;只有行已选择类型时才用对应categoryCode过滤
|
||||
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
|
||||
// 修复Bug #453:当adviceType为空字符串或NaN时,不传具体类型,让refresh函数根据searchKey决定搜索范围
|
||||
const effectiveAdviceType = (adviceType && !isNaN(Number(adviceType))) ? adviceType : '';
|
||||
tableRef.refresh(effectiveAdviceType, categoryCode, value);
|
||||
// 修复Bug #486:当行没有显式选择医嘱类型时,不传categoryCode,让搜索在全药库中进行
|
||||
const categoryCode = selectedItem ? selectedItem.categoryCode : (row?.adviceType !== undefined ? (adviceQueryParams.value.categoryCode || '') : '');
|
||||
tableRef.refresh(adviceType, categoryCode, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1213,27 +1181,19 @@ function handleSave() {
|
||||
});
|
||||
// 此处签发处方和单行保存处方传参相同,后台已经将传参存为JSON字符串,此处直接转换为JSON即可
|
||||
loading.value = true;
|
||||
let list = [];
|
||||
try {
|
||||
list = saveList.map((item) => {
|
||||
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) || {} : {};
|
||||
return {
|
||||
...parsedContent,
|
||||
adviceType: item.adviceType,
|
||||
requestId: item.requestId,
|
||||
dbOpType: '1',
|
||||
groupId: item.groupId,
|
||||
uniqueKey: undefined,
|
||||
// 确保 therapyEnum 被正确传递
|
||||
therapyEnum: parsedContent?.therapyEnum || item.therapyEnum || '1',
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
loading.value = false;
|
||||
isSaving.value = false;
|
||||
proxy.$modal.msgError('医嘱内容解析失败,请检查待签发医嘱');
|
||||
return;
|
||||
}
|
||||
let list = saveList.map((item) => {
|
||||
const parsedContent = JSON.parse(item.contentJson);
|
||||
return {
|
||||
...parsedContent,
|
||||
adviceType: item.adviceType,
|
||||
requestId: item.requestId,
|
||||
dbOpType: '1',
|
||||
groupId: item.groupId,
|
||||
uniqueKey: undefined,
|
||||
// 确保 therapyEnum 被正确传递
|
||||
therapyEnum: parsedContent.therapyEnum || item.therapyEnum || '1',
|
||||
};
|
||||
});
|
||||
// 保存签发按钮
|
||||
isSaving.value = true;
|
||||
console.log('签发处方参数:', {
|
||||
@@ -1248,16 +1208,9 @@ function handleSave() {
|
||||
if (res.code === 200) {
|
||||
proxy.$modal.msgSuccess('签发成功');
|
||||
isSaving.value = false;
|
||||
// 乐观更新:立即将已签发医嘱的状态设为"已签发",确保列表实时刷新
|
||||
saveList.forEach((item) => {
|
||||
const row = prescriptionList.value.find((r) => r.requestId && r.requestId === item.requestId);
|
||||
if (row) {
|
||||
row.statusEnum = 2;
|
||||
}
|
||||
});
|
||||
getListInfo(false);
|
||||
bindMethod.value = {};
|
||||
nextId.value = 1;
|
||||
nextId.value == 1;
|
||||
} else {
|
||||
proxy.$modal.msgError(res.message);
|
||||
isSaving.value = false;
|
||||
@@ -1358,12 +1311,11 @@ function handleCancelEdit(row, index) {
|
||||
|
||||
function handleSaveSign(row, index) {
|
||||
if (row.adviceType != 2) {
|
||||
// 修复 Bug #488:严格校验 itemNo,确保非空且为有效字符串才发起请求
|
||||
let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId;
|
||||
if (!itemNo || String(itemNo).trim() === '') {
|
||||
if (!itemNo) {
|
||||
console.warn('绑定设备检查跳过:itemNo为空(adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + ')');
|
||||
} else {
|
||||
getBindDevice({ typeCode: row.adviceType, itemNo: String(itemNo) }).then((res) => {
|
||||
getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => {
|
||||
if (res.data.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1424,21 +1376,13 @@ function handleSaveSign(row, index) {
|
||||
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
|
||||
if (res.code === 200) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
nextId.value = 1;
|
||||
nextId.value == 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增行:调用保存接口将数据持久化到后端
|
||||
row.dbOpType = '1';
|
||||
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
|
||||
if (res.code === 200) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
nextId.value = 1;
|
||||
// 保存成功后刷新列表,确保后端返回的数据带 requestId
|
||||
getListInfo(false);
|
||||
}
|
||||
});
|
||||
// 不需要再添加空行,保存成功后由 getListInfo 处理
|
||||
if (prescriptionList.value[0].adviceName) {
|
||||
handleAddPrescription();
|
||||
}
|
||||
}
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
@@ -1479,40 +1423,32 @@ function handleSaveBatch() {
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
// 修复 Bug #405:保存成功后锁定所有待保存行,避免医嘱条目仍处于可编辑状态
|
||||
// saveList 中的 item 与 prescriptionList 是同一对象引用,直接修改即可
|
||||
// 修复【#405】:保存成功后重置所有待保存行的 isEdit 为 false,锁定医嘱不再编辑
|
||||
saveList.forEach(item => {
|
||||
item.isEdit = false;
|
||||
const row = prescriptionList.value.find(r => r.uniqueKey === item.uniqueKey);
|
||||
if (row) row.isEdit = false;
|
||||
});
|
||||
// 兜底:锁定所有 statusEnum == 1 的行,确保没有遗漏
|
||||
prescriptionList.value.forEach(row => {
|
||||
if (row.statusEnum == 1) {
|
||||
row.isEdit = false;
|
||||
}
|
||||
});
|
||||
expandOrder.value = [];
|
||||
getListInfo(false);
|
||||
nextId.value = 1;
|
||||
nextId.value == 1;
|
||||
isSaving.value = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
isSaving.value = false;
|
||||
proxy.$modal.msgError(error?.msg || '保存失败,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
function setValue(row) {
|
||||
// 构造单位列表,确保 value 始终为 String 类型,避免 el-select 值类型不匹配
|
||||
// 构造单位列表
|
||||
unitCodeList.value = [
|
||||
{ value: String(row.unitCode ?? ''), label: row.unitCode_dictText, type: 'unit' },
|
||||
{ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' },
|
||||
{
|
||||
value: String(row.doseUnitCode ?? ''),
|
||||
value: row.doseUnitCode,
|
||||
label: row.doseUnitCode_dictText,
|
||||
type: 'dose',
|
||||
},
|
||||
{
|
||||
value: String(row.minUnitCode ?? ''),
|
||||
value: row.minUnitCode,
|
||||
label: row.minUnitCode_dictText,
|
||||
type: 'minUnit',
|
||||
},
|
||||
@@ -1577,9 +1513,9 @@ function setValue(row) {
|
||||
orgName: row.adviceType != 3 ? undefined : (findOrgName(row.orgId || row.positionId || patientInfo.value?.inHospitalOrgId) || row.orgName || patientInfo.value?.inHospitalOrgName || ''),
|
||||
// dose: undefined, Removed to preserve dose value from group package
|
||||
unitCodeList: unitCodeList.value,
|
||||
doseUnitCode: String(row.doseUnitCode ?? ''),
|
||||
minUnitCode: String(row.minUnitCode ?? ''),
|
||||
unitCode: row.partAttributeEnum == 1 ? String(row.minUnitCode ?? '') : String(row.unitCode ?? ''),
|
||||
doseUnitCode: row.doseUnitCode,
|
||||
minUnitCode: row.minUnitCode,
|
||||
unitCode: row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode,
|
||||
categoryEnum: row.categoryCode,
|
||||
definitionId: row.chargeItemDefinitionId,
|
||||
executeNum: 1,
|
||||
@@ -1595,10 +1531,6 @@ function setValue(row) {
|
||||
? new Decimal(selectedStock.price).div(row.partPercent).toFixed(6)
|
||||
: prevRow.minUnitPrice,
|
||||
positionName: selectedStock?.locationName,
|
||||
// 🔧 Bug #523 修复:初始化 totalPrice 为 0,避免总金额列显示为横杠
|
||||
totalPrice: row.quantity
|
||||
? new Decimal(row.quantity).mul(selectedStock?.price ?? 0).toFixed(6)
|
||||
: '0',
|
||||
}
|
||||
: {
|
||||
quantity: 1,
|
||||
@@ -1630,21 +1562,17 @@ function handleSaveGroup(orderGroupList) {
|
||||
// 🔥 新版组件已经预处理了数据,优先使用 mergedDetail
|
||||
const mergedDetail = item.mergedDetail || {
|
||||
...(item.orderDetailInfos || {}),
|
||||
adviceName: item.orderDefinitionName || item.orderDetailInfos?.adviceName || '未知项目',
|
||||
adviceName: item.orderDetailInfos?.adviceName || item.orderDefinitionName || '未知项目',
|
||||
adviceType: item.orderDetailInfos?.adviceType,
|
||||
adviceDefinitionId: item.orderDefinitionId || item.orderDetailInfos?.adviceDefinitionId,
|
||||
quantity: item.quantity,
|
||||
unitCode: item.unitCode || item.orderDetailInfos?.unitCode,
|
||||
unitCodeName: item.unitCodeName,
|
||||
// 🔧 Bug #403 修复:dose/doseQuantity/dispensePerDuration 需用 null 检查,
|
||||
// 避免组套中值为 null 时回退到医嘱库的 orderDetailInfos
|
||||
dose: item.dose !== undefined && item.dose !== null ? item.dose : item.orderDetailInfos?.dose,
|
||||
dose: item.dose || item.orderDetailInfos?.dose,
|
||||
rateCode: item.rateCode || item.orderDetailInfos?.rateCode,
|
||||
methodCode: item.methodCode || item.orderDetailInfos?.methodCode,
|
||||
dispensePerDuration: item.dispensePerDuration !== undefined && item.dispensePerDuration !== null
|
||||
? item.dispensePerDuration : item.orderDetailInfos?.dispensePerDuration,
|
||||
doseQuantity: item.doseQuantity !== undefined && item.doseQuantity !== null
|
||||
? item.doseQuantity : item.orderDetailInfos?.doseQuantity,
|
||||
dispensePerDuration: item.dispensePerDuration || item.orderDetailInfos?.dispensePerDuration,
|
||||
doseQuantity: item.doseQuantity,
|
||||
inventoryList: item.orderDetailInfos?.inventoryList || [],
|
||||
priceList: item.orderDetailInfos?.priceList || [],
|
||||
partPercent: item.orderDetailInfos?.partPercent || 1,
|
||||
@@ -1663,69 +1591,41 @@ function handleSaveGroup(orderGroupList) {
|
||||
setValue(mergedDetail);
|
||||
|
||||
// 创建新的处方项目
|
||||
// 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail(已由 setValue 填充完整数据)
|
||||
// 先取 setValue 填充的行数据作为基础
|
||||
const baseRow = prescriptionList.value[rowIndex.value];
|
||||
const newRow = {
|
||||
...baseRow,
|
||||
isEdit: false,
|
||||
...prescriptionList.value[rowIndex.value],
|
||||
patientId: patientInfo.value.patientId,
|
||||
encounterId: patientInfo.value.encounterId,
|
||||
accountId: accountId.value,
|
||||
quantity: item.quantity,
|
||||
methodCode: item.methodCode,
|
||||
rateCode: item.rateCode,
|
||||
dispensePerDuration: item.dispensePerDuration,
|
||||
dose: item.dose,
|
||||
doseQuantity: item.doseQuantity,
|
||||
executeNum: 1,
|
||||
unitCode: item.unitCode,
|
||||
unitCode_dictText: item.unitCodeName || '',
|
||||
statusEnum: 1,
|
||||
orgId: resolveOrgId(
|
||||
(item.orderDetailInfos?.orgId || mergedDetail.orgId)
|
||||
? (item.orderDetailInfos?.orgId || mergedDetail.orgId)
|
||||
: patientInfo.value?.inHospitalOrgId
|
||||
) || '',
|
||||
orgName: findOrgName(
|
||||
(item.orderDetailInfos?.orgId || mergedDetail.orgId)
|
||||
? (item.orderDetailInfos?.orgId || mergedDetail.orgId)
|
||||
: patientInfo.value?.inHospitalOrgId
|
||||
) || item.orderDetailInfos?.orgName || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '',
|
||||
orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
|
||||
// 🔧 修复:同时保存 orgName,确保树匹配不到时仍有中文名称可显示
|
||||
orgName: findOrgName(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || item.orderDetailInfos?.orgName || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '',
|
||||
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1',
|
||||
conditionId: conditionId.value,
|
||||
conditionDefinitionId: conditionDefinitionId.value,
|
||||
encounterDiagnosisId: encounterDiagnosisId.value,
|
||||
diagnosisName: diagnosisName.value,
|
||||
therapyEnum: baseRow?.therapyEnum || '1',
|
||||
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || '1',
|
||||
};
|
||||
// 覆盖关键字段:优先使用 item 的值,其次 mergedDetail(已由 setValue 填充),最后 baseRow
|
||||
newRow.quantity = item.quantity ?? mergedDetail.quantity ?? baseRow.quantity;
|
||||
newRow.methodCode = item.methodCode ?? mergedDetail.methodCode ?? baseRow.methodCode;
|
||||
newRow.rateCode = item.rateCode ?? mergedDetail.rateCode ?? baseRow.rateCode;
|
||||
newRow.dispensePerDuration = item.dispensePerDuration ?? mergedDetail.dispensePerDuration ?? baseRow.dispensePerDuration;
|
||||
newRow.dose = item.dose ?? mergedDetail.dose ?? baseRow.dose;
|
||||
newRow.doseQuantity = item.doseQuantity ?? mergedDetail.doseQuantity ?? baseRow.doseQuantity;
|
||||
newRow.unitCode = item.unitCode ?? mergedDetail.unitCode ?? baseRow.unitCode;
|
||||
newRow.unitCode_dictText = item.unitCodeName || mergedDetail.unitCodeName || baseRow.unitCode_dictText || '';
|
||||
// 确保价格字段有兜底值(setValue 可能因库存不足提前 return 导致未设置)
|
||||
newRow.unitPrice = baseRow.unitPrice ?? mergedDetail.unitPrice ?? 0;
|
||||
newRow.minUnitPrice = baseRow.minUnitPrice ?? mergedDetail.minUnitPrice ?? 0;
|
||||
newRow.unitTempPrice = newRow.unitPrice;
|
||||
newRow.methodCode_dictText = mergedDetail.methodCode_dictText || item.methodCode_dictText || '';
|
||||
newRow.rateCode_dictText = mergedDetail.rateCode_dictText || item.rateCode_dictText || '';
|
||||
newRow.doseUnitCode = mergedDetail.doseUnitCode || item.doseUnitCode || baseRow.doseUnitCode;
|
||||
newRow.doseUnitCode_dictText = mergedDetail.doseUnitCode_dictText || item.doseUnitCode_dictText || '';
|
||||
newRow.orgId = mergedDetail.orgId || mergedDetail.positionId || baseRow.orgId || '';
|
||||
newRow.positionName = mergedDetail.orgName || mergedDetail.positionName || baseRow.positionName || '';
|
||||
|
||||
// 计算价格和总量
|
||||
// 🔧 Bug #403 修复:使用 newRow.unitCode(已由 setValue 填充)而非 item.unitCode
|
||||
// 使用 ?? 替代 || 计算 partPercent,确保值为 0 时不会被错误替换
|
||||
const finalUnitCode = newRow.unitCode;
|
||||
const unitInfo = unitCodeList.value.find((k) => k.value == finalUnitCode);
|
||||
const finalQuantity = newRow.quantity;
|
||||
const partPercent = item.orderDetailInfos?.partPercent ?? mergedDetail.partPercent ?? baseRow.partPercent ?? 1;
|
||||
const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode);
|
||||
if (unitInfo && unitInfo.type == 'minUnit') {
|
||||
newRow.price = newRow.minUnitPrice;
|
||||
newRow.totalPrice = ((finalQuantity || 0) * newRow.minUnitPrice).toFixed(6);
|
||||
newRow.minUnitQuantity = finalQuantity || 0;
|
||||
newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6);
|
||||
newRow.minUnitQuantity = item.quantity;
|
||||
} else {
|
||||
newRow.price = newRow.unitPrice;
|
||||
newRow.totalPrice = ((finalQuantity || 0) * newRow.unitPrice).toFixed(6);
|
||||
newRow.minUnitQuantity = (finalQuantity || 0) * partPercent;
|
||||
newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6);
|
||||
newRow.minUnitQuantity = item.quantity * (item.orderDetailInfos?.partPercent || mergedDetail.partPercent || 1);
|
||||
}
|
||||
|
||||
newRow.contentJson = JSON.stringify(newRow);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<span class="descriptions-item-label">全选:</span>
|
||||
<el-switch v-model="chooseAll" @change="handelSwitchChange" />
|
||||
<el-button class="ml20" type="primary" @click="handleCheck"> 核对通过 </el-button>
|
||||
<el-button class="ml20 mr20" type="danger" @click="handleCancel"> 退回 </el-button>
|
||||
<el-button class="ml20 mr20" type="danger" :disabled="hasDispensedSelected" @click="handleCancel"> 退回 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -152,6 +152,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {ref, computed} from 'vue';
|
||||
import {adviceVerify, cancel, getPrescriptionList} from './api';
|
||||
import {patientInfoList} from '../../components/store/patient.js';
|
||||
import {formatDateStr} from '@/utils/index';
|
||||
@@ -163,6 +164,11 @@ const type = ref(null);
|
||||
const { proxy } = getCurrentInstance();
|
||||
const loading = ref(false);
|
||||
const chooseAll = ref(false);
|
||||
const selectionTrigger = ref(0);
|
||||
const hasDispensedSelected = computed(() => {
|
||||
selectionTrigger.value;
|
||||
return getSelectRows().some(item => item.dispenseStatus === 4);
|
||||
});
|
||||
const props = defineProps({
|
||||
requestStatus: {
|
||||
type: Number,
|
||||
@@ -262,7 +268,9 @@ function getGroupMarkers() {
|
||||
}
|
||||
|
||||
// 选择框改变时的处理
|
||||
function handleSelectionChange(selection, row) {}
|
||||
function handleSelectionChange(selection, row) {
|
||||
selectionTrigger.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核对通过
|
||||
@@ -291,7 +299,7 @@ function handleCancel() {
|
||||
// 校验已发药的医嘱不允许退回
|
||||
let dispensedItems = list.filter(item => item.dispenseStatus === 4);
|
||||
if (dispensedItems.length > 0) {
|
||||
proxy.$message.error('该医嘱已发药,无法退回');
|
||||
proxy.$message.error('该药品已由药房发放,请先执行退药处理,不可直接退回');
|
||||
return;
|
||||
}
|
||||
cancel(list).then((res) => {
|
||||
@@ -310,12 +318,16 @@ function getSelectRows() {
|
||||
// 获取选中的医嘱信息
|
||||
let list = [];
|
||||
prescriptionList.value.forEach((item, index) => {
|
||||
list = [...list, ...proxy.$refs['tableRef' + index][0].getSelectionRows()];
|
||||
const ref = proxy.$refs['tableRef' + index];
|
||||
if (ref && ref[0]) {
|
||||
list = [...list, ...ref[0].getSelectionRows()];
|
||||
}
|
||||
});
|
||||
return list.map((item) => {
|
||||
return {
|
||||
requestId: item.requestId,
|
||||
requestTable: item.adviceTable,
|
||||
dispenseStatus: item.dispenseStatus,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
v-model="form.admissionTime"
|
||||
type="datetime"
|
||||
placeholder="选择入室时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -558,7 +558,7 @@
|
||||
v-model="form.entryTime"
|
||||
type="datetime"
|
||||
placeholder="选择进室时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -570,7 +570,7 @@
|
||||
v-model="form.anesStart"
|
||||
type="datetime"
|
||||
placeholder="选择麻醉开始时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -584,7 +584,7 @@
|
||||
v-model="form.startTime"
|
||||
type="datetime"
|
||||
placeholder="选择切开时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -596,7 +596,7 @@
|
||||
v-model="form.endTime"
|
||||
type="datetime"
|
||||
placeholder="选择手术结束时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -608,7 +608,7 @@
|
||||
v-model="form.anesEnd"
|
||||
type="datetime"
|
||||
placeholder="选择麻醉结束时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -689,7 +689,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 手术申请查询弹窗 -->
|
||||
<el-dialog :title="'手术申请查询'" v-model="showApplyDialog" width="1200px" @close="cancelApplyDialog" class="apply-query-dialog">
|
||||
<el-dialog :title="'手术申请查询'" v-model="showApplyDialog" width="1200px" @close="cancelApplyDialog" class="surgery-apply-dialog">
|
||||
<!-- 查询条件区 -->
|
||||
<el-form :model="applyQueryParams" ref="applyQueryRef" :inline="true" class="query-form">
|
||||
<el-form-item label="手术单号" prop="surgeryNo">
|
||||
@@ -741,16 +741,16 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 结果表格区 -->
|
||||
<el-table
|
||||
ref="applyTableRef"
|
||||
v-loading="applyLoading"
|
||||
:data="applyList"
|
||||
row-key="surgeryNo"
|
||||
@row-click="handleApplyRowClick"
|
||||
:row-class-name="tableRowClassName"
|
||||
style="width: 100%"
|
||||
max-height="400"
|
||||
:scroll="{ y: 400 }"
|
||||
<el-table
|
||||
ref="applyTableRef"
|
||||
v-loading="applyLoading"
|
||||
:data="applyList"
|
||||
row-key="surgeryNo"
|
||||
@row-click="handleApplyRowClick"
|
||||
:row-class-name="tableRowClassName"
|
||||
style="width: 100%"
|
||||
max-height="340"
|
||||
:scroll="{ y: 340 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="handleSelectable" />
|
||||
<el-table-column label="ID" align="center" width="80" fixed>
|
||||
@@ -781,7 +781,7 @@
|
||||
</el-table>
|
||||
|
||||
<!-- 底部分页区 -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-container apply-pagination" style="margin-top: 10px; padding-bottom: 20px">
|
||||
<pagination
|
||||
v-show="applyTotal > 0"
|
||||
:total="applyTotal"
|
||||
@@ -792,10 +792,12 @@
|
||||
@pagination="getSurgicalScheduleList"
|
||||
/>
|
||||
</div>
|
||||
<!-- 分页与底部操作区之间的间隔 -->
|
||||
<div style="height: 48px"></div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<div class="dialog-footer" style="margin-top: 24px; padding-top: 12px; border-top: 1px solid #ebeef5">
|
||||
<el-button @click="cancelApplyDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmApply">确认</el-button>
|
||||
</div>
|
||||
@@ -830,6 +832,7 @@
|
||||
</div>
|
||||
<div style="padding: 10px">
|
||||
<prescriptionlist :patientInfo="chargePatientInfo" ref="prescriptionRef"
|
||||
:generateSourceEnum="1"
|
||||
:sourceBillNo="chargePatientInfo.sourceBillNo" />
|
||||
<div class="overlay" v-if="disabled"></div>
|
||||
</div>
|
||||
@@ -1322,13 +1325,13 @@ function handleEdit(row) {
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
Object.assign(form, data)
|
||||
// 修复#433:确保字典字段类型与下拉选项一致(Number类型)
|
||||
// 后端OpSchedule.anesMethod为String类型,需转为Number与el-select匹配
|
||||
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
|
||||
if (data.anesthesiaTypeEnum != null) form.anesMethod = Number(data.anesthesiaTypeEnum)
|
||||
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
|
||||
if (data.feeType != null) form.feeType = data.feeType
|
||||
if (data.isExternalExpert != null) form.isExternalExpert = Number(data.isExternalExpert)
|
||||
// 使用nextTick确保在Vue响应式更新后再赋值,避免el-select无法匹配选项
|
||||
nextTick(() => {
|
||||
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
|
||||
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
|
||||
if (data.feeType != null) form.feeType = data.feeType
|
||||
if (data.isExternalExpert != null) form.isExternalExpert = Number(data.isExternalExpert)
|
||||
})
|
||||
} else {
|
||||
proxy.$modal.msgError('获取手术安排详情失败')
|
||||
}
|
||||
@@ -1348,13 +1351,13 @@ function handleView(row) {
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
Object.assign(form, data)
|
||||
// 修复#433:确保字典字段类型与下拉选项一致(Number类型)
|
||||
// 后端OpSchedule.anesMethod为String类型,需转为Number与el-select匹配
|
||||
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
|
||||
if (data.anesthesiaTypeEnum != null) form.anesMethod = Number(data.anesthesiaTypeEnum)
|
||||
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
|
||||
if (data.feeType != null) form.feeType = data.feeType
|
||||
if (data.isExternalExpert != null) form.isExternalExpert = Number(data.isExternalExpert)
|
||||
// 使用nextTick确保在Vue响应式更新后再赋值,避免el-select无法匹配选项
|
||||
nextTick(() => {
|
||||
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
|
||||
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
|
||||
if (data.feeType != null) form.feeType = data.feeType
|
||||
if (data.isExternalExpert != null) form.isExternalExpert = Number(data.isExternalExpert)
|
||||
})
|
||||
} else {
|
||||
proxy.$modal.msgError('获取手术安排详情失败')
|
||||
}
|
||||
@@ -1941,6 +1944,30 @@ function handleQuoteBilling() {
|
||||
}
|
||||
})
|
||||
|
||||
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目,避免"引用计费"后已提交项目重新出现在"待生成"列表
|
||||
// 原因:后端返回的计费数据中,已生成医嘱的项目可能没有 requestId 字段
|
||||
// 方案:用 chargeItemId/requestId/id 与已有的 temporaryAdvices 做匹配,排除已生成项目
|
||||
if (temporaryAdvices.value.length > 0) {
|
||||
const existingAdviceIds = new Set()
|
||||
temporaryAdvices.value.forEach(a => {
|
||||
const om = a.originalMedicine || {}
|
||||
if (om.requestId) existingAdviceIds.add(String(om.requestId))
|
||||
if (om.chargeItemId) existingAdviceIds.add(String(om.chargeItemId))
|
||||
if (om.id) existingAdviceIds.add(String(om.id))
|
||||
})
|
||||
if (existingAdviceIds.size > 0) {
|
||||
temporaryBillingMedicines.value = temporaryBillingMedicines.value.filter(m => {
|
||||
const mRequestId = m.requestId != null ? String(m.requestId) : null
|
||||
const mChargeItemId = m.chargeItemId != null ? String(m.chargeItemId) : null
|
||||
const mId = m.id != null ? String(m.id) : null
|
||||
if (mRequestId && existingAdviceIds.has(mRequestId)) return false
|
||||
if (mChargeItemId && existingAdviceIds.has(mChargeItemId)) return false
|
||||
if (mId && existingAdviceIds.has(mId)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
temporaryMedicalLoading.value = false // 🔧 新增:加载完成
|
||||
ElMessage.success('已成功引用最新计费药品信息!')
|
||||
} else {
|
||||
@@ -2349,6 +2376,22 @@ function getRowClassName({ row, rowIndex }) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* 手术申请查询弹窗 — 分页与footer间距 */
|
||||
.surgery-apply-dialog :deep(.el-dialog__body) {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.surgery-apply-dialog :deep(.el-dialog__footer) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.surgery-apply-dialog :deep(.apply-pagination) {
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.surgery-apply-dialog :deep(.apply-pagination .el-pagination) {
|
||||
margin-right: 80px;
|
||||
}
|
||||
|
||||
/* 选中行样式 */
|
||||
:deep(.el-table .selected-row) {
|
||||
background-color: #ecf5ff !important;
|
||||
@@ -2358,21 +2401,22 @@ function getRowClassName({ row, rowIndex }) {
|
||||
border-bottom: 1px solid #d9ecff !important;
|
||||
}
|
||||
|
||||
/* 手术申请查询弹窗 — 防止分页与底部重叠 */
|
||||
:deep(.apply-query-dialog .el-dialog__body) {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.apply-query-dialog .pagination-container) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.apply-query-dialog .dialog-footer) {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 手术申请查询弹窗 — 非 scoped 确保穿透 teleport */
|
||||
.surgery-apply-dialog .apply-pagination {
|
||||
padding-bottom: 24px !important;
|
||||
margin-bottom: 16px !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
.surgery-apply-dialog .apply-pagination .el-pagination {
|
||||
margin-right: 80px !important;
|
||||
}
|
||||
.surgery-apply-dialog .el-dialog__body {
|
||||
padding-bottom: 32px !important;
|
||||
}
|
||||
.surgery-apply-dialog .el-dialog__footer {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user