3 Commits

Author SHA1 Message Date
wangjian963
f3d011951b 78 增加门诊医生开立检验申请单--对搜索项目区实现懒加载,以及动态搜索。 2026-03-13 18:34:14 +08:00
7dc76d7b59 fix(advice): 修复禅道 Bug #147 - 添加耗材处理逻辑
- 在 saveRegAdvice 方法中添加耗材列表过滤
- 添加 handDevice 方法调用处理耗材请求
- 实现完整的 handDevice 方法处理耗材的保存、签发和删除操作
- 添加必要的导入:ActivityDefinition、DeviceRequest、IDeviceRequestService、IDeviceDispenseService
2026-03-13 12:10:24 +08:00
b2dec2667a fix(document): 修复文书定义树形列表查询逻辑
- 添加了对organizationId和useRanges参数的空值检查和日志警告
- 在SQL查询中增加了isValid字段过滤条件
- 添加了对primaryMenuEnum参数的条件查询支持
- 增加了详细的请求参数和查询结果日志记录
- 优化了参数传递的一致性,使用变量替代直接访问对象属性
2026-03-13 12:10:24 +08:00
5 changed files with 528 additions and 199 deletions

View File

@@ -245,15 +245,37 @@ public class DocDefinitionAppServiceImpl implements IDocDefinitionAppService {
public R<?> getTreeList(DocDefinitonParam param) { public R<?> getTreeList(DocDefinitonParam param) {
// 1. 获取当前登录用户的医院ID避免跨医院查询 // 1. 获取当前登录用户的医院ID避免跨医院查询
Long hospitalId = SecurityUtils.getLoginUser().getHospitalId(); Long hospitalId = SecurityUtils.getLoginUser().getHospitalId();
Long organizationId = param.getOrganizationId();
List<Integer> useRanges = param.getUseRanges();
log.info("获取文书定义树形列表 - 请求参数: hospitalId={}, organizationId={}, useRanges={}, name={}, primaryMenuEnum={}",
hospitalId, organizationId, useRanges, param.getName(), param.getPrimaryMenuEnum());
if (hospitalId == null) { if (hospitalId == null) {
log.warn("当前登录用户未关联医院ID将使用默认值"); log.warn("当前登录用户未关联医院ID将使用默认值");
// 设置默认医院ID为1或其他合适的默认值 // 设置默认医院ID为1或其他合适的默认值
hospitalId = 1L; hospitalId = 1L;
} }
if (organizationId == null || organizationId == 0) {
log.warn("organizationId为空或0将跳过医院过滤和使用范围过滤");
}
if (useRanges == null || useRanges.isEmpty()) {
log.warn("useRanges为空可能返回所有使用范围的文书");
}
// 2. 数据库查询文书定义列表 // 2. 数据库查询文书定义列表
List<DocDefinitionDto> docList = docDefinitionAppMapper.getDefinationList(param.getUseRanges(), List<DocDefinitionDto> docList = docDefinitionAppMapper.getDefinationList(useRanges,
param.getOrganizationId(), hospitalId, param.getName(), param.getPrimaryMenuEnum()); organizationId, hospitalId, param.getName(), param.getPrimaryMenuEnum());
log.info("获取文书定义树形列表 - 查询结果: 记录数={}", docList != null ? docList.size() : 0);
if (docList != null && !docList.isEmpty()) {
for (DocDefinitionDto doc : docList) {
log.debug("文书: id={}, name={}, useRangeEnum={}, hospitalId={}",
doc.getId(), doc.getName(), doc.getUseRangeEnum(), doc.getHospitalId());
}
}
// 3. 构建树形结构(空列表时返回空树,避免空指针) // 3. 构建树形结构(空列表时返回空树,避免空指针)
List<DirectoryNode> treeNodes = new ArrayList<>(); List<DirectoryNode> treeNodes = new ArrayList<>();

View File

@@ -29,9 +29,12 @@ import com.openhis.web.regdoctorstation.appservice.IAdviceManageAppService;
import com.openhis.web.regdoctorstation.dto.*; import com.openhis.web.regdoctorstation.dto.*;
import com.openhis.web.regdoctorstation.mapper.AdviceManageAppMapper; import com.openhis.web.regdoctorstation.mapper.AdviceManageAppMapper;
import com.openhis.web.regdoctorstation.utils.RegPrescriptionUtils; import com.openhis.web.regdoctorstation.utils.RegPrescriptionUtils;
import com.openhis.workflow.domain.ActivityDefinition; import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.ServiceRequest; import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IActivityDefinitionService; import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IServiceRequestService; import com.openhis.workflow.service.IServiceRequestService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -76,6 +79,12 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
@Resource @Resource
IActivityDefinitionService iActivityDefinitionService; IActivityDefinitionService iActivityDefinitionService;
@Resource
IDeviceRequestService iDeviceRequestService;
@Resource
IDeviceDispenseService iDeviceDispenseService;
/** /**
* 查询住院患者信息 * 查询住院患者信息
* *
@@ -174,6 +183,9 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
// 诊疗活动 // 诊疗活动
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream() List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList()); .filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 耗材 🔧 Bug #147 修复
List<RegAdviceSaveDto> deviceList = regAdviceSaveList.stream()
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 保存时,校验临时医嘱库存 // 保存时,校验临时医嘱库存
if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) { if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) {
@@ -210,6 +222,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
*/ */
this.handService(activityList, startTime, authoredTime, curDate, adviceOpType, organizationId, signCode); this.handService(activityList, startTime, authoredTime, curDate, adviceOpType, organizationId, signCode);
/**
* 🔧 Bug #147 修复:处理耗材请求
*/
this.handDevice(deviceList, startTime, authoredTime, curDate, adviceOpType, organizationId, signCode);
// 签发时,把草稿状态的账单更新为待收费 // 签发时,把草稿状态的账单更新为待收费
if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !regAdviceSaveList.isEmpty()) { if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !regAdviceSaveList.isEmpty()) {
// 签发的医嘱id集合 // 签发的医嘱id集合
@@ -594,6 +611,150 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
/**
* 🔧 Bug #147 修复:处理耗材
*/
private void handDevice(List<RegAdviceSaveDto> deviceList, Date startTime, Date authoredTime, Date curDate,
String adviceOpType, Long organizationId, String signCode) {
// 当前登录账号的科室id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
// 获取当前登录用户的tenantId
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 保存操作
boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType);
// 签发操作
boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType);
// 删除
List<RegAdviceSaveDto> deleteList = deviceList.stream()
.filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList());
for (RegAdviceSaveDto regAdviceSaveDto : deleteList) {
iDeviceRequestService.removeById(regAdviceSaveDto.getRequestId());
// 删除已经产生的耗材发放信息
iDeviceDispenseService.deleteDeviceDispense(regAdviceSaveDto.getRequestId());
// 删除费用项
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_DEVICE_REQUEST,
regAdviceSaveDto.getRequestId());
}
// 声明耗材请求
DeviceRequest deviceRequest;
// 声明费用项
ChargeItem chargeItem;
// 新增 + 修改 (长期医嘱)
List<RegAdviceSaveDto> longInsertOrUpdateList = deviceList.stream().filter(e -> TherapyTimeType.LONG_TERM
.getValue().equals(e.getTherapyEnum())
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
for (RegAdviceSaveDto regAdviceSaveDto : longInsertOrUpdateList) {
deviceRequest = new DeviceRequest();
deviceRequest.setId(regAdviceSaveDto.getRequestId()); // 主键id
deviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态
deviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
if (is_sign) {
deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间
}
// 保存时处理的字段属性
if (is_save) {
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setQuantity(regAdviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(regAdviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(regAdviceSaveDto.getLotNumber()); // 产品批号
deviceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型
deviceRequest.setDeviceDefId(regAdviceSaveDto.getAdviceDefinitionId());// 耗材定义id
deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室
deviceRequest.setReqAuthoredTime(startTime); // 医嘱开始时间
deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室
deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id
deviceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
deviceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
}
iDeviceRequestService.saveOrUpdate(deviceRequest);
}
// 新增 + 修改 (临时医嘱)
List<RegAdviceSaveDto> tempInsertOrUpdateList = deviceList.stream().filter(e -> TherapyTimeType.TEMPORARY
.getValue().equals(e.getTherapyEnum())
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
for (RegAdviceSaveDto regAdviceSaveDto : tempInsertOrUpdateList) {
deviceRequest = new DeviceRequest();
deviceRequest.setId(regAdviceSaveDto.getRequestId()); // 主键id
deviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态
deviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
if (is_sign) {
deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间
}
// 保存时处理的字段属性
if (is_save) {
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setQuantity(regAdviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(regAdviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(regAdviceSaveDto.getLotNumber()); // 产品批号
deviceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型
deviceRequest.setDeviceDefId(regAdviceSaveDto.getAdviceDefinitionId());// 耗材定义id
deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室
deviceRequest.setReqAuthoredTime(startTime); // 医嘱开始时间
deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室
deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id
deviceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
deviceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
}
iDeviceRequestService.saveOrUpdate(deviceRequest);
// 保存时,保存耗材费用项
if (is_save) {
// 处理耗材发放
Long dispenseId = iDeviceDispenseService.handleDeviceDispense(deviceRequest,
regAdviceSaveDto.getDbOpType());
// 保存耗材费用项
chargeItem = new ChargeItem();
chargeItem.setId(regAdviceSaveDto.getChargeItemId()); // 费用项id
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(deviceRequest.getBusNo()));
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
chargeItem.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
chargeItem.setContextEnum(regAdviceSaveDto.getAdviceType()); // 类型
chargeItem.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
chargeItem.setDefinitionId(regAdviceSaveDto.getDefinitionId()); // 费用定价ID
chargeItem.setDefDetailId(regAdviceSaveDto.getDefinitionDetailId()); // 定价子表主键
chargeItem.setEntererId(regAdviceSaveDto.getPractitionerId());// 开立人ID
chargeItem.setEnteredDate(curDate); // 开立时间
chargeItem.setServiceTable(CommonConstants.TableName.WOR_DEVICE_REQUEST);// 医疗服务类型
chargeItem.setServiceId(deviceRequest.getId()); // 医疗服务ID
chargeItem.setProductTable(regAdviceSaveDto.getAdviceTableName());// 产品所在表
chargeItem.setProductId(regAdviceSaveDto.getAdviceDefinitionId());// 收费项id
chargeItem.setAccountId(regAdviceSaveDto.getAccountId());// 关联账户ID
chargeItem.setRequestingOrgId(orgId); // 开立科室
chargeItem.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
chargeItem.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
chargeItem.setDispenseId(dispenseId); // 发放ID
chargeItem.setQuantityValue(regAdviceSaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(regAdviceSaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(regAdviceSaveDto.getUnitPrice()); // 单价
chargeItem.setTotalPrice(regAdviceSaveDto.getTotalPrice()); // 总价
iChargeItemService.saveOrUpdate(chargeItem);
}
}
}
/** /**
* 查询住院医嘱请求数据 * 查询住院医嘱请求数据
* *

View File

@@ -55,10 +55,10 @@
<if test="ew.customSqlSegment != null and ew.customSqlSegment != ''"> <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''">
<choose> <choose>
<when test="ew.customSqlSegment.contains('tenant_id')"> <when test="ew.customSqlSegment.contains('tenant_id')">
${ew.customSqlSegment.replaceFirst('tenant_id', 'T1.tenant_id').replaceFirst('status_enum', 'T1.status_enum').replaceFirst('WHERE', 'AND')} ${ew.customSqlSegment.replaceFirst('tenant_id', 'T1.tenant_id').replaceFirst('status_enum', 'T1.status_enum').replaceFirst('name', 'T1.name').replaceFirst('WHERE', 'AND')}
</when> </when>
<otherwise> <otherwise>
${ew.customSqlSegment.replaceFirst('status_enum', 'T1.status_enum').replaceFirst('WHERE', 'AND')} ${ew.customSqlSegment.replaceFirst('status_enum', 'T1.status_enum').replaceFirst('name', 'T1.name').replaceFirst('WHERE', 'AND')}
</otherwise> </otherwise>
</choose> </choose>
</if> </if>

View File

@@ -30,6 +30,7 @@
AND ddo.delete_flag = '0' AND ddo.delete_flag = '0'
WHERE dd.delete_flag = '0' WHERE dd.delete_flag = '0'
AND dd.is_valid = 0
<!-- 关键:医院 + 科室联合可见 --> <!-- 关键:医院 + 科室联合可见 -->
<if test="organizationId != null and organizationId != 0"> <if test="organizationId != null and organizationId != 0">
@@ -80,6 +81,10 @@
AND dd.name LIKE CONCAT('%', #{name}, '%') AND dd.name LIKE CONCAT('%', #{name}, '%')
</if> </if>
<if test="primaryMenuEnum != null">
AND dd.primary_menu_enum = #{primaryMenuEnum}
</if>
GROUP BY dd.id, dd.primary_menu_enum, dd.sub_menu GROUP BY dd.id, dd.primary_menu_enum, dd.sub_menu
ORDER BY dd.display_order ORDER BY dd.display_order

View File

@@ -4,7 +4,7 @@
<!-- 顶部操作按钮区 --> <!-- 顶部操作按钮区 -->
<el-header class="top-action-bar" height="50px"> <el-header class="top-action-bar" height="50px">
<el-row class="action-buttons" type="flex" justify="end" :gutter="10"> <el-row class="action-buttons" type="flex" justify="end" :gutter="10">
<el-button type="success" size="large" @click="handleSave" class="save-btn"> <el-button type="success" size="large" @click="handleSave" class="save-btn" :loading="saving">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
保存 保存
</el-button> </el-button>
@@ -400,19 +400,35 @@
<span class="card-title">检验项目选择</span> <span class="card-title">检验项目选择</span>
</template> </template>
<!-- 搜索框 --> <!-- 搜索框自动完成 -->
<el-input <el-autocomplete
v-model="searchKeyword" v-model="searchKeyword"
:fetch-suggestions="querySearchInspectionItems"
placeholder="搜索检验项目..." placeholder="搜索检验项目..."
size="small" size="small"
clearable clearable
prefix-icon="Search" prefix-icon="Search"
@input="handleSearch" @select="handleSearchSelect"
@clear="handleSearchClear"
value-key="itemName"
class="search-input" class="search-input"
/> :debounce="300"
>
<template #default="{ item }">
<div class="suggestion-item">
<span class="suggestion-name">{{ item.itemName }}</span>
<el-tag size="small" type="info" class="suggestion-category">{{ item.typeName || '检验' }}</el-tag>
<span class="suggestion-price">¥{{ item.itemPrice }}</span>
</div>
</template>
</el-autocomplete>
<!-- 分类树 --> <!-- 分类树 -->
<el-scrollbar class="category-tree" style="max-height: 280px"> <el-scrollbar
class="category-tree"
style="max-height: 280px"
@scroll="handleScroll"
>
<!-- 无数据提示 --> <!-- 无数据提示 -->
<el-empty v-if="!inspectionLoading && inspectionCategories.length === 0" description="暂无检验项目数据" :image-size="80" /> <el-empty v-if="!inspectionLoading && inspectionCategories.length === 0" description="暂无检验项目数据" :image-size="80" />
<!-- 数据列表 --> <!-- 数据列表 -->
@@ -427,9 +443,19 @@
> >
<span class="category-tree-icon">{{ category.expanded ? '▼' : '▶' }}</span> <span class="category-tree-icon">{{ category.expanded ? '▼' : '▶' }}</span>
<span>{{ category.label }}</span> <span>{{ category.label }}</span>
<span class="category-count">({{ category.items.length }})</span> <span class="category-count">({{ category.total || category.items.length }})</span>
<!-- 加载状态图标 -->
<el-icon v-if="category.loading" class="is-loading" style="margin-left: 8px; color: #409eff;">
<Loading />
</el-icon>
</div> </div>
<div v-if="category.expanded" class="category-tree-children"> <div v-if="category.expanded" class="category-tree-children">
<!-- 加载中占位 -->
<div v-if="category.loading && category.items.length === 0" class="loading-placeholder">
<el-icon class="is-loading" style="margin-right: 8px;"><Loading /></el-icon>
<span>加载中...</span>
</div>
<!-- 项目列表 -->
<div <div
v-for="item in getFilteredItems(category.key)" v-for="item in getFilteredItems(category.key)"
:key="item.itemId" :key="item.itemId"
@@ -444,6 +470,22 @@
<span class="item-itemName">{{ item.itemName }}</span> <span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}</span> <span class="item-price">¥{{ item.itemPrice }}</span>
</div> </div>
<!-- 加载更多 -->
<div v-if="category.hasMore && category.items.length > 0" class="load-more">
<el-button
link
size="small"
:loading="category.loading"
@click.stop="loadMoreItems(category.key)"
>
{{ category.loading ? '加载中...' : '加载更多' }}
</el-button>
<span class="load-info">(已加载 {{ category.items.length }}/{{ category.total }} )</span>
</div>
<!-- 加载完成提示 -->
<div v-if="!category.hasMore && category.items.length > 0" class="no-more">
已全部加载 ( {{ category.total }} )
</div>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
@@ -492,7 +534,7 @@
<script setup> <script setup>
import {onMounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue' import {onMounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus' import {ElMessage, ElMessageBox} from 'element-plus'
import { DocumentChecked, Plus, Document, Printer, Delete, Check } from '@element-plus/icons-vue' import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
import { import {
checkInspectionApplicationNo, checkInspectionApplicationNo,
deleteInspectionApplication, getApplyList, deleteInspectionApplication, getApplyList,
@@ -504,6 +546,7 @@ import useUserStore from '@/store/modules/user.js'
// 迁移到 hiprint // 迁移到 hiprint
import { previewPrint } from '@/utils/printUtils.js' import { previewPrint } from '@/utils/printUtils.js'
import {storeToRefs} from 'pinia' import {storeToRefs} from 'pinia'
import { debounce } from 'lodash-es'
// 获取当前组件实例和字典 // 获取当前组件实例和字典
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
@@ -532,6 +575,7 @@ const emit = defineEmits(['save'])
// 响应式数据 // 响应式数据
const loading = ref(false) const loading = ref(false)
const saving = ref(false) // 保存状态
const total = ref(0) const total = ref(0)
const leftActiveTab = ref('application') const leftActiveTab = ref('application')
const isGeneratingNewApplyNo = ref(false) // 标志:是否正在生成新申请单号 const isGeneratingNewApplyNo = ref(false) // 标志:是否正在生成新申请单号
@@ -624,7 +668,8 @@ const validationErrors = reactive({
clinicDiag: false, clinicDiag: false,
medicalHistorySummary: false, medicalHistorySummary: false,
purposeofInspection: false, purposeofInspection: false,
labApplyItemList: false labApplyItemList: false,
applyTime: false
}) })
// 已选择的表格行 // 已选择的表格行
@@ -639,15 +684,21 @@ const searchKeyword = ref('')
// 活动分类 // 活动分类
const activeCategory = ref('') const activeCategory = ref('')
// 检验项目分类动态从API获取 // 检验项目分类动态从API获取,支持懒加载和分页
const inspectionCategories = ref([]) const inspectionCategories = ref([])
// 检验项目加载状态 // 检验项目加载状态(整体)
const inspectionLoading = ref(false) const inspectionLoading = ref(false)
// 加载检验项目分类和项目 // 每页加载条数
const PAGE_SIZE = 50
// 搜索防抖时间(毫秒)
const SEARCH_DEBOUNCE_TIME = 300
// 加载检验类型分类列表(只加载分类,项目懒加载)
async function loadInspectionData() { async function loadInspectionData() {
// 如果已经加载过数据,直接返回(避免重复请求) // 如果已经加载过分类,直接返回
if (inspectionCategories.value.length > 0) { if (inspectionCategories.value.length > 0) {
return return
} }
@@ -655,194 +706,244 @@ async function loadInspectionData() {
inspectionLoading.value = true inspectionLoading.value = true
try { try {
// 并行请求:同时获取检验类型列表和检验项目列表 // 获取检验类型列表
const [typeRes, itemRes] = await Promise.all([ const typeRes = await getInspectionTypeList().catch(error => {
getInspectionTypeList().catch(error => {
console.error('获取检验类型失败:', error) console.error('获取检验类型失败:', error)
return { data: [] } return { data: [] }
}),
getInspectionItemList({
pageNo: 1,
pageSize: 500,
searchKey: '',
categoryCode: inspectionCategoryCode.value
}).catch(error => {
console.error('获取检验项目失败:', error)
return { data: { records: [] } }
}) })
])
const typeList = typeRes.data || [] const typeList = typeRes.data || []
// 解析检验项目数据 // 创建分类结构,但不加载项目(懒加载)
let allItems = []
if (itemRes.data && itemRes.data.records) {
allItems = itemRes.data.records
} else if (itemRes.data && Array.isArray(itemRes.data)) {
allItems = itemRes.data
} else if (Array.isArray(itemRes)) {
allItems = itemRes
}
// 按分类组织数据(与检验项目设置维护保持一致的字段映射)
const categories = typeList const categories = typeList
.filter(type => type.validFlag === 1 || type.validFlag === undefined) .filter(type => type.validFlag === 1 || type.validFlag === undefined)
.map((type, index) => { .map((type, index) => ({
const categoryItems = allItems
.filter(item => {
// 转换为字符串进行比较,避免类型不匹配问题
const itemTypeId = String(item.inspectionTypeId || '')
const typeId = String(type.id || '')
const itemTypeName = String(item.inspectionTypeId_dictText || item.typeName || '').trim()
const typeName = String(type.name || '').trim()
// 按检验类型ID匹配优先使用 inspectionTypeId
const matchById = itemTypeId && typeId && itemTypeId === typeId
const matchByName = itemTypeName && typeName && itemTypeName === typeName
const matchByCode = String(item.typeCode || '') === String(type.code || '')
return matchById || matchByName || matchByCode
})
.map(item => ({
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
itemName: item.name || item.itemName || '',
itemPrice: item.retailPrice || item.price || 0,
itemAmount: item.retailPrice || item.price || 0,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
itemQty: 1,
serviceFee: 0,
type: type.name || item.inspectionTypeId_dictText || '检验',
isSelfPay: false,
activityId: item.activityId,
code: item.busNo || item.code || item.activityCode,
inspectionTypeId: item.inspectionTypeId || null
}))
return {
key: type.code || `type_${index}`, key: type.code || `type_${index}`,
label: type.name || `分类${index + 1}`, label: type.name || `分类${index + 1}`,
expanded: index === 0, typeId: type.id, // 保存类型ID用于分页查询
items: categoryItems expanded: index === 0, // 默认展开第一个
} loaded: false, // 是否已加载项目
}) loading: false, // 是否正在加载
items: [], // 项目列表
// 过滤掉没有项目的分类 pageNo: 1, // 当前页码
const validCategories = categories.filter(cat => cat.items.length > 0) pageSize: PAGE_SIZE, // 每页条数
total: 0, // 总条数
// 如果没有有效分类,但有项目数据,按项目自带的检验类型文本分组 hasMore: true // 是否还有更多数据
if (validCategories.length === 0 && allItems.length > 0) {
// 按检验类型文本分组
const typeMap = new Map()
allItems.forEach(item => {
const typeName = item.inspectionTypeId_dictText || '其他检验'
if (!typeMap.has(typeName)) {
typeMap.set(typeName, [])
}
typeMap.get(typeName).push(item)
})
// 创建分类
typeMap.forEach((items, typeName) => {
const mappedItems = items.map(item => ({
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
itemName: item.name || item.itemName || '',
itemPrice: item.retailPrice || item.price || 0,
itemAmount: item.retailPrice || item.price || 0,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
itemQty: 1,
serviceFee: 0,
type: typeName,
isSelfPay: false,
activityId: item.activityId,
code: item.busNo || item.code || item.activityCode,
inspectionTypeId: item.inspectionTypeId || null
})) }))
validCategories.push({ if (categories.length > 0) {
key: `type_${typeName}`, inspectionCategories.value = categories
label: typeName, activeCategory.value = categories[0].key
expanded: validCategories.length === 0,
items: mappedItems
})
})
}
if (validCategories.length > 0) { // 预加载第一个分类的项目
inspectionCategories.value = validCategories await loadCategoryItems(categories[0].key)
activeCategory.value = validCategories[0].key
} else { } else {
console.warn('未获取到有效的检验项目数据,使用备用数据') console.warn('未获取到检验类型分类')
// 直接使用备用数据,不抛出错误
inspectionCategories.value = [
{
key: 'biochemical',
label: '生化',
expanded: true,
items: [
{ itemId: 1, itemName: '肝功能', itemPrice: 31, itemAmount: 31, sampleType: '血清', unit: 'U/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false },
{ itemId: 2, itemName: '肾功能', itemPrice: 28, itemAmount: 28, sampleType: '血清', unit: 'U/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false },
{ itemId: 3, itemName: '血糖', itemPrice: 15, itemAmount: 15, sampleType: '血清', unit: 'mmol/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false }
]
},
{
key: 'blood',
label: '临检',
expanded: false,
items: [
{ itemId: 4, itemName: '血常规+crp', itemPrice: 50, itemAmount: 50, sampleType: '全血', unit: '×10^9/L', itemQty: 1, serviceFee: 0, type: '血液', isSelfPay: false },
{ itemId: 5, itemName: '血常规(五分类)', itemPrice: 15, itemAmount: 15, sampleType: '全血', unit: '×10^9/L', itemQty: 1, serviceFee: 0, type: '血液', isSelfPay: false }
]
}
]
activeCategory.value = 'biochemical'
} }
} catch (error) { } catch (error) {
console.error('加载检验项目数据失败:', error) console.error('加载检验类型分类失败:', error)
// 加载失败时使用静态数据作为备用
inspectionCategories.value = [
{
key: 'biochemical',
label: '生化',
expanded: true,
items: [
{ itemId: 1, itemName: '肝功能', itemPrice: 31, itemAmount: 31, sampleType: '血清', unit: 'U/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false },
{ itemId: 2, itemName: '肾功能', itemPrice: 28, itemAmount: 28, sampleType: '血清', unit: 'U/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false },
{ itemId: 3, itemName: '血糖', itemPrice: 15, itemAmount: 15, sampleType: '血清', unit: 'mmol/L', itemQty: 1, serviceFee: 0, type: '生化', isSelfPay: false }
]
},
{
key: 'blood',
label: '临检',
expanded: false,
items: [
{ itemId: 4, itemName: '血常规+crp', itemPrice: 50, itemAmount: 50, sampleType: '全血', unit: '×10^9/L', itemQty: 1, serviceFee: 0, type: '血液', isSelfPay: false },
{ itemId: 5, itemName: '血常规(五分类)', itemPrice: 15, itemAmount: 15, sampleType: '全血', unit: '×10^9/L', itemQty: 1, serviceFee: 0, type: '血液', isSelfPay: false }
]
}
]
activeCategory.value = 'biochemical'
} finally { } finally {
inspectionLoading.value = false inspectionLoading.value = false
} }
} }
// 获取过滤后的项目 // 懒加载分类项目(分页)
async function loadCategoryItems(categoryKey, loadMore = false) {
const category = inspectionCategories.value.find(c => c.key === categoryKey)
if (!category) return
// 已加载完成且不是加载更多,或正在加载中,跳过
if ((category.loaded && !loadMore) || category.loading) return
// 没有更多数据了,跳过
if (loadMore && !category.hasMore) return
category.loading = true
try {
const params = {
pageNo: category.pageNo,
pageSize: category.pageSize,
categoryCode: inspectionCategoryCode.value,
searchKey: searchKeyword.value || ''
}
// 如果有类型ID添加筛选条件
if (category.typeId) {
params.inspectionTypeId = category.typeId
}
const res = await getInspectionItemList(params)
// 解析数据
let records = []
let total = 0
if (res.data && res.data.records) {
records = res.data.records
total = res.data.total || 0
} else if (res.data && Array.isArray(res.data)) {
records = res.data
total = records.length
} else if (Array.isArray(res)) {
records = res
total = records.length
}
// 映射数据格式
const mappedItems = records.map(item => ({
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
itemName: item.name || item.itemName || '',
itemPrice: item.retailPrice || item.price || 0,
itemAmount: item.retailPrice || item.price || 0,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
itemQty: 1,
serviceFee: 0,
type: category.label,
isSelfPay: false,
activityId: item.activityId,
code: item.busNo || item.code || item.activityCode,
inspectionTypeId: item.inspectionTypeId || null
}))
// 更新分类数据
if (loadMore) {
// 追加数据
category.items.push(...mappedItems)
} else {
// 首次加载
category.items = mappedItems
}
category.total = total
category.hasMore = category.items.length < total
category.loaded = true
} catch (error) {
console.error(`加载分类 [${category.label}] 项目失败:`, error)
// 加载失败时设置空数据
if (!loadMore) {
category.items = []
category.total = 0
category.hasMore = false
category.loaded = true
}
} finally {
category.loading = false
}
}
// 加载更多项目
function loadMoreItems(categoryKey) {
const category = inspectionCategories.value.find(c => c.key === categoryKey)
if (!category || !category.hasMore || category.loading) return
category.pageNo++
loadCategoryItems(categoryKey, true)
}
// 处理滚动事件(无限滚动)
function handleScroll({ scrollTop, scrollHeight, clientHeight }) {
// 距离底部 50px 时触发加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
const expandedCategory = inspectionCategories.value.find(c => c.expanded)
if (expandedCategory && expandedCategory.hasMore && !expandedCategory.loading) {
loadMoreItems(expandedCategory.key)
}
}
}
// 防抖搜索处理
const handleSearchDebounced = debounce(() => {
// 重新加载当前展开分类的数据
const expandedCategory = inspectionCategories.value.find(c => c.expanded)
if (expandedCategory) {
// 重置分页状态
expandedCategory.pageNo = 1
expandedCategory.loaded = false
expandedCategory.hasMore = true
expandedCategory.items = []
// 重新加载
loadCategoryItems(expandedCategory.key)
}
}, SEARCH_DEBOUNCE_TIME)
// 获取过滤后的项目(本地搜索)
const getFilteredItems = (categoryKey) => { const getFilteredItems = (categoryKey) => {
const category = inspectionCategories.value.find(cat => cat.key === categoryKey) const category = inspectionCategories.value.find(cat => cat.key === categoryKey)
if (!category) return [] if (!category) return []
if (!searchKeyword.value) { // 如果正在加载,返回现有数据
if (category.loading) {
return category.items return category.items
} }
// 本地过滤(安全检查 itemName
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
return category.items.filter(item => return category.items.filter(item =>
item.itemName.toLowerCase().includes(searchKeyword.value.toLowerCase()) item.itemName && item.itemName.toLowerCase().includes(keyword)
) )
}
return category.items
} }
// 搜索建议查询(自动完成)
async function querySearchInspectionItems(queryString, cb) {
if (!queryString) {
cb([])
return
}
try {
const params = {
pageNo: 1,
pageSize: 20, // 限制返回数量
categoryCode: inspectionCategoryCode.value,
searchKey: queryString
}
const res = await getInspectionItemList(params)
let suggestions = []
if (res.data && res.data.records) {
// 映射数据格式,与 loadInspectionItemsByType 保持一致
suggestions = res.data.records.map(item => ({
itemId: item.id || item.activityId,
itemName: item.name || item.itemName || '',
itemPrice: item.retailPrice || item.price || 0,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
code: item.busNo || item.code || item.activityCode,
activityId: item.activityId,
inspectionTypeId: item.inspectionTypeId || null
}))
}
cb(suggestions)
} catch (error) {
console.error('搜索检验项目失败:', error)
cb([])
}
}
// 搜索选择处理
function handleSearchSelect(item) {
// 直接添加到已选列表
if (!isItemSelected(item)) {
selectedInspectionItems.value.push({
...item,
itemName: item.itemName
})
}
// 清空搜索关键词
searchKeyword.value = ''
}
// 搜索框清空处理
function handleSearchClear() {
searchKeyword.value = ''
}
// 获取检验申请单列表 // 获取检验申请单列表
function getInspectionList() { function getInspectionList() {
@@ -1054,6 +1155,9 @@ async function resetForm() {
// 保存 // 保存
function handleSave() { function handleSave() {
// 如果正在保存,直接返回
if (saving.value) return
// 重置验证错误状态 // 重置验证错误状态
Object.keys(validationErrors).forEach(key => { Object.keys(validationErrors).forEach(key => {
validationErrors[key] = false validationErrors[key] = false
@@ -1061,29 +1165,28 @@ function handleSave() {
let hasErrors = false let hasErrors = false
// 检查必填字段
// 检查必填字段,执行科室 // 检查必填字段,执行科室
if (!formData.executeDepartment) { if (!formData.executeDepartment) {
validationErrors.executeDepartment = true validationErrors.executeDepartment = true
hasErrors = true hasErrors = true
} }
// 检查必填字段,诊断描述 // 检查必填字段,诊断描述
if (!formData.clinicDesc.trim()) { if (!formData.clinicDesc?.trim()) {
validationErrors.clinicDesc = true validationErrors.clinicDesc = true
hasErrors = true hasErrors = true
} }
// 检查必填字段,临床诊断 // 检查必填字段,临床诊断
if (!formData.clinicDiag.trim()) { if (!formData.clinicDiag?.trim()) {
validationErrors.clinicDiag = true validationErrors.clinicDiag = true
hasErrors = true hasErrors = true
} }
// 检查必填字段,病史摘要 // 检查必填字段,病史摘要
if (!formData.medicalHistorySummary.trim()) { if (!formData.medicalHistorySummary?.trim()) {
validationErrors.medicalHistorySummary = true validationErrors.medicalHistorySummary = true
hasErrors = true hasErrors = true
} }
// 检查必填字段,检验目的 // 检查必填字段,检验目的
if (!formData.purposeofInspection.trim()) { if (!formData.purposeofInspection?.trim()) {
validationErrors.purposeofInspection = true validationErrors.purposeofInspection = true
hasErrors = true hasErrors = true
} }
@@ -1095,7 +1198,7 @@ function handleSave() {
return return
} }
// 检查必填字段,申请日期 // 检查必填字段,申请日期
if(!formData.applyTime || (typeof formData.applyTime === 'string' && !formData.applyTime.trim())) { if(!formData.applyTime || (typeof formData.applyTime === 'string' && !formData.applyTime?.trim())) {
validationErrors.applyTime = true validationErrors.applyTime = true
hasErrors = true hasErrors = true
} }
@@ -1182,9 +1285,11 @@ function handleSave() {
} }
const executeSave = (saveData) => { const executeSave = (saveData) => {
saving.value = true
saveInspectionApplication(saveData).then((res) => { saveInspectionApplication(saveData).then((res) => {
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('保存成功') ElMessage.success('保存成功')
emit('save', res.data) // 通知父组件保存成功
resetForm() resetForm()
// 生成新的申请单号 // 生成新的申请单号
generateApplicationNo().then((newApplyNo) => { generateApplicationNo().then((newApplyNo) => {
@@ -1211,6 +1316,8 @@ const executeSave = (saveData) => {
// 处理请求失败的其他错误 // 处理请求失败的其他错误
console.error('保存检验申请单时发生错误:', error); console.error('保存检验申请单时发生错误:', error);
ElMessage.error('保存失败,请稍后重试'); ElMessage.error('保存失败,请稍后重试');
}).finally(() => {
saving.value = false
}) })
} }
@@ -1222,10 +1329,10 @@ function handleView(row) {
// 根据检验项目名称找到对应的项目数据 // 根据检验项目名称找到对应的项目数据
selectedInspectionItems.value = [] selectedInspectionItems.value = []
const itemNames = row.inspectionItem.split('、') const itemNames = row.itemName?.split('、') || row.inspectionItem?.split('、') || []
inspectionCategories.value.forEach(category => { inspectionCategories.value.forEach(category => {
category.items.forEach(item => { category.items.forEach(item => {
if (itemNames.includes(item.name)) { if (itemNames.includes(item.itemName)) {
selectedInspectionItems.value.push(item) selectedInspectionItems.value.push(item)
} }
}) })
@@ -1234,7 +1341,7 @@ function handleView(row) {
leftActiveTab.value = 'application' leftActiveTab.value = 'application'
} }
// 切换分类 // 切换分类(修改为懒加载)
function switchCategory(category) { function switchCategory(category) {
if (activeCategory.value === category) { if (activeCategory.value === category) {
// 如果点击的是当前激活的分类,则收起 // 如果点击的是当前激活的分类,则收起
@@ -1250,12 +1357,13 @@ function switchCategory(category) {
inspectionCategories.value.forEach(cat => { inspectionCategories.value.forEach(cat => {
cat.expanded = cat.key === category cat.expanded = cat.key === category
}) })
}
}
// 处理搜索 // 懒加载该分类的项目
function handleSearch() { const targetCategory = inspectionCategories.value.find(c => c.key === category)
// 搜索逻辑已在getFilteredItems中实现 if (targetCategory && !targetCategory.loaded) {
loadCategoryItems(category)
}
}
} }
// 处理项目项点击(排除勾选框点击) // 处理项目项点击(排除勾选框点击)
@@ -1409,8 +1517,8 @@ function handleCellClick(row, column) {
// 根据检验项目名称解析已选项目 // 根据检验项目名称解析已选项目
selectedInspectionItems.value = [] selectedInspectionItems.value = []
if (row.inspectionItem) { if (row.itemName || row.inspectionItem) {
const itemNames = row.inspectionItem.split('、') const itemNames = (row.itemName || row.inspectionItem).split('、')
inspectionCategories.value.forEach(category => { inspectionCategories.value.forEach(category => {
category.items.forEach(item => { category.items.forEach(item => {
if (itemNames.includes(item.itemName)) { if (itemNames.includes(item.itemName)) {
@@ -1956,6 +2064,39 @@ defineExpose({
border-top: 1px solid #ebeef5; border-top: 1px solid #ebeef5;
} }
/* 加载中占位样式 */
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
font-size: 14px;
}
/* 加载更多样式 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-top: 1px dashed #ebeef5;
}
.load-info {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
/* 没有更多数据提示 */
.no-more {
text-align: center;
padding: 10px;
font-size: 12px;
color: #c0c4cc;
}
.inspection-tree-item { .inspection-tree-item {
display: flex; display: flex;
align-items: center; align-items: center;