Compare commits
4 Commits
d70181f7d8
...
72ffd84eed
| Author | SHA1 | Date | |
|---|---|---|---|
| 72ffd84eed | |||
| eed67f7375 | |||
|
|
60f963ad83 | ||
| af84e24db4 |
37
.agentforge/analysis/529.md
Normal file
37
.agentforge/analysis/529.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Bug #529 分析报告
|
||||
|
||||
## Title
|
||||
[住院医生工作站-检验申请] 点击"修改"打开编辑弹窗后,原已选中的项目未回显
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 数据流
|
||||
1. `testApplication.vue` 列表中点击"修改" → `handleEdit(row)` 设置 `editRowData = row` → 打开编辑弹窗
|
||||
2. 弹窗使用 `destroy-on-close`,每次打开都重新创建 `LaboratoryTests` 组件
|
||||
3. `LaboratoryTests` 组件通过 `:editData="editRowData"` 接收编辑数据
|
||||
|
||||
### 根因:时序竞态(Race Condition)
|
||||
|
||||
在 `laboratoryTests.vue` 中:
|
||||
|
||||
1. **`onMounted()`** (line 262) 调用 `loadAllData()` 异步加载检验项目列表到 `applicationListAll.value`
|
||||
2. **watch on `props.editData`** (line 347-382) 设置了 `{ immediate: true }`,组件创建时立即触发
|
||||
3. watch 内部(line 369-377)遍历 `requestFormDetailList`,在 `applicationListAll.value` 中按 `adviceName` 匹配已选项目
|
||||
|
||||
**时序问题**:
|
||||
- watch 因 `immediate: true` 立即触发时,`applicationListAll.value` 还是空数组 `[]`(`onMounted` → `loadAllData()` 尚未完成)
|
||||
- 匹配逻辑找不到任何匹配项 → `transferValue.value = []`
|
||||
- 随后 `loadAllData()` 完成,`applicationListAll.value` 被填充,但 watch 不会重新触发(因为 `props.editData` 没变化)
|
||||
- 结果:transfer 组件的 "已选择" 区域显示"无数据"
|
||||
|
||||
### 涉及文件
|
||||
- **前端**: `openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/laboratoryTests.vue` (line 347-382)
|
||||
- **前端**: `openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/testApplication.vue` (line 193-210, 弹窗渲染处)
|
||||
|
||||
### 修复方案
|
||||
|
||||
在 `laboratoryTests.vue` 中新增一个 watch 监听 `applicationListAll.value` 的变化,当数据加载完成且当前处于编辑模式时,重新执行回显匹配逻辑。这样确保:
|
||||
- 编辑模式 watch 先触发(但匹配不到数据,因为 `applicationListAll` 为空)
|
||||
- `applicationListAll` 加载完成后,新增 watch 触发,重新执行匹配,成功回显
|
||||
|
||||
改动量:约 12 行新增代码
|
||||
32
BUG_522_ANALYSIS.md
Normal file
32
BUG_522_ANALYSIS.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Bug #522 分析报告
|
||||
|
||||
## Bug 描述
|
||||
[住院护士站-三测单] 体征录入点击保存后缺乏执行反馈且窗口异常自动关闭
|
||||
|
||||
## 涉及文件
|
||||
- 前端: `openhis-ui-vue3/src/views/inpatientNurse/tprChart/components/addTprDialog.vue`
|
||||
- API: `openhis-ui-vue3/src/views/inpatientNurse/tprChart/components/api.js`
|
||||
- 父组件: `openhis-ui-vue3/src/views/inpatientNurse/tprChart/index.vue`
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 问题1:弹窗异常自动关闭 — 根因
|
||||
|
||||
在 `addTprDialog.vue` 模板中,保存按钮使用了 `:disabled="buttonDisabled"`(第50行和第108行),但 **`buttonDisabled` 变量在整个 script setup 中从未声明**。
|
||||
|
||||
在 Vue 3 `<script setup>` + Composition API 中,模板引用的变量必须在 script 中声明。未声明的变量会触发 `ReferenceError`,导致组件渲染失败或运行时异常。这个错误会破坏组件的响应式系统,使得 `dialogVisible` 的响应式绑定失效,从而导致弹窗在保存操作后异常关闭。
|
||||
|
||||
### 问题2:缺乏保存成功反馈 — 连带结果
|
||||
|
||||
虽然 `confirmCharge()` 函数在第1087行已有 `proxy.$modal.msgSuccess('保存成功')` 的调用,但由于 `buttonDisabled` 未声明引发的异常,导致代码执行路径被破坏,success 回调中的提示逻辑可能未能正常执行。
|
||||
|
||||
## 修复方案
|
||||
|
||||
1. **在 `addTprDialog.vue` 的 script setup 中新增 `buttonDisabled` ref 声明**,初始值为 `false`
|
||||
2. **在保存操作中添加 loading 状态**:点击保存后将按钮禁用,API 返回后恢复,防止重复提交的同时也保证了响应式状态的一致性
|
||||
|
||||
## 验收标准
|
||||
- [ ] 点击保存后弹窗保持开启状态
|
||||
- [ ] 保存成功后弹出"保存成功"提示
|
||||
- [ ] 左侧体征历史记录列表自动刷新
|
||||
- [ ] 录入区域表单被清空,方便继续录入下一条
|
||||
@@ -559,9 +559,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
if (adviceSaveList != null && !adviceSaveList.isEmpty()) {
|
||||
for (int i = 0; i < adviceSaveList.size(); i++) {
|
||||
AdviceSaveDto dto = adviceSaveList.get(i);
|
||||
log.info("Request[{}]: requestId={}, dbOpType={}, adviceType={}, encounterId={}, patientId={}",
|
||||
i, dto.getRequestId(), dto.getDbOpType(), dto.getAdviceType(),
|
||||
dto.getEncounterId(), dto.getPatientId());
|
||||
log.info("Request[{}]: requestId={}, dbOpType={}, adviceType={}, encounterId={}, patientId={}, categoryEnum={}, categoryEnum.class={}, categoryCode={}, categoryCode.class={}",
|
||||
i, dto.getRequestId(), dto.getDbOpType(), dto.getAdviceType(),
|
||||
dto.getEncounterId(), dto.getPatientId(),
|
||||
dto.getCategoryEnum(), dto.getCategoryEnum() != null ? dto.getCategoryEnum().getClass().getName() : "NULL",
|
||||
dto.getCategoryCode(), dto.getCategoryCode() != null ? dto.getCategoryCode().getClass().getName() : "NULL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1562,7 +1564,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
// 🔧 BugFix #498: categoryEnum=22(检查) 走 ServiceRequest,不走 DeviceRequest
|
||||
// 检查申请单的诊疗定义ID存在 activityId,不在 adviceDefinitionId
|
||||
// deviceDefId 对应耗材定义ID,不能用诊疗定义ID填充
|
||||
if (adviceSaveDto.getCategoryEnum() == 22) {
|
||||
if (Integer.valueOf(22).equals(adviceSaveDto.getCategoryEnum())) {
|
||||
log.info("handDevice skip - 检查申请单(categoryEnum=22) 走 ServiceRequest 路径,跳过 DeviceRequest 保存");
|
||||
continue; // 跳过本次循环,不走耗材请求路径
|
||||
} else if (adviceSaveDto.getAdviceDefinitionId() != null) {
|
||||
|
||||
@@ -3801,6 +3801,8 @@ function handleSaveHistory(value) {
|
||||
uniqueKey: undefined,
|
||||
dbOpType: value.requestId ? '2' : '1',
|
||||
minUnitQuantity: value.quantity * value.partPercent,
|
||||
// 🔧 修复:确保 categoryEnum 被传递(耗材必填字段),避免后端 NPE
|
||||
categoryEnum: value.categoryEnum || value.categoryCode,
|
||||
conditionId: conditionId.value,
|
||||
conditionDefinitionId: conditionDefinitionId.value,
|
||||
encounterDiagnosisId: encounterDiagnosisId.value,
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<span class="medicine-info"> 诊断:{{ config.diagnosisName }} </span>
|
||||
<span class="medicine-info"> 皮试:{{ row.skinTestFlag_enumText }} </span>
|
||||
<span class="medicine-info"> 注射药品:{{ row.injectFlag_enumText }} </span>
|
||||
<span class="total-amount" v-if="row.therapyEnum == '2'">
|
||||
<span class="total-amount">
|
||||
总金额:{{ row.totalPrice ? Number(row.totalPrice).toFixed(2) + ' 元' : '0.00 元' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
:controls="false"
|
||||
style="width: 70px"
|
||||
:ref="(el) => setInputRef('doseQuantity', el)"
|
||||
@input="convertValues"
|
||||
@input="() => { convertValues(); calculateTotalAmount(); }"
|
||||
@keyup.enter.prevent="handleEnter('doseQuantity')"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -110,7 +110,7 @@
|
||||
:controls="false"
|
||||
style="width: 70px; margin-left: 32px"
|
||||
:ref="(el) => setInputRef('dose', el)"
|
||||
@input="convertDoseValues"
|
||||
@input="() => { convertDoseValues(); calculateTotalAmount(); }"
|
||||
@keyup.enter.prevent="handleEnter('dose')"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -119,7 +119,7 @@
|
||||
v-model="row.doseUnitCode"
|
||||
style="width: 70px"
|
||||
placeholder=" "
|
||||
@change="convertValues"
|
||||
@change="() => { convertValues(); calculateTotalAmount(); }"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in row.unitCodeList"
|
||||
@@ -271,13 +271,17 @@
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
:ref="(el) => setInputRef('firstDose', el)"
|
||||
@input="calculateTotalAmount"
|
||||
@keyup.enter.prevent="handleEnter('firstDose')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-select v-model="row.unitCode" style="width: 70px" placeholder=" ">
|
||||
<template v-for="item in row.unitCodeList" :key="item.value">
|
||||
<el-option v-if="checkUnit(item)" :value="item.value" :label="item.label" />
|
||||
</template>
|
||||
<el-select v-model="row.doseUnitCode" style="width: 70px" placeholder=" ">
|
||||
<el-option
|
||||
v-for="item in row.unitCodeList"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
:key="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</div>
|
||||
@@ -426,6 +430,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, getCurrentInstance, nextTick, onMounted, ref, watch} from 'vue';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
interface Config {
|
||||
diagnosisName: string; // 仅用于显示
|
||||
@@ -623,7 +628,18 @@ const orgFallbackOption = (value: any) => {
|
||||
const convertValues = () => props.handlers.convertValue('doseQuantity', props.row, props.index);
|
||||
const convertDoseValues = () => props.handlers.convertValue('dose', props.row, props.index);
|
||||
const calculateTotalPrice = () => props.handlers.calculateTotal('price', props.row, props.index);
|
||||
const calculateTotalAmount = () => props.handlers.calculateTotal('amount', props.row, props.index);
|
||||
// 直接用 row 计算总金额:数量 * 单价,避免父组件索引不匹配的问题
|
||||
const calculateTotalAmount = () => {
|
||||
nextTick(() => {
|
||||
const row = props.row;
|
||||
const qty = new Decimal(row.doseQuantity || 0);
|
||||
const isMinUnit = row.unitCode == row.minUnitCode;
|
||||
const price = isMinUnit ? row.minUnitPrice : row.unitPrice;
|
||||
// 四舍五入到2位再算,与页面显示的单价一致
|
||||
const roundedPrice = new Decimal(price || 0).toDecimalPlaces(2, Decimal.ROUND_HALF_UP);
|
||||
row.totalPrice = qty.mul(roundedPrice).toFixed(6);
|
||||
});
|
||||
};
|
||||
const setInputRef = props.handlers.setInputRef;
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -380,6 +380,28 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 编辑模式下,applicationListAll 加载完成后重新回显已选项目
|
||||
watch(
|
||||
() => applicationListAll.value,
|
||||
() => {
|
||||
if (!props.editData?.requestFormId) return;
|
||||
if (!props.editData.requestFormDetailList?.length) return;
|
||||
if (!applicationListAll.value.length) return;
|
||||
|
||||
const selectedIds = [];
|
||||
props.editData.requestFormDetailList.forEach((detail) => {
|
||||
const matched = applicationListAll.value.find(
|
||||
(item) => item.adviceName === detail.adviceName
|
||||
);
|
||||
if (matched) {
|
||||
selectedIds.push(matched.adviceDefinitionId);
|
||||
}
|
||||
});
|
||||
transferValue.value = selectedIds;
|
||||
}
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
if (transferValue.value.length == 0) {
|
||||
return proxy.$message.error('请选择申请单');
|
||||
|
||||
@@ -605,13 +605,26 @@ function getListInfo(addNewRow) {
|
||||
prescriptionList.value = res.data
|
||||
.map((item) => {
|
||||
const parsedContent = JSON.parse(item.contentJson);
|
||||
// 构造 unitCodeList,确保编辑时下拉框有正确的选项
|
||||
console.log('【DEBUG】unitCode:', parsedContent?.unitCode, typeof parsedContent?.unitCode, 'unitCodeList:', JSON.stringify(parsedContent?.unitCodeList));
|
||||
const unitCodeListData = parsedContent?.unitCodeList || [
|
||||
{ value: String(parsedContent?.unitCode ?? item.unitCode ?? ''), label: parsedContent?.unitCode_dictText ?? item.unitCode_dictText ?? '', type: 'unit' },
|
||||
{ value: String(parsedContent?.doseUnitCode ?? ''), label: parsedContent?.doseUnitCode_dictText ?? '', type: 'dose' },
|
||||
{ value: String(parsedContent?.minUnitCode ?? ''), label: parsedContent?.minUnitCode_dictText ?? '', type: 'minUnit' },
|
||||
];
|
||||
return {
|
||||
...parsedContent,
|
||||
...item,
|
||||
// 🔧 修复:contentJson 中的 totalPrice 优先于 charge_item 表的 totalPrice
|
||||
// charge_item.totalPrice 可能为 0 或 null(新建医嘱时),导致总金额显示为 "-"
|
||||
totalPrice: parsedContent?.totalPrice || item.totalPrice,
|
||||
isEdit: false,
|
||||
showPopover: false,
|
||||
doseQuantity: parsedContent?.doseQuantity,
|
||||
doseUnitCode_dictText: parsedContent?.doseUnitCode_dictText,
|
||||
// 确保 unitCode 为字符串类型,与 unitCodeList 的 option value 类型一致
|
||||
unitCode: String(parsedContent?.unitCode ?? item.unitCode ?? ''),
|
||||
unitCodeList: unitCodeListData,
|
||||
// 确保 therapyEnum 被正确设置,优先使用 contentJson 中的值
|
||||
therapyEnum: String(parsedContent?.therapyEnum ?? item.therapyEnum ?? '1'),
|
||||
// 🔧 修复:确保 orgId 为 String 类型,与 organization 树的 id 类型一致
|
||||
@@ -1622,7 +1635,10 @@ function handleSaveGroup(orderGroupList) {
|
||||
conditionId: conditionId.value,
|
||||
conditionDefinitionId: conditionDefinitionId.value,
|
||||
encounterDiagnosisId: encounterDiagnosisId.value,
|
||||
diagnosisName: diagnosisName.value,
|
||||
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || mergedDetail.therapyEnum || '1',
|
||||
// 🔧 修复:确保组套医嘱的 categoryEnum 被正确映射,防止后端 NPE
|
||||
categoryEnum: mergedDetail?.categoryEnum || mergedDetail?.categoryCode || item?.categoryCode,
|
||||
};
|
||||
|
||||
// 计算价格和总量
|
||||
@@ -1663,6 +1679,8 @@ function handleSaveHistory(value) {
|
||||
encounterDiagnosisId: encounterDiagnosisId.value,
|
||||
// 确保 therapyEnum 被正确传递,默认为长期医嘱('1')
|
||||
therapyEnum: value.therapyEnum || '1',
|
||||
// 🔧 修复:历史医嘱的 categoryCode(String) 映射为后端 categoryEnum(Integer),防止 NPE
|
||||
categoryEnum: value.categoryEnum || value.categoryCode,
|
||||
contentJson: JSON.stringify({
|
||||
...value,
|
||||
therapyEnum: value.therapyEnum || '1',
|
||||
|
||||
@@ -777,6 +777,9 @@ const InputOptions = ref([
|
||||
// 日期范围 - 体征信息搜索时间
|
||||
const receptionTime = ref(null);
|
||||
|
||||
// 按钮禁用状态(保存按钮依赖)
|
||||
const buttonDisabled = ref(false);
|
||||
|
||||
// 表单数据 - 体征录入
|
||||
const formData = ref({
|
||||
recordingDate: '',
|
||||
@@ -847,26 +850,10 @@ function getPatientList() {
|
||||
patientId: props.patientId
|
||||
}
|
||||
}).then((res) => {
|
||||
|
||||
// 判断返回的数据结构
|
||||
let data = res.data;
|
||||
if (res.data && res.data.data && typeof res.data.data === 'object') {
|
||||
// 如果是嵌套结构 {data: {data: Array(3)}}
|
||||
data = res.data.data;
|
||||
} else if (res.data && typeof res.data === 'object' && res.data.data !== undefined) {
|
||||
// 如果是 {code: 200, msg: '操作成功', data: Array(3)}
|
||||
data = res.data.data;
|
||||
}
|
||||
|
||||
console.log('=== data 长度 ===', data?.length);
|
||||
|
||||
if (res.code === 200 && data) {
|
||||
console.log('=== 准备赋值 patientList.value ===');
|
||||
patientList.value = data;
|
||||
console.log('=== patientList.value 赋值后 ===', patientList.value);
|
||||
console.log('=== patientList.value 长度 ===', patientList.value?.length);
|
||||
if (res.code === 200 && res.data) {
|
||||
patientList.value = Array.isArray(res.data) ? res.data : (res.data.data || []);
|
||||
} else {
|
||||
console.error('=== 查询失败或无数据 ===');
|
||||
patientList.value = [];
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('=== 查询报错 ===', err);
|
||||
@@ -885,8 +872,22 @@ function getPatientDetial() {
|
||||
queryParams.value.patientId = props.patientId;
|
||||
// 默认查询今天的数据
|
||||
const today = moment().format('YYYY-MM-DD');
|
||||
const now = moment();
|
||||
receptionTime.value = [today, today];
|
||||
formData.value.recordingDate = today;
|
||||
// 自动填充最近的整点时间点(2/6/10/14/18/22点)
|
||||
const hour = now.hour();
|
||||
const timePoints = [2, 6, 10, 14, 18, 22];
|
||||
let nearestHour = timePoints[0];
|
||||
let minDiff = Math.abs(hour - nearestHour);
|
||||
for (const tp of timePoints) {
|
||||
const diff = Math.abs(hour - tp);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
nearestHour = tp;
|
||||
}
|
||||
}
|
||||
formData.value.timePoint = String(nearestHour).padStart(2, '0') + '00';
|
||||
// 自动加载数据
|
||||
getPatientList();
|
||||
listPatient(queryParams.value).then((res) => {
|
||||
@@ -967,6 +968,29 @@ function confirmCharge() {
|
||||
encounterId: props.patientInfo.encounterId,
|
||||
};
|
||||
|
||||
// 自动获取当前日期和时间
|
||||
const now = moment();
|
||||
if (!params.recordingDate) {
|
||||
params.recordingDate = now.format('YYYY-MM-DD');
|
||||
formData.value.recordingDate = params.recordingDate;
|
||||
}
|
||||
if (!params.timePoint) {
|
||||
// 取最近的整点时间点(2/6/10/14/18/22点)
|
||||
const hour = now.hour();
|
||||
const timePoints = [2, 6, 10, 14, 18, 22];
|
||||
let nearestHour = timePoints[0];
|
||||
let minDiff = Math.abs(hour - nearestHour);
|
||||
for (const tp of timePoints) {
|
||||
const diff = Math.abs(hour - tp);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
nearestHour = tp;
|
||||
}
|
||||
}
|
||||
params.timePoint = String(nearestHour).padStart(2, '0') + '00';
|
||||
formData.value.timePoint = params.timePoint;
|
||||
}
|
||||
|
||||
// 收集所有录入的体征数据
|
||||
const vitalSignsCode = [];
|
||||
const vitalSignsValues = [];
|
||||
@@ -1052,9 +1076,14 @@ function confirmCharge() {
|
||||
vitalSignsValues.push(params.stoolVolume);
|
||||
}
|
||||
|
||||
// 校验:没有录入任何体征数据时提示用户
|
||||
if (vitalSignsCode.length === 0) {
|
||||
proxy.$modal.msgWarning('请录入患者体征信息');
|
||||
return;
|
||||
}
|
||||
|
||||
params.vitalSignsCode = vitalSignsCode;
|
||||
params.vitalSignsValues = vitalSignsValues;
|
||||
params.recordingDate = formData.value.recordingDate || moment(new Date()).format('YYYY-MM-DD');
|
||||
|
||||
addVitalSigns(params).then(res => {
|
||||
if (res.code === 200) {
|
||||
|
||||
Reference in New Issue
Block a user