10 Commits

Author SHA1 Message Date
赵云
df2dbccbc3 Fix Bug #435: 门诊手术安排:编辑弹窗中"费用类别"字段数据未回显
根因:OpCreateScheduleDto 缺少 feeType 字段,导致新建手术安排时 BeanUtils.copyProperties 无法复制该字段,
保存到数据库后 fee_type 为空字符串/null,编辑时详情查询返回 null 导致前端不显示。

修复:在 OpCreateScheduleDto 新增 feeType 字段,使创建流程完整传递费用类别数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:28:07 +08:00
荀彧
23b993600d Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
- 分类对象初始化时增加 methods: [],确保 Vue 响应式追踪分类下检查方法的加载
- handleMethodSelect 创建新项目时复制 cat.methods 全部方法数组(原只放单个方法),允许用户在右侧面板切换其他方法
- handleMethodSelect 新增/更新项目时同步 packageName 字段,确保 toggleItemExpand 能通过名称查找并加载套餐明细

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:13:47 +08:00
赵云
13025d8adb Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
- 已选择面板的套餐项增加"套餐"标签,便于用户识别
- 展开/收起图标改为 ArrowRight 旋转样式,符合标准交互习惯
- toggleItemExpand 函数增加 packageName 兜底判断,不强制依赖 isPackage 标记
- loadPackageDetailsForItem 添加 loading 状态和更健壮的 packageId 解析逻辑
- 新增 expanded-content 和 package-loading-hint CSS 样式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:13:47 +08:00
关羽
ae9450b85f Fix Bug #452: 领用出库模块选择药品时提示"仓库数量为0,无法调用",与实际库存数据不符
根因:药品目录列表中返回的lotNumber是任意仓库中的批号,但getCount查询时用该lotNumber过滤用户选择的仓库库存。若该批号在目标仓库不存在(但其他批号存在),则返回0条记录导致误报"仓库数量为0"。
修复:在领用出库的handleLocationClick中移除getCount的lotNumber参数,改为查询该药品在所选仓库的所有批号库存。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:13:47 +08:00
荀彧
0d32364ce4 feat: 手术管理列表点击行高亮 (highlight-current-row) 2026-05-14 10:13:47 +08:00
关羽
fa3fb44b14 Fix Bug #446: 【手术管理-门诊手术安排】临时医嘱生成后界面非法关闭且按钮名称/功能显示不一致
根因: handleMedicalAdvice 中盲目重置 temporarySigned.value = false,导致重新打开医嘱弹窗时按钮状态错误。

修复:
1. index.vue: 根据已有医嘱数据是否有 requestId 来决定 temporarySigned 状态,而非盲目重置
2. temporaryMedical.vue: 新增 allItemsSubmitted 计算属性,当所有计费药品已提交(requestId)时显示"已签发"按钮并禁用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:21:31 +08:00
赵云
0c98ca2485 Fix Bug #402: 住院医生站诊断录入:点击保存诊断后,列表出现重复记录且部分条目元数据缺失
根因分析:
1. 前端保存按钮无防重复点击保护,连续点击会发送多个请求
2. 保存成功后前端使用本地排序数据更新,未从服务器重新加载,导致前后端数据不一致
3. 后端 saveDoctorDiagnosis 保存后未回写 encounterDiagnosisId,后续保存无法正确更新已有记录

修复方案:
- 前端:在 handleSaveDiagnosis 入口增加 isSaving 守卫,防止重复提交
- 前端:保存成功后调用 getList() 从服务器重新加载数据,确保前后端一致
- 后端:saveOrUpdate 后回写 encounterDiagnosisId 到返回参数,前端可跟踪记录ID

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:21:31 +08:00
关羽
b5b490dabb Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,"检查明细"标签页显示"暂无数据"
根因:handleRowClick 中 const resp = res.data || res 对 Axios 拦截器已解包的响应
进行二次解包,导致 resp 被赋为 ExamApply 实体对象(不含 items),后续 items 提取
逻辑始终返回空数组,明细列表无法加载。

修复:用 res.code !== undefined 判定 res 是否已是 AjaxResult 体,若是则直接使用,
否则再执行 res.data 解包。items 和数据提取统一从正确层级取值,避免二次解包。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:17:32 +08:00
关羽
b13ab03912 Fix Bug #443: 手术计费:点击"签发"耗材时异常报错
根因分析:getRequestBaseInfo SQL查询的UNION 2(门诊术中计费耗材)缺少generate_source_enum过滤条件,导致手术计费弹窗显示所有来源的耗材(包括医生站开立的项目)。当用户尝试签发非手术计费创建的耗材时,后端handDevice处理失败。

修复内容:
1. 后端SQL:在UNION 2的WHERE子句中添加generate_source_enum过滤,确保手术计费弹窗仅显示手术计费来源的耗材
2. 前端JS:handleSave函数补充.then/.catch错误处理,避免显示笼统的"后端程序异常",改为展示具体错误信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:17:32 +08:00
关羽
3819be1636 Fix Bug #405: 住院医生工作站:临床医嘱保存成功后,医嘱条目仍处于可编辑状态(未锁定展示)
根因:handleSaveBatch 保存成功后,原有修复通过 uniqueKey 查找 prescriptionList 中的行并设置 isEdit=false,
但由于 saveList 中的 item 本身就是 prescriptionList 中对象的同一引用,通过 find(uniqueKey) 查找存在匹配失败的风险。

修复:直接对 saveList 中的对象引用设置 isEdit=false(同引用无需查找),并兜底遍历所有 statusEnum==1 的行锁定。
同时清空 expandOrder 展开状态,确保医嘱行完全回到只读展示模式。
2026-05-14 08:58:21 +08:00
11 changed files with 116 additions and 43 deletions

View File

@@ -261,6 +261,11 @@ public class OpCreateScheduleDto {
*/
private String remark;
/**
* 费用类别
*/
private String feeType;
/**
* 创建时间
*/

View File

@@ -261,8 +261,10 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
// 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
// 回写就诊诊断ID供前端后续更新使用
saveDiagnosisChildParam.setEncounterDiagnosisId(encounterDiagnosis.getId());
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
return R.ok(saveDiagnosisParam, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
}

View File

@@ -583,6 +583,9 @@
LEFT JOIN adm_location AS AL ON AL.id = DR.perform_location AND AL.delete_flag = '0'
WHERE CI.delete_flag = '0'
AND CI.service_table = 'wor_device_request'
<if test="generateSourceEnum != null">
AND (DR.generate_source_enum IS NULL OR DR.generate_source_enum = #{generateSourceEnum})
</if>
<if test="historyFlag == '0'.toString()">
AND CI.encounter_id = #{encounterId}
</if>

View File

@@ -1059,7 +1059,12 @@ function handleSave() {
groupIndexList.value = []
groupList.value = []
nextId.value = 1;
} else {
proxy.$modal.msgError(res?.msg || '签发失败,请重试');
}
}).catch((error) => {
console.error('签发失败:', error);
proxy.$modal.msgError(error?.response?.data?.msg || error?.message || '签发失败,请重试');
});
}

View File

@@ -413,29 +413,33 @@
:key="idx"
class="selected-item-card"
>
<!-- Bug #384修复: 项目卡片头部可展开/收起 -->
<!-- Bug #384修复 + #426修复: 项目卡片头部可展开/收起 -->
<div class="card-header" @click="toggleItemExpand(item)">
<el-tag v-if="item.isPackage || item.packageName" size="small" type="warning" style="margin-right: 4px; flex-shrink: 0;">套餐</el-tag>
<span class="card-name">{{ item.name }}</span>
<span class="card-price">¥{{ item.price }}</span>
<!-- 展开图标 -->
<el-icon :class="['expand-icon', { expanded: item.expanded }]">
<ArrowDown v-if="!item.expanded" />
<ArrowUp v-if="item.expanded" />
<!-- 展开/收起图标 -->
<el-icon class="expand-icon" :class="{ expanded: item.expanded }">
<ArrowRight />
</el-icon>
<!-- 删除按钮 -->
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- Bug #428修复: 展开后显示套餐明细或检查方法 -->
<div v-if="item.expanded">
<!-- Bug #428修复 + #426修复: 展开后显示套餐明细或检查方法 -->
<div v-show="item.expanded" class="expanded-content">
<!-- 显示套餐明细 -->
<div v-if="item.packageDetails && item.packageDetails.length > 0" class="package-details-list">
<div v-if="(item.isPackage || item.packageName) && item.packageDetails && item.packageDetails.length > 0" class="package-details-list">
<div class="detail-row" v-for="detail in item.packageDetails" :key="detail.id">
<span class="detail-name">{{ detail.name }}</span>
<span class="detail-info">数量: {{ detail.quantity }} 单价: ¥{{ detail.price }}</span>
</div>
</div>
<!-- 套餐明细加载中 -->
<div v-else-if="(item.isPackage || item.packageName) && item.packageDetailsLoading" class="package-loading-hint">
加载中...
</div>
<!-- 显示检查方法 -->
<div v-else-if="item.methods && item.methods.length > 0" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-option">
@@ -473,7 +477,7 @@
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import { Printer, Delete, ArrowDown, ArrowUp, Close, ArrowRight } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user';
import request from '@/utils/request';
import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType';
@@ -592,11 +596,13 @@ async function loadPackageDetails(row, treeNode, resolve) {
}
}
// #428修复: 为已选择项目加载套餐明细通过packageId或packageName查询
// #428修复 + #426修复: 为已选择项目加载套餐明细通过packageId或packageName查询
async function loadPackageDetailsForItem(item) {
if (!item.isPackage || (!item.packageId && !item.packageName)) {
// 只要有 packageName 就认为是套餐,不强制要求 isPackage 或 packageId
if (!item.packageName && !item.packageId) {
return;
}
item.packageDetailsLoading = true;
try {
let packageId = item.packageId;
if (!packageId && item.packageName) {
@@ -612,6 +618,10 @@ async function loadPackageDetailsForItem(item) {
}
packageId = packages[0].id;
}
if (!packageId) {
item.packageDetails = [];
return;
}
const res = await request({
url: `/system/package/${packageId}/details`,
method: 'get'
@@ -630,6 +640,8 @@ async function loadPackageDetailsForItem(item) {
} catch (err) {
console.error('加载套餐明细失败:', err);
item.packageDetails = [];
} finally {
item.packageDetailsLoading = false;
}
}
const detailTableRef = ref(null);
@@ -912,7 +924,8 @@ async function loadCategoryList() {
categoryName: t.name,
// “检查类型管理”里配置的执行科室(图三)
performDeptName: t.department || '',
items: []
items: [],
methods: [] // #428修复: 初始化 methods 数组,确保 Vue 响应式追踪
});
}
const unclassified = [];
@@ -1136,17 +1149,19 @@ function handleRowClick(row) {
selectedItems.value = [];
activeDetailTab.value = 'applyForm';
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
const resp = res.data || res;
// Bug #408修复: items 在 AjaxResult 顶层(res.items / resp.items),不在 ExamApply 对象内
// 防御性提取:优先取顶层 items兼容嵌套在 resp.data.items 的情况
let rawItems = res.items || resp.items;
if (!rawItems && resp.data && typeof resp.data === 'object') {
rawItems = resp.data.items;
}
rawItems = rawItems || [];
const d = resp.data || resp;
if (d) Object.assign(form, d);
if (Array.isArray(rawItems) && rawItems.length > 0) {
// 响应结构判定Axios拦截器对 code===200 返回 res.dataAjaxResult体
// 但某些情况下可能返回完整 Axios 响应 {data: AjaxResult}。
// 用 res.code 判定是否已是 AjaxResult 体,避免二次解包导致 items 丢失。
const isAjaxResult = res && typeof res === 'object' && res.code !== undefined;
const ajaxBody = isAjaxResult ? res : (res.data || res);
// items 在 AjaxResult 顶层data 字段是 ExamApply 实体
const rawItems = Array.isArray(ajaxBody.items) ? ajaxBody.items : [];
const detailData = ajaxBody.data || {};
if (detailData && typeof detailData === 'object') Object.assign(form, detailData);
if (rawItems.length > 0) {
try {
// 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(rawItems.map(async m => {
@@ -1253,6 +1268,7 @@ async function handleMethodSelect(checked, method, cat) {
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId;
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
// 预加载套餐明细
loadPackageDetailsForItem(existingItem);
}
@@ -1280,12 +1296,13 @@ async function handleMethodSelect(checked, method, cat) {
checkType: cat.typeName,
nationalCode: targetItem.nationalCode || '',
checked: true,
methods: [method],
methods: cat.methods || [method], // #428修复: 复制分类下全部方法,允许用户切换
selectedMethod: method,
expanded: false,
// 从方法中获取套餐信息(优先级高于项目本身的 packageName
// 从方法或项目中获取套餐信息
isPackage: !!method.packageId || !!targetItem.packageName,
packageId: method.packageId || targetItem.packageId || null
packageId: method.packageId || targetItem.packageId || null,
packageName: method.packageName || targetItem.packageName || null // #428修复: 复制 packageName确保套餐明细可加载
};
selectedItems.value.push(newItem);
@@ -1394,11 +1411,11 @@ async function handleItemSelect(checked, item, cat) {
// Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
}
// Bug #384修复: 展开/收起项目卡片
// Bug #384修复 + #426修复: 展开/收起项目卡片
async function toggleItemExpand(item) {
item.expanded = !item.expanded;
// 如果是展开且该项目是套餐,加载套餐明细
if (item.expanded && item.isPackage && (!item.packageDetails || item.packageDetails.length === 0)) {
// 如果是展开且该项目是套餐(通过 isPackage 或 packageName 判断),加载套餐明细
if (item.expanded && (item.isPackage || item.packageName) && (!item.packageDetails || item.packageDetails.length === 0) && !item.packageDetailsLoading) {
await loadPackageDetailsForItem(item);
}
}
@@ -1810,10 +1827,24 @@ defineExpose({ getList });
font-size: 12px;
color: #909399;
transition: transform 0.2s;
transform: rotate(0deg);
}
.expand-icon.expanded {
transform: rotate(180deg);
transform: rotate(90deg);
}
/* Bug #426修复: 展开内容容器 */
.expanded-content {
overflow: hidden;
}
/* Bug #426修复: 套餐明细加载提示 */
.package-loading-hint {
padding: 8px 10px;
font-size: 11px;
color: #c0c4cc;
text-align: center;
}
/* Bug #428修复: 套餐明细列表样式 */

View File

@@ -568,6 +568,11 @@ function handleMaindise(value, index) {
* 保存诊断
*/
function handleSaveDiagnosis() {
// 防止重复点击保存
if (isSaving.value) {
return;
}
for (let index = 0; index < (form.value.diagnosisList || []).length; index++) {
const item = form.value.diagnosisList[index];
if (!item.diagSrtNo) {
@@ -600,7 +605,7 @@ function handleSaveDiagnosis() {
// 步骤2重新分配连续的序号从1开始
sortedList.forEach((item, index) => {
item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序
item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序
});
// 步骤3提交排序后的数据
@@ -610,12 +615,12 @@ function handleSaveDiagnosis() {
diagnosisChildList: sortedList,
}).then((res) => {
if (res.code === 200) {
// 步骤4更新本地数据使用全新对象防止响应式问题
form.value.diagnosisList = sortedList.map(item => ({ ...item }));
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) {

View File

@@ -1477,11 +1477,18 @@ function handleSaveBatch() {
.then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
// 修复#405:保存成功后重置所有待保存行的 isEdit 为 false锁定医嘱不再编辑
// 修复 Bug #405保存成功后锁定所有待保存行,避免医嘱条目仍处于可编辑状态
// saveList 中的 item 与 prescriptionList 是同一对象引用,直接修改即可
saveList.forEach(item => {
const row = prescriptionList.value.find(r => r.uniqueKey === item.uniqueKey);
if (row) row.isEdit = false;
item.isEdit = false;
});
// 兜底:锁定所有 statusEnum == 1 的行,确保没有遗漏
prescriptionList.value.forEach(row => {
if (row.statusEnum == 1) {
row.isEdit = false;
}
});
expandOrder.value = [];
getListInfo(false);
nextId.value = 1;
isSaving.value = false;

View File

@@ -1009,8 +1009,6 @@ function handleLocationClick(item, row, index) {
getCount({
itemId: form.purchaseinventoryList[index].itemId,
orgLocationId: form.purchaseinventoryList[index].sourceLocationId,
// objLocationId:purposeLocationId,
lotNumber: form.purchaseinventoryList[index].lotNumber,
}).then((res) => {
if (res.data && res.data.length > 0) {
form.purchaseinventoryList[index].itemTable = res.data[0].itemTable || '';

View File

@@ -76,7 +76,7 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="surgeryList" row-key="id" :row-class-name="getRowClassName">
<el-table v-loading="loading" :data="surgeryList" row-key="id" :row-class-name="getRowClassName" highlight-current-row @current-change="handleCurrentChange">
<!-- 申请日期datetime - 2025-09-19 14:15:00 - 不可操作 -->
<el-table-column label="申请日期" align="center" prop="createTime" width="160">
<template #default="scope">
@@ -1405,6 +1405,12 @@ function getRowClassName({ row }) {
return ''
}
// 当前选中行(高亮)
const currentRow = ref(null)
function handleCurrentChange(row) {
currentRow.value = row
}
// 时间格式化函数
function parseTime(time, pattern) {
if (!time) return ''

View File

@@ -1528,7 +1528,11 @@ function handleMedicalAdvice(row) {
// 先清空旧数据
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
temporarySigned.value = false // 🔧 重置签名状态
// 🔧 修复 Bug #446: 如果是同一 encounter 且已有提交的医嘱(有 requestId保留签名状态
const hasSubmittedAdvices = temporaryAdvices.value.length > 0 &&
temporaryAdvices.value[0]?.originalMedicine?.encounterId === row.visitId &&
temporaryAdvices.value.some(a => a.originalMedicine?.requestId);
temporarySigned.value = hasSubmittedAdvices; // 修复:根据已有数据状态设置,而非盲目重置
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
// 调用计费接口获取数据

View File

@@ -147,9 +147,10 @@
<el-button class="cancel-btn" @click="handleCancel">取消</el-button>
<el-button
class="sign-btn"
:disabled="allItemsSubmitted"
@click="handleSignAndSubmit"
>
{{ isSigned ? '提交医嘱' : '一键签名并生成医嘱' }}
{{ allItemsSubmitted ? '已签发' : (isSigned ? '提交医嘱' : '一键签名并生成医嘱') }}
</el-button>
</div>
</div>
@@ -310,6 +311,12 @@ const getMethodCodeDict = computed(() => {
return dict
})
// 🔧 修复 Bug #446: 检查计费药品是否已全部提交(有 requestId用于区分"首次签名"和"已提交重开"
const allItemsSubmitted = computed(() => {
const meds = props.billingMedicines || []
return meds.length > 0 && meds.every(m => m.requestId)
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)