8 Commits

Author SHA1 Message Date
关羽
c96d4ac8d6 Fix Bug #504: 【住院医生工作站-临床医嘱】护士退回药品医嘱后,医生修改并保存时提示"未匹配到库存信息"
根因分析:
1. SQL查询 getRegRequestBaseInfo 未返回 medication_id/adviceDefinitionId 字段,
   退回医嘱的 adviceDefinitionId 为 null,导致库存校验查询无法匹配到库存记录
2. 退回医嘱可能缺少 locationId,严格的 locationId 匹配导致校验失败

修复方案:
1. AdviceManageAppMapper.xml:在三个UNION查询中分别添加 medication_id/device_def_id/activity_id AS advice_definition_id
2. AdviceUtils.checkInventory():
   - 过滤 null adviceDefinitionId,避免SQL查询异常
   - 所有adviceDefinitionId为null时跳过库存校验
   - 退回医嘱单个adviceDefinitionId为null时跳过该校验项
   - 添加 locationId 容错匹配(为null时跳过location匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:05 +08:00
赵云
a466719899 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
划价组套选择对话框嵌套在补费弹窗内部,缺少 append-to-body 属性导致
Dialog 被渲染在外层弹窗 DOM 内,z-index 层级被遮挡而不可见。
添加 append-to-body 使其挂载到 body 下,确保正常显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:05 +08:00
荀彧
4ffbd6070e Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:05 +08:00
华佗
9dbef1459e Fix Bug #505: 【业务逻辑缺陷】药品医嘱已由药房发药,护士仍能在"医嘱校对"模块执行"退回"操作
前后端双重校验防止已发药医嘱被退回:
1. 后端 InpatientAdviceDto 新增 dispenseStatus 字段,Mapper SQL LEFT JOIN med_medication_dispense 获取发药状态
2. 后端 adviceReject 方法增加前置校验,已发药(COMPLETED)的医嘱直接拒绝退回
3. 前端 prescriptionList.vue handleCancel 方法增加 dispenseStatus 校验,已发药医嘱点击退回时弹窗提示
2026-05-10 12:28:05 +08:00
赵云
4c3378ab05 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击“取消执行”报错 2026-05-10 12:28:05 +08:00
赵云
73b5248564 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击"取消执行"报错
取消执行时 procedureIds 数组可能为空导致后端 SQL 异常,改为从
exePerformRecordList 直接提取 procedureId,并增加空数据校验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:05 +08:00
赵云
502176f438 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:05 +08:00
关羽
87924a5f25 Fix Bug #503: 【住院发退药】发药明细与发药汇总单数据触发时机不一致,存在业务脱节风险
在 selectEncounterInfoListPage 和 selectMedicineDispenseOrderPage 两个查询中增加
summary_no IS NOT NULL 过滤条件,使发药明细单仅在护士执行"汇总发药申请"后才显示记录,
与发药汇总单保持一致的触发时机。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:06:33 +08:00
10 changed files with 299 additions and 33 deletions

View File

@@ -83,9 +83,14 @@ public class AdviceUtils {
* @return 提示信息 * @return 提示信息
*/ */
public String checkInventory(List<AdviceSaveDto> adviceSaveList) { public String checkInventory(List<AdviceSaveDto> adviceSaveList) {
// 医嘱定义ID集合 // 医嘱定义ID集合过滤null值避免SQL查询异常
List<Long> adviceDefinitionIdList List<Long> adviceDefinitionIdList
= adviceSaveList.stream().map(AdviceSaveDto::getAdviceDefinitionId).collect(Collectors.toList()); = adviceSaveList.stream().map(AdviceSaveDto::getAdviceDefinitionId)
.filter(id -> id != null).collect(Collectors.toList());
// 🔧 Bug #504 修复如果所有adviceDefinitionId都为null跳过库存校验
if (adviceDefinitionIdList.isEmpty()) {
return null;
}
// 医嘱库存集合 // 医嘱库存集合
List<AdviceInventoryDto> adviceInventoryList List<AdviceInventoryDto> adviceInventoryList
= doctorStationAdviceAppMapper.getAdviceInventory(null, adviceDefinitionIdList, = doctorStationAdviceAppMapper.getAdviceInventory(null, adviceDefinitionIdList,
@@ -99,6 +104,10 @@ public class AdviceUtils {
= this.subtractInventory(adviceInventoryList, adviceDraftInventoryList); = this.subtractInventory(adviceInventoryList, adviceDraftInventoryList);
// 检查库存 // 检查库存
for (AdviceSaveDto saveDto : adviceSaveList) { for (AdviceSaveDto saveDto : adviceSaveList) {
// 🔧 Bug #504 修复退回医嘱可能adviceDefinitionId为空跳过校验
if (saveDto.getAdviceDefinitionId() == null) {
continue;
}
boolean matched = false; boolean matched = false;
for (AdviceInventoryDto inventoryDto : adviceInventory) { for (AdviceInventoryDto inventoryDto : adviceInventory) {
// 匹配条件adviceDefinitionId, adviceTableName, locationId, lotNumber 同时相等 // 匹配条件adviceDefinitionId, adviceTableName, locationId, lotNumber 同时相等
@@ -108,10 +117,12 @@ public class AdviceUtils {
|| saveDto.getLotNumber().equals(inventoryDto.getLotNumber()); || saveDto.getLotNumber().equals(inventoryDto.getLotNumber());
boolean tableNameMatch = StringUtils.isEmpty(saveDto.getAdviceTableName()) boolean tableNameMatch = StringUtils.isEmpty(saveDto.getAdviceTableName())
|| inventoryDto.getItemTable().equals(saveDto.getAdviceTableName()); || inventoryDto.getItemTable().equals(saveDto.getAdviceTableName());
// if (saveDto.) // 🔧 Bug #504 修复退回医嘱可能locationId为空跳过location匹配
boolean locationMatch = saveDto.getLocationId() == null
|| inventoryDto.getLocationId().equals(saveDto.getLocationId());
if (inventoryDto.getItemId().equals(saveDto.getAdviceDefinitionId()) if (inventoryDto.getItemId().equals(saveDto.getAdviceDefinitionId())
&& tableNameMatch && tableNameMatch
&& inventoryDto.getLocationId().equals(saveDto.getLocationId()) && lotNumberMatch) { && locationMatch && lotNumberMatch) {
matched = true; matched = true;
// 检查库存是否充足 // 检查库存是否充足
BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity(); BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity();

View File

@@ -206,6 +206,8 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum())); e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum()));
// 请求状态 // 请求状态
e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus())); e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus()));
// 发药状态
e.setDispenseStatus_enumText(EnumUtils.getInfoByValue(DispenseStatus.class, e.getDispenseStatus()));
// 性别枚举 // 性别枚举
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum())); e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
// 计算年龄 // 计算年龄
@@ -356,6 +358,17 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
medRequestList.add(item); medRequestList.add(item);
} }
} }
// 校验药品医嘱是否已发药,已发药的医嘱不允许退回
if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
List<MedicationDispense> dispenseList = medicationDispenseService.list(
new LambdaQueryWrapper<MedicationDispense>()
.in(MedicationDispense::getMedReqId, medReqIds)
.eq(MedicationDispense::getStatusEnum, DispenseStatus.COMPLETED.getValue()));
if (!dispenseList.isEmpty()) {
return R.fail("该医嘱已发药,无法退回");
}
}
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date(); Date checkDate = new Date();
if (!serviceRequestList.isEmpty()) { if (!serviceRequestList.isEmpty()) {

View File

@@ -255,4 +255,8 @@ public class InpatientAdviceDto {
/** 总价 */ /** 总价 */
private BigDecimal totalPrice; private BigDecimal totalPrice;
/** 发药状态 */
private Integer dispenseStatus;
private String dispenseStatus_enumText;
} }

View File

@@ -153,7 +153,8 @@
ii.balance_amount AS balance_amount, ii.balance_amount AS balance_amount,
ii.account_id AS account_id, ii.account_id AS account_id,
ii.performer_check_id, ii.performer_check_id,
ii.category_code ii.category_code,
ii.dispense_status
FROM (( SELECT T1.encounter_id, FROM (( SELECT T1.encounter_id,
T1.tenant_id, T1.tenant_id,
#{medMedicationRequest} AS advice_table, #{medMedicationRequest} AS advice_table,
@@ -197,7 +198,8 @@
pra."name" AS admitting_doctor_name, pra."name" AS admitting_doctor_name,
personal_account.balance_amount, personal_account.balance_amount,
personal_account.id AS account_id, personal_account.id AS account_id,
T2.category_code T2.category_code,
mmd.status_enum AS dispense_status
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 LEFT JOIN med_medication_definition AS T2
ON T2.id = T1.medication_id ON T2.id = T1.medication_id
@@ -278,6 +280,9 @@
aa.balance_amount aa.balance_amount
) AS personal_account ) AS personal_account
ON personal_account.encounter_id = ae.id ON personal_account.encounter_id = ae.id
LEFT JOIN med_medication_dispense mmd
ON mmd.med_req_id = T1.id
AND mmd.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
AND T1.generate_source_enum = #{doctorPrescription} AND T1.generate_source_enum = #{doctorPrescription}
@@ -331,7 +336,8 @@
pra."name" AS admitting_doctor_name, pra."name" AS admitting_doctor_name,
personal_account.balance_amount, personal_account.balance_amount,
personal_account.id AS account_id, personal_account.id AS account_id,
T2.category_code T2.category_code,
NULL AS dispense_status
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.id = T1.activity_id ON T2.id = T1.activity_id

View File

@@ -96,23 +96,17 @@
INNER JOIN med_medication_request AS T5 INNER JOIN med_medication_request AS T5
ON T4.med_req_id = T5.id ON T4.med_req_id = T5.id
AND T5.delete_flag = '0' AND T5.delete_flag = '0'
WHERE EXISTS ( WHERE <if test="statusEnum == null">
SELECT 1 FROM wor_supply_request wsr T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})
WHERE wsr.type_enum = 3
AND wsr.delete_flag = '0'
AND wsr.bus_no = T4.summary_no
AND T4.summary_no IS NOT NULL
AND T4.summary_no != ''
)
AND <if test="statusEnum == null">
T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},8)
</if> </if>
<if test="statusEnum == 3"> <if test="statusEnum == 3">
T4.status_enum IN (#{inProgress},#{preparation},#{prepared},8) T4.status_enum IN (#{inProgress},#{preparation},#{prepared})
</if> </if>
<if test="statusEnum == 4"> <if test="statusEnum == 4">
T4.status_enum IN (#{completed},8) T4.status_enum = #{completed}
</if> </if>
AND T4.summary_no IS NOT NULL
AND T4.summary_no != ''
) AS ii ) AS ii
${ew.customSqlSegment} ${ew.customSqlSegment}
GROUP BY ii.encounter_id, GROUP BY ii.encounter_id,
@@ -271,6 +265,8 @@
AND T15.delete_flag = '0' AND T15.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
-- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查 -- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查
AND T1.summary_no IS NOT NULL
AND T1.summary_no != ''
AND AND
<if test="dispenseStatus == null"> <if test="dispenseStatus == null">
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared}) T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})

View File

@@ -216,7 +216,8 @@
ccd.name AS condition_definition_name, ccd.name AS condition_definition_name,
T1.therapy_enum AS therapyEnum, T1.therapy_enum AS therapyEnum,
T1.sort_number AS sort_number, T1.sort_number AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.medication_id AS advice_definition_id
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -268,7 +269,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
2 AS therapyEnum, 2 AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.device_def_id AS advice_definition_id
FROM wor_device_request AS T1 FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -317,7 +319,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
COALESCE(T1.therapy_enum, 2) AS therapyEnum, COALESCE(T1.therapy_enum, 2) AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.activity_id AS advice_definition_id
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id ON T2.ID = T1.activity_id

View File

@@ -314,6 +314,7 @@
> >
<template #title> <template #title>
<span class="cat-title">{{ cat.categoryName }}</span> <span class="cat-title">{{ cat.categoryName }}</span>
<span v-if="categoryLoadingSet.has(cat.typeId)" class="loading-dot"></span>
</template> </template>
<div <div
v-for="item in cat.items" v-for="item in cat.items"
@@ -329,6 +330,9 @@
</el-checkbox> </el-checkbox>
<span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span> <span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span>
</div> </div>
<div v-if="categoryLoadingSet.has(cat.typeId)" class="category-loading-hint">
加载中...
</div>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
@@ -517,6 +521,7 @@ const rules = {
const categoryList = ref([]); // 原始分类+项目数据 const categoryList = ref([]); // 原始分类+项目数据
const dictSearchKey = ref(''); const dictSearchKey = ref('');
const activeNames = ref(''); // 当前展开的折叠项 const activeNames = ref(''); // 当前展开的折叠项
const categoryLoadingSet = ref(new Set()); // Bug #500: 正在加载方法的分类集合
const allMethods = ref([]); const allMethods = ref([]);
@@ -632,9 +637,14 @@ const availableMethods = computed(() => {
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空 // 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
// #428: 分类展开时联动加载检查方法 // #428: 分类展开时联动加载检查方法
// Bug #500: 使用 categoryLoadingSet 替代 dictLoading避免切换分类时整个区域闪烁
async function handleCategoryExpand(cat) { async function handleCategoryExpand(cat) {
if (!cat || !cat.typeName) return; if (!cat || !cat.typeName) return;
// 如果已加载过或正在加载中,跳过
if ((cat.methods && cat.methods.length > 0) || categoryLoadingSet.value.has(cat.typeId)) return;
categoryLoadingSet.value.add(cat.typeId);
try { try {
const res = await searchCheckMethod({ checkType: cat.typeName }); const res = await searchCheckMethod({ checkType: cat.typeName });
let data = res?.data?.data || res?.data || res?.rows || res; let data = res?.data?.data || res?.data || res?.rows || res;
@@ -655,14 +665,16 @@ async function handleCategoryExpand(cat) {
} }
} catch (err) { } catch (err) {
console.error('加载分类检查方法失败', err); console.error('加载分类检查方法失败', err);
} finally {
categoryLoadingSet.value.delete(cat.typeId);
} }
} }
async function handleCollapseChange(activeName) { // Bug #500: 改为非 async 函数,避免 el-collapse 的 @change 等待异步完成导致抖动
// 当折叠面板展开时,加载对应分类的检查方法 function handleCollapseChange(activeName) {
if (activeName) { if (activeName) {
const cat = filteredCategoryList.value.find(c => c.typeId == activeName); const cat = filteredCategoryList.value.find(c => c.typeId == activeName);
if (cat && (!cat.methods || cat.methods.length === 0)) { if (cat && (!cat.methods || cat.methods.length === 0)) {
await handleCategoryExpand(cat); handleCategoryExpand(cat); // 异步加载,不 await
} }
} }
} }
@@ -1309,6 +1321,20 @@ defineExpose({ getList });
font-weight: 500; font-weight: 500;
color: #303133; color: #303133;
} }
/* Bug #500: 分类加载中的小圆点动画 */
.loading-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #409eff;
margin-left: 6px;
animation: dotPulse 1s ease-in-out infinite;
}
@keyframes dotPulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.item-row { .item-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1448,10 +1474,24 @@ defineExpose({ getList });
height: auto; height: auto;
line-height: 1.5; line-height: 1.5;
} }
/* Bug #500: 折叠内容添加平滑过渡动画,避免切换时高度跳变 */
:deep(.el-collapse-item__content) { :deep(.el-collapse-item__content) {
padding-bottom: 4px; padding-bottom: 4px;
transition: all 0.3s ease;
} }
/* Bug #500: 折叠面板展开/收起动画使用 will-change 优化性能 */
:deep(.el-collapse-item__wrap) { :deep(.el-collapse-item__wrap) {
border: none; border: none;
will-change: height;
}
:deep(.el-collapse-item) {
transition: margin 0.2s ease;
}
/* Bug #500: 分类加载中提示样式 */
.category-loading-hint {
color: #909399;
font-size: 12px;
text-align: center;
padding: 8px 0;
} }
</style> </style>

View File

@@ -40,7 +40,7 @@
</template> </template>
</el-input> </el-input>
</div> </div>
<el-button type="primary">划价组套</el-button> <el-button type="primary" @click="openGroupSetDialog">划价组套</el-button>
</div> </div>
<!-- 弹窗内容 - 左右布局 --> <!-- 弹窗内容 - 左右布局 -->
<div style="display: flex; gap: 20px; height: 70vh"> <div style="display: flex; gap: 20px; height: 70vh">
@@ -147,6 +147,7 @@
<el-select <el-select
v-model="scope.row.selectUnitCode" v-model="scope.row.selectUnitCode"
placeholder="单位" placeholder="单位"
filterable
style="width: 100px" style="width: 100px"
@change="unitCodeChange(scope.row)" @change="unitCodeChange(scope.row)"
> >
@@ -171,12 +172,13 @@
v-if="scope.row.adviceType == 3" v-if="scope.row.adviceType == 3"
clearable clearable
filterable filterable
:filter-method="(val) => filterOptions(val, scope.row, 'departmentOptions')"
v-model="scope.row.positionId" v-model="scope.row.positionId"
placeholder="选择科室" placeholder="选择科室"
style="width: 220px" style="width: 220px"
> >
<el-option <el-option
v-for="dept in departmentOptions" v-for="dept in getFilteredOptions(scope.row, 'departmentOptions')"
:key="dept.id" :key="dept.id"
:label="dept.name" :label="dept.name"
:value="dept.id" :value="dept.id"
@@ -186,12 +188,13 @@
v-if="scope.row.adviceType == 2" v-if="scope.row.adviceType == 2"
clearable clearable
filterable filterable
:filter-method="(val) => filterOptions(val, scope.row, 'locationOptions')"
v-model="scope.row.positionId" v-model="scope.row.positionId"
placeholder="选择药房/耗材房" placeholder="选择药房/耗材房"
style="width: 220px" style="width: 220px"
> >
<el-option <el-option
v-for="dept in locationOptions" v-for="dept in getFilteredOptions(scope.row, 'locationOptions')"
:key="dept.value" :key="dept.value"
:label="dept.label" :label="dept.label"
:value="dept.value" :value="dept.value"
@@ -247,6 +250,49 @@
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
<!-- 划价组套选择对话框 -->
<el-dialog v-model="groupSetDialogVisible" title="划价组套选择" width="600px" :close-on-click-modal="false" append-to-body>
<div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px">
<el-input
v-model="groupSetSearchText"
placeholder="请输入组套名称搜索"
clearable
style="width: 250px"
@keyup.enter="loadGroupSets"
>
<template #append>
<el-button icon="Search" @click="loadGroupSets" />
</template>
</el-input>
<el-radio-group v-model="groupSetRange" @change="loadGroupSets">
<el-radio-button :label="1">个人</el-radio-button>
<el-radio-button :label="2">科室</el-radio-button>
<el-radio-button :label="3">全院</el-radio-button>
</el-radio-group>
</div>
<el-table
:data="groupSetList"
v-loading="groupSetLoading"
highlight-current-row
border
stripe
@current-change="handleGroupSetSelect"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="组套名称" prop="name" min-width="180" show-overflow-tooltip />
<el-table-column label="使用范围" prop="rangeCode_dictText" width="100" align="center" />
<el-table-column label="明细数量" width="100" align="center">
<template #default="scope">
{{ scope.row.detailList?.length || 0 }} 项
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px; text-align: right">
<el-button @click="groupSetDialogVisible = false">取消</el-button>
<el-button type="primary" @click="applyGroupSet" :disabled="!selectedGroupSet">应用</el-button>
</div>
</el-dialog>
</template> </template>
<script setup> <script setup>
@@ -254,6 +300,7 @@ import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from 'vue
import {ElMessage} from 'element-plus'; import {ElMessage} from 'element-plus';
import {formatDateStr} from '@/utils/index'; import {formatDateStr} from '@/utils/index';
import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList} from './api.js'; import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList} from './api.js';
import {getOrderGroup} from '@/views/doctorstation/components/api.js';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -312,6 +359,8 @@ const locationOptions = ref([]);
const searchText = ref(''); const searchText = ref('');
const userId = ref(''); const userId = ref('');
const orgId = ref(''); const orgId = ref('');
// 下拉框模糊搜索关键字(按行存储)
const filterKeywords = ref({});
const queryParams = ref({ const queryParams = ref({
pageSize: 100, pageSize: 100,
pageNum: 1, pageNum: 1,
@@ -389,6 +438,14 @@ const submitData = reactive({
}); });
const adviceLoading = ref(false); const adviceLoading = ref(false);
// 划价组套相关
const groupSetDialogVisible = ref(false);
const groupSetList = ref([]);
const groupSetLoading = ref(false);
const groupSetSearchText = ref('');
const groupSetRange = ref(2);
const selectedGroupSet = ref(null);
// 【优化核心】计算总金额 - 使用计算属性实现实时更新 // 【优化核心】计算总金额 - 使用计算属性实现实时更新
const totalAmount = computed(() => { const totalAmount = computed(() => {
return feeItemsList.value.reduce((sum, item) => { return feeItemsList.value.reduce((sum, item) => {
@@ -452,6 +509,25 @@ function getDiseaseInitLoc() {
locationOptions.value = response.data.locationOptions; locationOptions.value = response.data.locationOptions;
}); });
} }
// 下拉框模糊搜索过滤
function filterOptions(val, row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey;
filterKeywords.value[key] = val;
}
function getFilteredOptions(row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey;
const keyword = filterKeywords.value[key];
const options = optionsKey === 'departmentOptions' ? departmentOptions.value : locationOptions.value;
if (!keyword || keyword.trim() === '') {
return options;
}
const lower = keyword.toLowerCase();
return options.filter(item => {
const name = (item.name || item.label || '').toLowerCase();
const id = String(item.id || item.value || '').toLowerCase();
return name.includes(lower) || id.includes(lower);
});
}
// 获取组套类型文本 // 获取组套类型文本
function getItemType_Text(type) { function getItemType_Text(type) {
const map = { 2: '耗材', 3: '诊疗' }; const map = { 2: '耗材', 3: '诊疗' };
@@ -527,14 +603,24 @@ function selectChange(row) {
const price = row.priceList?.[0]?.price || 0; const price = row.priceList?.[0]?.price || 0;
//获取大小单位 //获取大小单位
const uniqueUnitCodes = getUnitCodeOptions(row); const uniqueUnitCodes = getUnitCodeOptions(row);
// 设置默认执行科室/位置
let defaultPositionId = undefined;
if (row.adviceType === 3 && departmentOptions.value.length > 0) {
// 诊疗:优先使用患者所在科室,否则取第一个科室
defaultPositionId = departmentOptions.value.find(d => d.id === props.patientInfo.organizationId)?.id
|| departmentOptions.value[0]?.id;
} else if (row.adviceType === 2 && locationOptions.value.length > 0) {
// 耗材:默认取第一个药房/耗材房
defaultPositionId = locationOptions.value[0]?.value;
}
//插入费用列表 //插入费用列表
feeItemsList.value.push({ feeItemsList.value.push({
...row, ...row,
uniqueUnitCodes: uniqueUnitCodes, uniqueUnitCodes: uniqueUnitCodes,
unitPrice: (price / (row.partPercent || 1)).toFixed(6), // 根据拆零比计算单价 unitPrice: (price / (row.partPercent || 1)).toFixed(6), // 根据拆零比计算单价
quantity: 1, quantity: 1,
// positionId: row.positionId === null || row.positionId === undefined ? orgId : row.positionId, // 默认执行科室 positionId: defaultPositionId, // 默认执行科室/位置
selectUnitCode: row.minUnitCode, // 默认选择小单位 selectUnitCode: String(row.minUnitCode || ''), // 默认选择小单位,确保字符串类型
}); });
} }
@@ -648,6 +734,94 @@ function resetData() {
searchText.value = ''; searchText.value = '';
executeTime.value = ''; executeTime.value = '';
} }
// 划价组套相关功能
function openGroupSetDialog() {
groupSetDialogVisible.value = true;
groupSetSearchText.value = '';
selectedGroupSet.value = null;
loadGroupSets();
}
function loadGroupSets() {
groupSetLoading.value = true;
getOrderGroup({ organizationId: orgId.value, searchKey: groupSetSearchText.value })
.then((res) => {
const data = res?.data || {};
if (groupSetRange.value === 1) {
groupSetList.value = data.personalList || [];
} else if (groupSetRange.value === 2) {
groupSetList.value = data.organizationList || [];
} else {
groupSetList.value = data.hospitalList || [];
}
})
.catch(() => {
console.warn('组套列表加载失败(可能无权限)');
groupSetList.value = [];
})
.finally(() => {
groupSetLoading.value = false;
});
}
function handleGroupSetSelect(row) {
selectedGroupSet.value = row;
}
function applyGroupSet() {
if (!selectedGroupSet.value) {
ElMessage.warning('请先选择一个组套');
return;
}
const detailList = selectedGroupSet.value.detailList;
if (!detailList || detailList.length === 0) {
ElMessage.warning('该组套没有明细项');
return;
}
let successCount = 0;
detailList.forEach((item) => {
const orderDetail = item.orderDetailInfos || {};
const feeItem = {
adviceDefinitionId: item.orderDefinitionId || orderDetail.adviceDefinitionId,
adviceName: orderDetail.adviceName || item.orderDefinitionName || '未知项目',
adviceType: orderDetail.adviceType,
unitPrice: orderDetail.unitPrice,
minUnitPrice: orderDetail.minUnitPrice,
inventoryList: orderDetail.inventoryList || [],
priceList: orderDetail.priceList || [],
partPercent: orderDetail.partPercent || 1,
positionId: item.positionId || orderDetail.positionId,
defaultLotNumber: orderDetail.defaultLotNumber,
unitCode: item.unitCode || orderDetail.unitCode,
unitCode_dictText: item.unitCodeName || orderDetail.unitCode_dictText,
minUnitCode: orderDetail.minUnitCode,
doseUnitCode: orderDetail.doseUnitCode,
categoryCode: orderDetail.categoryCode,
pharmacologyCategoryCode: orderDetail.pharmacologyCategoryCode,
partAttributeEnum: orderDetail.partAttributeEnum,
chargeItemDefinitionId: orderDetail.chargeItemDefinitionId,
adviceTableName: orderDetail.adviceTableName,
methodCode: item.methodCode || orderDetail.methodCode,
rateCode: item.rateCode || orderDetail.rateCode,
dose: item.dose || orderDetail.dose,
doseQuantity: item.doseQuantity,
dispensePerDuration: item.dispensePerDuration || orderDetail.dispensePerDuration,
dosageInstruction: orderDetail.dosageInstruction,
skinTestFlag: orderDetail.skinTestFlag,
injectFlag: orderDetail.injectFlag,
quantity: item.quantity || 1,
chrgitmLv_dictText: orderDetail.chrgitmLv_dictText,
volume: orderDetail.volume,
};
selectChange(feeItem);
successCount++;
});
if (successCount > 0) {
ElMessage.success(`已添加 ${successCount} 项组套费用`);
}
groupSetDialogVisible.value = false;
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -67,6 +67,9 @@
</el-button> </el-button>
</div> </div>
</div> </div>
</div>
</template>
</div>
<div <div
style="padding: 10px; background-color: #eef9fd; height: 100%; overflow-y: auto" style="padding: 10px; background-color: #eef9fd; height: 100%; overflow-y: auto"
v-loading="loading" v-loading="loading"
@@ -497,8 +500,15 @@ function handleCancel() {
let list = getSelectRows(); let list = getSelectRows();
let producerIds = []; let producerIds = [];
list.forEach((item) => { list.forEach((item) => {
// 从 exePerformRecordList 直接提取 procedureId确保取消执行时数据完整
const procedureIds = (item.exePerformRecordList || []).map((record) => record.procedureId);
if (procedureIds.length === 0 && (!item.procedureIds || item.procedureIds.length === 0)) {
proxy.$modal.msgError('请选择已执行的医嘱记录');
return;
}
const ids = procedureIds.length > 0 ? procedureIds : item.procedureIds;
producerIds.push( producerIds.push(
...item.procedureIds.map((value) => { ...ids.map((value) => {
return { return {
procedureId: value, procedureId: value,
therapyEnum: item.therapyEnum, therapyEnum: item.therapyEnum,
@@ -506,6 +516,9 @@ function handleCancel() {
}) })
); );
}); });
if (producerIds.length === 0) {
return;
}
adviceCancel({ adviceExecuteDetailList: producerIds }).then((res) => { adviceCancel({ adviceExecuteDetailList: producerIds }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess(res.msg || '取消执行成功'); proxy.$modal.msgSuccess(res.msg || '取消执行成功');

View File

@@ -288,6 +288,12 @@ function handleCheck() {
function handleCancel() { function handleCancel() {
let list = getSelectRows(); let list = getSelectRows();
if (list.length > 0) { if (list.length > 0) {
// 校验已发药的医嘱不允许退回
let dispensedItems = list.filter(item => item.dispenseStatus === 4);
if (dispensedItems.length > 0) {
proxy.$message.error('该医嘱已发药,无法退回');
return;
}
cancel(list).then((res) => { cancel(list).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess(res.msg); proxy.$modal.msgSuccess(res.msg);