Compare commits
5 Commits
bugfix-403
...
d838be1a18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d838be1a18 | ||
|
|
3ab3ddbdf1 | ||
|
|
d2cb02eeef | ||
|
|
8850689f1f | ||
|
|
4c7d362946 |
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**: 两处代码一致,均需要同步修复
|
||||
@@ -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. 更新状态、完成时间以及初复诊标识
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1325,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('获取手术安排详情失败')
|
||||
}
|
||||
@@ -1351,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('获取手术安排详情失败')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user