Bug #384: 检查方法联动功能完善,增加套餐价格查询和项目卡片展开选择

Bug #386: 检验申请删除时同步删除关联收费项目
  Bug #382: 选择项目后保持当前页签状态
  Bug #380,381: 临床诊断获取主诊断字段名修正
  Bug #387: 套餐项目回充默认展开并自动加载明细
This commit is contained in:
wangjian963
2026-04-21 10:18:26 +08:00
parent 5ab4650c4e
commit 994ffcb8b8
6 changed files with 513 additions and 184 deletions

View File

@@ -4,8 +4,11 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.openhis.check.domain.CheckMethod; import com.openhis.check.domain.CheckMethod;
import com.openhis.check.domain.CheckPackage;
import com.openhis.check.service.ICheckMethodService; import com.openhis.check.service.ICheckMethodService;
import com.openhis.check.service.ICheckPackageService;
import com.openhis.web.check.appservice.ICheckMethodAppService; import com.openhis.web.check.appservice.ICheckMethodAppService;
import com.openhis.web.check.dto.CheckMethodDto;
import com.openhis.web.reportmanage.utils.ExcelFillerUtil; import com.openhis.web.reportmanage.utils.ExcelFillerUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,6 +19,7 @@ import java.io.IOException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -24,10 +28,15 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
@Resource @Resource
private ICheckMethodService checkMethodService; private ICheckMethodService checkMethodService;
@Resource
private ICheckPackageService checkPackageService; // Bug #384修复注入套餐服务
@Override @Override
public R<?> getCheckMethodList() { public R<?> getCheckMethodList() {
List<CheckMethod> list = checkMethodService.list(); List<CheckMethod> list = checkMethodService.list();
return R.ok(list); // Bug #384修复转换为DTO并关联套餐价格
List<CheckMethodDto> dtoList = convertToDtoWithPackagePrice(list);
return R.ok(dtoList);
} }
@Override @Override
@@ -43,7 +52,67 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
wrapper.eq(CheckMethod::getPackageName, packageName); wrapper.eq(CheckMethod::getPackageName, packageName);
} }
List<CheckMethod> list = checkMethodService.list(wrapper); List<CheckMethod> list = checkMethodService.list(wrapper);
return R.ok(list); // Bug #384修复转换为DTO并关联套餐价格
List<CheckMethodDto> dtoList = convertToDtoWithPackagePrice(list);
return R.ok(dtoList);
}
/**
* Bug #384修复转换CheckMethod为DTO并通过packageName关联查询套餐价格
* @param methods 检查方法列表
* @return 包含套餐价格的DTO列表
*/
private List<CheckMethodDto> convertToDtoWithPackagePrice(List<CheckMethod> methods) {
if (methods == null || methods.isEmpty()) {
return List.of();
}
// 获取所有packageName批量查询套餐
List<String> packageNames = methods.stream()
.map(CheckMethod::getPackageName)
.filter(ObjectUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList());
// Bug #384修复: 批量查询套餐信息使用final变量
final Map<String, CheckPackage> packageMap;
if (!packageNames.isEmpty()) {
List<CheckPackage> packages = checkPackageService.list(
new LambdaQueryWrapper<CheckPackage>()
.in(CheckPackage::getPackageName, packageNames)
.eq(CheckPackage::getIsDisabled, 0) // 只查未停用的套餐
);
packageMap = packages.stream()
.collect(Collectors.toMap(CheckPackage::getPackageName, p -> p, (p1, p2) -> p1));
} else {
packageMap = Map.of();
}
// 转换为DTO并填充价格
return methods.stream().map(m -> {
CheckMethodDto dto = new CheckMethodDto();
dto.setId(m.getId() != null ? m.getId().longValue() : null);
dto.setCheckType(m.getCheckType());
dto.setCode(m.getCode());
dto.setName(m.getName());
dto.setPackageName(m.getPackageName());
dto.setExposureNum(m.getExposureNum());
dto.setOrderNum(m.getOrderNum());
dto.setRemark(m.getRemark());
dto.setCreateTime(m.getCreateTime());
dto.setUpdateTime(m.getUpdateTime());
// 通过packageName匹配套餐价格
if (ObjectUtil.isNotEmpty(m.getPackageName())) {
CheckPackage pkg = packageMap.get(m.getPackageName());
if (pkg != null) {
dto.setPackagePrice(pkg.getPackagePrice());
dto.setServiceFee(pkg.getServiceFee());
}
}
return dto;
}).collect(Collectors.toList());
} }
@Override @Override

View File

@@ -1,12 +1,15 @@
package com.openhis.web.check.dto; package com.openhis.web.check.dto;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 检查方法DTO - Bug #384修复增加套餐价格字段
* 用于API返回数据传输不含数据库注解
*/
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class CheckMethodDto { public class CheckMethodDto {
@@ -14,7 +17,6 @@ public class CheckMethodDto {
/** /**
* 检查方法ID * 检查方法ID
*/ */
@TableId(type = IdType.AUTO)
private Long id; private Long id;
/* 检查类型 */ /* 检查类型 */
@@ -29,6 +31,12 @@ public class CheckMethodDto {
/* 套餐名称 */ /* 套餐名称 */
private String packageName; private String packageName;
/* 套餐价格 - Bug #384修复通过packageName匹配CheckPackage获取 */
private BigDecimal packagePrice;
/* 服务费 - Bug #384修复通过packageName匹配CheckPackage获取 */
private BigDecimal serviceFee;
/* 曝光次数 */ /* 曝光次数 */
private Integer exposureNum; private Integer exposureNum;

View File

@@ -7,6 +7,7 @@ import com.core.common.utils.SecurityUtils;
import com.openhis.common.enums.DbOpType; import com.openhis.common.enums.DbOpType;
import com.openhis.administration.service.IAccountService; import com.openhis.administration.service.IAccountService;
import com.openhis.administration.domain.Account; import com.openhis.administration.domain.Account;
import com.openhis.administration.service.IChargeItemService; // Bug #386修复: 添加 ChargeItemService
import com.openhis.lab.domain.InspectionLabApply; import com.openhis.lab.domain.InspectionLabApply;
import com.openhis.lab.domain.InspectionLabApplyItem; import com.openhis.lab.domain.InspectionLabApplyItem;
import com.openhis.lab.domain.BarCode; import com.openhis.lab.domain.BarCode;
@@ -97,6 +98,10 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
@Autowired @Autowired
private ILabActivityDefinitionService labActivityDefinitionService; private ILabActivityDefinitionService labActivityDefinitionService;
// Bug #386修复: ChargeItemService 用于删除收费项目
@Autowired
private IChargeItemService chargeItemService;
/** /**
* 保存检验申请单信息 * 保存检验申请单信息
* @param doctorStationLabApplyDto * @param doctorStationLabApplyDto
@@ -598,8 +603,14 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
); );
if (updateResult) { if (updateResult) {
log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1更新人{},更新时间:{}", log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1更新人{},更新时间:{}",
applyNo, requestIds.size(), currentUsername, currentTime); applyNo, requestIds.size(), currentUsername, currentTime);
// Bug #386修复: 同步删除关联的收费项目
for (Long requestId : requestIds) {
chargeItemService.deleteByServiceTableAndId("wor_service_request", requestId);
}
log.debug("成功删除申请单号 [{}] 关联的 {} 条收费项目", applyNo, requestIds.size());
} else { } else {
log.warn("更新申请单号 [{}] 关联的门诊医嘱删除状态失败", applyNo); log.warn("更新申请单号 [{}] 关联的门诊医嘱删除状态失败", applyNo);
} }

View File

@@ -104,14 +104,14 @@
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="申请科室" prop="applyDeptCode"> <el-form-item label="申请科室" prop="applyDeptCode">
<el-input v-model="form.applyDeptCode" /> <el-input v-model="form.applyDeptCode" disabled />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="申请医生" prop="applyDocCode"> <el-form-item label="申请医生" prop="applyDocCode">
<el-input v-model="form.applyDocCode" /> <el-input v-model="form.applyDocCode" disabled />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
@@ -184,16 +184,10 @@
<el-input v-model="form.inspectionArea" readonly /> <el-input v-model="form.inspectionArea" readonly />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- Bug #384修复: 添加检查方法只读输入框联动显示选中的检查方法 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="检查方法"> <el-form-item label="检查方法">
<el-select v-model="form.inspectionMethod" placeholder="请选择" clearable filterable style="width: 100%;"> <el-input v-model="form.selectedMethodDisplay" readonly placeholder="请在右侧选择" />
<el-option
v-for="method in availableMethods"
:key="method.id"
:label="method.name"
:value="method.name"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -233,16 +227,34 @@
<el-input v-model="scope.row.applyPart" size="small" /> <el-input v-model="scope.row.applyPart" size="small" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="检查方法" min-width="120">
<template #default="scope">
<!-- Bug #384修复: 显示检查方法名称不显示套餐名称 -->
<span v-if="scope.row.selectedMethod">
{{ scope.row.selectedMethod.name }}
</span>
<span v-else-if="scope.row.methods && scope.row.methods.length > 0" style="color: #909399;">
未选择
</span>
<span v-else style="color: #c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="55" align="center" /> <el-table-column label="单位" prop="unit" width="55" align="center" />
<el-table-column label="总量" prop="quantity" width="70" align="center"> <el-table-column label="总量" prop="quantity" width="70" align="center">
<template #default="scope"> <template #default="scope">
<el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" /> <el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="单价" prop="price" width="75" align="right" /> <!-- Bug #384修复: 单价显示套餐价格如果选中或部位价格 -->
<el-table-column label="单价" width="75" align="right">
<template #default="scope">
{{ scope.row.selectedMethod?.packagePrice || scope.row.price }}
</template>
</el-table-column>
<!-- Bug #384修复: 金额使用有效价格计算 -->
<el-table-column label="金额" width="80" align="right"> <el-table-column label="金额" width="80" align="right">
<template #default="scope"> <template #default="scope">
{{ ((scope.row.price || 0) * (scope.row.quantity || 1)).toFixed(2) }} {{ ((scope.row.selectedMethod?.packagePrice || scope.row.price || 0) * (scope.row.quantity || 1)).toFixed(2) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" prop="checkType" width="70" align="center" /> <el-table-column label="类型" prop="checkType" width="70" align="center" />
@@ -307,22 +319,48 @@
</div> </div>
</div> </div>
<!-- 右侧已选择 tags --> <!-- 右侧已选择 项目卡片可展开显示检查方法 -->
<div class="selected-panel"> <div class="selected-panel">
<div class="panel-label">已选择:</div> <div class="panel-label">已选择:</div>
<div class="selected-tags"> <div class="selected-tags">
<div v-if="selectedItems.length === 0" class="empty-selected"></div> <div v-if="selectedItems.length === 0" class="empty-selected"></div>
<el-tag <div
v-else v-else
v-for="(item, idx) in selectedItems" v-for="(item, idx) in selectedItems"
:key="idx" :key="idx"
closable class="selected-item-card"
size="small"
@close="handleRemoveItem(idx, item)"
class="selected-tag"
> >
{{ item.name }} ¥{{ item.price }} <!-- Bug #384修复: 项目卡片头部可展开/收起 -->
</el-tag> <div class="card-header" @click="toggleItemExpand(item)">
<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>
<!-- 删除按钮 -->
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- Bug #384修复: 展开后显示检查方法勾选框列表 -->
<div v-if="item.expanded && item.methods && item.methods.length > 0" class="method-list">
<div
v-for="method in item.methods"
:key="method.id"
class="method-option"
>
<el-checkbox
:model-value="item.selectedMethod?.id === method.id"
@change="(val) => selectMethodCheckbox(val, item, method)"
>
<span class="method-name">{{ method.name }}</span>
<span class="method-price">¥{{ method.packagePrice || item.price }}</span>
</el-checkbox>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -334,10 +372,11 @@
<script setup> <script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'; import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete } from '@element-plus/icons-vue'; import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import request from '@/utils/request'; import request from '@/utils/request';
import { listCheckMethod } from '@/api/system/checkType'; import { listCheckMethod, searchCheckMethod } from '@/api/system/checkType';
import { getEncounterDiagnosis } from '../api.js';
const props = defineProps({ const props = defineProps({
patientInfo: { type: Object, default: () => ({}) }, patientInfo: { type: Object, default: () => ({}) },
@@ -384,7 +423,8 @@ const form = reactive({
isCharged: 0, isCharged: 0,
isRefunded: 0, isRefunded: 0,
isExecuted: 0, isExecuted: 0,
examTypeCode: '' // 检查类型编码,必填字段,保存时从已选项目自动推导 examTypeCode: '', // 检查类型编码,必填字段,保存时从已选项目自动推导
selectedMethodDisplay: '' // Bug #384修复: 检查方法显示字段(联动)
}); });
const rules = { const rules = {
@@ -590,6 +630,7 @@ async function loadCategoryList() {
unit: '次', unit: '次',
checkType: p.checkType || '', checkType: p.checkType || '',
nationalCode: p.nationalCode || '', nationalCode: p.nationalCode || '',
packageName: p.packageName || '',
checked: false checked: false
}; };
@@ -631,9 +672,11 @@ const filteredCategoryList = computed(() => {
}); });
// ====== 合计 ====== // ====== 合计 ======
// Bug #384修复: 如果选中了检查方法,使用套餐价格;否则使用部位价格
const totalAmountCalc = computed(() => { const totalAmountCalc = computed(() => {
const total = selectedItems.value.reduce((sum, item) => { const total = selectedItems.value.reduce((sum, item) => {
return sum + (item.price * (item.quantity || 1)); const effectivePrice = item.selectedMethod?.packagePrice || item.price;
return sum + (effectivePrice * (item.quantity || 1));
}, 0); }, 0);
return total.toFixed(2); return total.toFixed(2);
}); });
@@ -652,19 +695,49 @@ watch(() => props.patientInfo, (newVal) => {
} }
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
watch(() => props.activeTab, (val) => { watch(() => props.activeTab, async (val) => {
if (val === 'examination') getList(); if (val === 'examination') {
getList();
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
if (props.patientInfo?.encounterId) {
await loadClinicalDiag();
}
}
}); });
function initPatientForm(patient) { function initPatientForm(patient) {
form.patientName = patient.patientName || ''; form.patientName = patient.patientName || '';
form.medicalrecordNumber = patient.busNo || patient.visitNo || ''; // 就诊卡号应取值于 identifierNo而非 busNobusNo 是病历号)
form.medicalrecordNumber = patient.identifierNo || patient.visitNo || '';
form.patientId = patient.patientId || ''; form.patientId = patient.patientId || '';
form.visitNo = patient.visitNo || ''; form.visitNo = patient.visitNo || '';
form.applyDeptCode = userStore.orgName || patient.organizationName || ''; form.applyDeptCode = userStore.orgName || patient.organizationName || '';
form.applyDocCode = userStore.nickName || ''; form.applyDocCode = userStore.nickName || '';
} }
// 加载临床诊断:获取患者主诊断并填充到临床诊断字段
async function loadClinicalDiag() {
if (!props.patientInfo?.encounterId) return;
try {
const res = await getEncounterDiagnosis(props.patientInfo.encounterId);
const diagnoses = res.data || res.rows || res;
if (Array.isArray(diagnoses) && diagnoses.length > 0) {
// Bug #380, #381 修复: 主诊断字段名为 maindiseFlag (后端 DiagnosisQueryDto 定义)
const mainDiag = diagnoses.find(d => d.maindiseFlag === 1 || d.maindiseFlag === '1');
// 如果有主诊断使用主诊断,否则使用第一个诊断
const targetDiag = mainDiag || diagnoses[0];
// 优先使用 diagnosisName其次是 conditionName 或 name
form.clinicalDiag = targetDiag.diagnosisName || targetDiag.conditionName || targetDiag.name || '';
} else {
// 如果没有诊断,清空临床诊断字段
form.clinicalDiag = '';
}
} catch (err) {
console.error('加载临床诊断失败', err);
// 获取失败时不阻断用户操作,保持字段为空
}
}
// ====== 申请单 CRUD ====== // ====== 申请单 CRUD ======
function getList() { function getList() {
loading.value = true; loading.value = true;
@@ -682,24 +755,30 @@ function getList() {
function handleAdd() { function handleAdd() {
formRef.value?.resetFields(); formRef.value?.resetFields();
Object.assign(form, { Object.assign(form, {
applyNo: '', patientId: props.patientInfo?.patientId || '', applyNo: '',
patientId: props.patientInfo?.patientId || '',
visitNo: props.patientInfo?.visitNo || '', visitNo: props.patientInfo?.visitNo || '',
// 保留患者姓名和就诊卡号,不应重置为空
patientName: props.patientInfo?.patientName || '',
medicalrecordNumber: props.patientInfo?.identifierNo || '',
applyDeptCode: userStore.orgName || '', applyDeptCode: userStore.orgName || '',
performDeptCode: '', performDeptCode: '',
applyDocCode: userStore.nickName || '', applyDocCode: userStore.nickName || '',
applyTime: new Date().toISOString().split('T')[0] + ' 12:00:00', applyTime: new Date().toISOString().split('T')[0] + ' 12:00:00',
medicalrecordNumber: props.patientInfo?.busNo || '',
natureofCost: '自费医疗', natureofCost: '自费医疗',
clinicDesc: '', contraindication: '', medicalHistorySummary: '', clinicDesc: '', contraindication: '', medicalHistorySummary: '',
purposeofInspection: '', inspectionArea: '', inspectionMethod: '', purposeofInspection: '', inspectionArea: '', inspectionMethod: '',
applyRemark: '', clinicalDiag: '', purposeDesc: '', applyRemark: '', clinicalDiag: '', purposeDesc: '',
isUrgent: 0, pregnancyState: 0, allergyDesc: '', isUrgent: 0, pregnancyState: 0, allergyDesc: '',
applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0, applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0,
examTypeCode: '' examTypeCode: '',
selectedMethodDisplay: '' // Bug #384修复: 重置检查方法显示
}); });
selectedItems.value = []; selectedItems.value = [];
resetCategoryChecked(); resetCategoryChecked();
activeDetailTab.value = 'applyForm'; activeDetailTab.value = 'applyForm';
// 自动加载临床诊断
loadClinicalDiag();
} }
function handleSave() { function handleSave() {
@@ -713,6 +792,12 @@ function handleSave() {
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown'; const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
form.examTypeCode = firstCheckType; form.examTypeCode = firstCheckType;
// 如果有选中的检查方法,更新表单中的检查方法字段(取第一个选中项目的检查方法)
const firstItemWithMethod = selectedItems.value.find(item => item.selectedMethod);
if (firstItemWithMethod?.selectedMethod) {
form.inspectionMethod = firstItemWithMethod.selectedMethod.name;
}
const payload = { const payload = {
...form, ...form,
encounterId: props.patientInfo?.encounterId || null, encounterId: props.patientInfo?.encounterId || null,
@@ -721,10 +806,16 @@ function handleSave() {
itemCode: String(item.id), itemCode: String(item.id),
itemName: item.name, itemName: item.name,
bodyPartCode: item.checkType || 'unknown', bodyPartCode: item.checkType || 'unknown',
itemFee: item.price, // Bug #384修复: 如果选中了检查方法且有套餐价格,使用套餐价格;否则使用部位价格
itemFee: item.selectedMethod?.packagePrice || item.price,
performDeptCode: form.performDeptCode || '', performDeptCode: form.performDeptCode || '',
itemStatus: 0, itemStatus: 0,
itemSeq: index + 1 itemSeq: index + 1,
// 检查方法信息
checkMethodId: item.selectedMethod?.id || null,
checkMethodName: item.selectedMethod?.name || null,
checkMethodCode: item.selectedMethod?.code || null,
checkMethodPackageName: item.selectedMethod?.packageName || null // Bug #384修复: 保存套餐名称
})) }))
}; };
request({ request({
@@ -743,22 +834,70 @@ function handleSave() {
function handleRowClick(row) { function handleRowClick(row) {
Object.assign(form, row); Object.assign(form, row);
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
selectedItems.value = []; selectedItems.value = [];
activeDetailTab.value = 'applyForm'; activeDetailTab.value = 'applyForm';
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(res => { request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
const d = res.data || res; const d = res.data || res;
if (d.data) Object.assign(form, d.data); if (d.data) Object.assign(form, d.data);
if (d.items && Array.isArray(d.items)) { if (d.items && Array.isArray(d.items)) {
selectedItems.value = d.items.map(m => ({ try {
id: m.itemCode, name: m.itemName, // 为每个项目加载检查方法
price: m.itemFee || 0, quantity: 1, const itemsWithMethods = await Promise.all(d.items.map(async m => {
serviceFee: 0, unit: '次', const item = {
applyPart: m.itemName, id: m.itemCode, name: m.itemName,
checkType: m.bodyPartCode || '', price: m.itemFee || 0, quantity: 1,
nationalCode: '', checked: true serviceFee: 0, unit: '次',
})); applyPart: m.itemName,
syncCategoryChecked(); checkType: m.bodyPartCode || '',
nationalCode: '', checked: true,
methods: [],
selectedMethod: null,
expanded: false // Bug #384修复: 添加展开状态
};
// 加载该项目的检查方法
if (m.bodyPartCode) {
try {
const methodRes = await searchCheckMethod({ checkType: m.bodyPartCode });
// Bug #384修复: 正确解析 API 返回结构
let methodData = methodRes?.data?.data || methodRes?.data || methodRes?.rows || methodRes;
if (!Array.isArray(methodData) && methodRes?.data && Array.isArray(methodRes.data.data)) {
methodData = methodRes.data.data;
}
if (Array.isArray(methodData)) {
item.methods = methodData.map(md => ({
id: md.id,
name: md.name,
code: md.code,
price: m.itemFee || 0, // fallback 到已保存的价格
packageName: md.packageName || '',
packagePrice: md.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: md.serviceFee || null
}));
// 如果有已保存的检查方法信息,尝试匹配
if (m.checkMethodId) {
item.selectedMethod = item.methods.find(md => md.id === m.checkMethodId) || null;
}
}
} catch (err) {
console.error('加载检查方法失败', err);
// 单个项目加载失败不影响其他项目,继续返回 item
}
}
return item;
}));
selectedItems.value = itemsWithMethods;
syncCategoryChecked();
// Bug #384修复: 回充后更新检查方法显示
updateMethodDisplay();
} catch (err) {
console.error('加载申请单详情失败', err);
ElMessage.error('加载申请单详情失败');
}
} }
}).catch(err => {
console.error('获取申请单详情失败', err);
ElMessage.error('获取申请单详情失败');
}); });
} }
@@ -775,8 +914,37 @@ function handleDelete(row) {
} }
// ====== 勾选逻辑 ====== // ====== 勾选逻辑 ======
function handleItemSelect(checked, item, cat) { async function handleItemSelect(checked, item, cat) {
if (checked) { if (checked) {
// Bug #384修复: 检查方法表的 checkType 字段关联的是检查类型的 name中文名称如"心电图")
const effectiveCheckType = cat?.typeName || cat?.categoryName || '';
// 查询该检查类型对应的检查方法
let methods = [];
try {
if (effectiveCheckType) {
const res = await searchCheckMethod({ checkType: effectiveCheckType });
// Bug #384修复: API 返回结构可能是 {data: {data: Array}} 或 {data: Array}
let data = res?.data?.data || res?.data || res?.rows || res;
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
data = res.data.data;
}
if (Array.isArray(data)) {
methods = data.map(m => ({
id: m.id,
name: m.name,
code: m.code,
price: m.price || item.price, // fallback 到项目价格
packageName: m.packageName || '',
packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: m.serviceFee || null // Bug #384修复: 服务费
}));
}
}
} catch (err) {
console.error('加载检查方法失败', err);
}
if (selectedItems.value.length > 0) { if (selectedItems.value.length > 0) {
const currentCategory = selectedItems.value[0].checkType; const currentCategory = selectedItems.value[0].checkType;
const newCategory = cat.typeCode || ''; const newCategory = cat.typeCode || '';
@@ -793,9 +961,12 @@ function handleItemSelect(checked, item, cat) {
serviceFee: item.serviceFee || 0, serviceFee: item.serviceFee || 0,
unit: item.unit || '次', unit: item.unit || '次',
applyPart: item.name, applyPart: item.name,
checkType: cat.typeCode || '', checkType: effectiveCheckType, // Bug #384修复: 使用有效的 checkType
nationalCode: item.nationalCode || '', nationalCode: item.nationalCode || '',
checked: true checked: true,
methods: methods,
selectedMethod: null,
expanded: false // Bug #384修复: 新增展开状态,默认不展开
}); });
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室 // 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
@@ -804,6 +975,13 @@ function handleItemSelect(checked, item, cat) {
} else if (!form.performDeptCode && cat?.performDeptName) { } else if (!form.performDeptCode && cat?.performDeptName) {
form.performDeptCode = cat.performDeptName; form.performDeptCode = cat.performDeptName;
} }
// 如果有且仅有一个检查方法,自动选中并更新显示
if (methods.length === 1) {
const lastIdx = selectedItems.value.length - 1;
selectedItems.value[lastIdx].selectedMethod = methods[0];
updateMethodDisplay(); // Bug #384修复: 联动更新显示
}
} else { } else {
const idx = selectedItems.value.findIndex(s => s.id === item.id); const idx = selectedItems.value.findIndex(s => s.id === item.id);
if (idx > -1) selectedItems.value.splice(idx, 1); if (idx > -1) selectedItems.value.splice(idx, 1);
@@ -813,11 +991,45 @@ function handleItemSelect(checked, item, cat) {
form.examTypeCode = ''; form.examTypeCode = '';
} }
} }
// 有选项时切换到明细tab // Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
if (selectedItems.value.length > 0) { }
activeDetailTab.value = 'applyDetail';
nextTick(() => detailTableRef.value?.doLayout()); // Bug #384修复: 展开/收起项目卡片
function toggleItemExpand(item) {
item.expanded = !item.expanded;
}
// Bug #384修复: 勾选框选择检查方法(单选逻辑)
function selectMethodCheckbox(checked, item, method) {
if (checked) {
item.selectedMethod = method;
} else {
item.selectedMethod = null;
} }
// 联动更新表单检查方法显示字段
updateMethodDisplay();
}
// Bug #384修复: 更新检查方法显示字段(联动)
function updateMethodDisplay() {
// 找到第一个有选中检查方法的项目
const itemWithMethod = selectedItems.value.find(item => item.selectedMethod);
if (itemWithMethod?.selectedMethod) {
form.selectedMethodDisplay = itemWithMethod.selectedMethod.name; // 显示检查方法名称,不显示套餐名称
} else {
form.selectedMethodDisplay = '';
}
}
// 选择检查方法
function selectMethod(item, method) {
if (item.selectedMethod?.id === method.id) {
item.selectedMethod = null;
} else {
item.selectedMethod = method;
}
// Bug #384修复: 联动更新表单检查方法显示字段
updateMethodDisplay();
} }
function handleRemoveItem(idx, item) { function handleRemoveItem(idx, item) {
@@ -827,10 +1039,14 @@ function handleRemoveItem(idx, item) {
const found = cat.items.find(x => x.id === item.id); const found = cat.items.find(x => x.id === item.id);
if (found) { found.checked = false; break; } if (found) { found.checked = false; break; }
} }
if (selectedItems.value.length === 0) { if (selectedItems.value.length === 0) {
form.performDeptCode = ''; form.performDeptCode = '';
form.examTypeCode = ''; form.examTypeCode = '';
form.selectedMethodDisplay = ''; // Bug #384修复: 清空检查方法显示
} else {
// Bug #384修复: 移除后重新计算检查方法显示
updateMethodDisplay();
} }
} }
@@ -999,7 +1215,7 @@ defineExpose({ getList });
/* 已选择 tags */ /* 已选择 tags */
.selected-panel { .selected-panel {
width: 120px; width: 140px; /* Bug #384修复: 加宽以适应展开内容 */
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1009,7 +1225,7 @@ defineExpose({ getList });
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
} }
.selected-tag { .selected-tag {
max-width: 100%; max-width: 100%;
@@ -1022,6 +1238,86 @@ defineExpose({ getList });
font-size: 12px; font-size: 12px;
} }
/* Bug #384修复: 已选择项目卡片(可展开) */
.selected-item-card {
display: flex;
flex-direction: column;
background: #F5F5F5;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.selected-item-card .card-header {
display: flex;
align-items: center;
padding: 8px 10px;
cursor: pointer;
gap: 4px;
}
.selected-item-card .card-header:hover {
background: #E6F7FF;
}
.card-name {
flex: 1;
font-size: 12px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-price {
font-size: 12px;
color: #1890FF;
font-weight: 500;
}
.expand-icon {
font-size: 12px;
color: #909399;
transition: transform 0.2s;
}
.expand-icon.expanded {
transform: rotate(180deg);
}
/* Bug #384修复: 检查方法勾选框列表 */
.method-list {
padding: 6px 10px;
background: #fff;
border-top: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
gap: 4px;
}
.method-option {
display: flex;
align-items: center;
}
.method-option :deep(.el-checkbox__label) {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.method-option .method-name {
font-size: 11px;
color: #606266;
}
.method-option .method-price {
font-size: 11px;
color: #e6a23c;
font-weight: 500;
margin-left: 8px;
}
/* 折叠组件细节 */ /* 折叠组件细节 */
:deep(.el-collapse) { :deep(.el-collapse) {
border: none; border: none;

View File

@@ -45,35 +45,10 @@
class="inspection-table" class="inspection-table"
highlight-current-row highlight-current-row
row-key="applicationId" row-key="applicationId"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@current-change="handleRowClick" @current-change="handleRowClick"
@cell-click="handleCellClick" @cell-click="handleCellClick"
> >
<el-table-column type="expand" width="50" align="center" header-align="center">
<template #default="scope">
<div v-if="scope.row.children && scope.row.children.length > 0" class="expand-content">
<el-table :data="scope.row.children" border size="small" style="width: 100%">
<el-table-column label="明细项目" prop="itemName" min-width="150" />
<el-table-column label="样本类型" prop="sampleType" width="100" />
<el-table-column label="单位" prop="unit" width="80" />
<el-table-column label="单价" prop="itemPrice" width="80" align="right">
<template #default="itemScope">
¥{{ formatAmount(itemScope.row.itemPrice) }}
</template>
</el-table-column>
<el-table-column label="数量" prop="itemQty" width="80" align="center" />
<el-table-column label="金额" prop="itemAmount" width="80" align="right">
<template #default="itemScope">
¥{{ formatAmount(itemScope.row.itemAmount) }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="expand-empty">无明细项目</div>
</template>
</el-table-column>
<el-table-column type="selection" width="55" align="center" header-align="center" /> <el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column label="申请 ID" prop="applicationId" width="80" align="center" header-align="center"> <el-table-column label="申请 ID" prop="applicationId" width="80" align="center" header-align="center">
<template #default="scope"> <template #default="scope">
@@ -83,14 +58,7 @@
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" /> <el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center"> <el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.hasChildren" style="color: #409EFF; cursor: pointer" @click.stop="toggleExpand(scope.row)"> <span>{{ scope.row.itemName }}</span>
<el-icon style="vertical-align: middle; margin-right: 4px">
<Right v-if="!isExpanded(scope.row.applicationId)" />
<Bottom v-else />
</el-icon>
{{ scope.row.itemName }}
</span>
<span v-else>{{ scope.row.itemName }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="申请医生" prop="applyDocName" width="120" align="center" header-align="center" /> <el-table-column label="申请医生" prop="applyDocName" width="120" align="center" header-align="center" />
@@ -645,7 +613,7 @@
<script setup> <script setup>
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue' import {onMounted, onUnmounted, 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, Loading, Right, Bottom } from '@element-plus/icons-vue' import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
import { import {
deleteInspectionApplication, getApplyList, deleteInspectionApplication, getApplyList,
saveInspectionApplication, saveInspectionApplication,
@@ -792,53 +760,39 @@ const loading = ref(false)
const saving = ref(false) // 保存状态 const saving = ref(false) // 保存状态
const total = ref(0) const total = ref(0)
const leftActiveTab = ref('application') const leftActiveTab = ref('application')
const expandedRowKeys = ref([])
const isExpanded = (applicationId) => { /**
return expandedRowKeys.value.includes(applicationId) * 加载套餐明细(公共函数)
} * @param {string|number} packageId 套餐ID
* @returns {Promise<Array>} 明细数组
const toggleExpand = async (row) => { */
const applicationId = row.applicationId const fetchPackageDetails = async (packageId) => {
const isCurrentlyExpanded = isExpanded(applicationId) if (!packageId) return []
if (isCurrentlyExpanded) {
// 收起
expandedRowKeys.value = expandedRowKeys.value.filter(id => id !== applicationId)
} else {
// 展开 - 先检查是否需要加载明细数据
if (row.hasChildren && (!row.children || row.children.length === 0)) {
// 加载套餐明细
await loadPackageDetails(row)
}
expandedRowKeys.value = [...expandedRowKeys.value, applicationId]
}
}
const handleExpandChange = (row, expandedRows) => {
expandedRowKeys.value = expandedRows.map(r => r.applicationId)
}
const loadPackageDetails = async (row) => {
try { try {
const packageId = row.feePackageId || row.packageId
if (!packageId) return
const res = await getInspectionPackageDetails(packageId) const res = await getInspectionPackageDetails(packageId)
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
row.children = res.data.map(detail => ({ return res.data.map(detail => {
itemId: detail.detailId || detail.id || detail.itemId, const detailId = detail.detailId || detail.id || detail.itemId
itemName: detail.itemName || detail.name, const qty = detail.quantity || detail.itemQty || detail.qty || 1
sampleType: detail.sampleType || '', const price = detail.unitPrice || detail.itemPrice || detail.price || 0
unit: detail.unit || '', return {
itemPrice: detail.unitPrice || detail.itemPrice || detail.price || 0, detailId: detailId,
itemQty: detail.quantity || detail.itemQty || detail.qty || 1, itemId: detailId, // 兼容表格 row-key
itemAmount: (detail.unitPrice || detail.itemPrice || 0) * (detail.quantity || detail.itemQty || 1) itemName: detail.itemName || detail.name,
})) sampleType: detail.sampleType || '',
unit: detail.unit || '',
quantity: qty,
itemQty: qty, // 兼容表格"总量"列
unitPrice: price,
itemPrice: price, // 兼容表格"单价"列
itemAmount: price * qty
}
})
} }
return []
} catch (error) { } catch (error) {
console.error('加载套餐明细失败:', error) console.error('加载套餐明细失败:', error)
row.children = [] return []
} }
} }
@@ -847,35 +801,9 @@ const loadPackageDetailsForTable = async (row, treeNode, resolve) => {
resolve([]) resolve([])
return return
} }
const packageId = row.feePackageId || row.packageId
try { const children = await fetchPackageDetails(packageId)
const packageId = row.feePackageId || row.packageId resolve(children)
if (!packageId) {
resolve([])
return
}
const res = await getInspectionPackageDetails(packageId)
if (res.code === 200 && res.data) {
// 构建明细数据结构
// BugFix: 后端返回字段为 unitPrice 和 quantity需正确映射
const children = res.data.map(detail => ({
itemId: detail.detailId || detail.id || detail.itemId,
itemName: detail.itemName || detail.name,
sampleType: detail.sampleType || '',
unit: detail.unit || '',
itemPrice: detail.unitPrice || detail.itemPrice || detail.price || 0,
itemQty: detail.quantity || detail.itemQty || detail.qty || 1,
itemAmount: (detail.unitPrice || detail.itemPrice || 0) * (detail.quantity || detail.itemQty || 1)
}))
resolve(children)
} else {
resolve([])
}
} catch (error) {
console.error('加载套餐明细失败:', error)
resolve([])
}
} }
const togglePackageExpand = async (item) => { const togglePackageExpand = async (item) => {
@@ -885,27 +813,9 @@ const togglePackageExpand = async (item) => {
if (item.expanded && (!item.children || item.children.length === 0)) { if (item.expanded && (!item.children || item.children.length === 0)) {
item.loading = true item.loading = true
try { const packageId = item.feePackageId || item.packageId
const packageId = item.feePackageId || item.packageId item.children = await fetchPackageDetails(packageId)
if (packageId) { item.loading = false
const res = await getInspectionPackageDetails(packageId)
if (res.code === 200 && res.data) {
item.children = res.data.map(detail => ({
detailId: detail.detailId || detail.id || detail.itemId,
itemName: detail.itemName || detail.name,
unit: detail.unit || '',
quantity: detail.quantity || detail.itemQty || detail.qty || 1,
unitPrice: detail.unitPrice || detail.itemPrice || detail.price || 0,
itemPrice: detail.unitPrice || detail.itemPrice || detail.price || 0
}))
}
}
} catch (error) {
console.error('加载套餐明细失败:', error)
item.children = []
} finally {
item.loading = false
}
} }
} }
@@ -1822,6 +1732,26 @@ const clearAllSelected = () => {
selectedInspectionItems.value = [] selectedInspectionItems.value = []
} }
// Bug #387修复: 同步分类勾选状态
const syncCategoryChecked = () => {
// 重置所有分类项目的勾选状态
inspectionCategories.value.forEach(category => {
category.items.forEach(item => {
item.checked = false
})
})
// 获取已选项目的ID集合
const ids = new Set(selectedInspectionItems.value.map(s => s.itemId))
// 同步勾选状态
for (const cat of inspectionCategories.value) {
for (const item of cat.items) {
if (ids.has(item.itemId)) {
item.checked = true
}
}
}
}
// 分页大小改变 // 分页大小改变
const handleSizeChange = (size) => { const handleSizeChange = (size) => {
queryParams.pageSize = size queryParams.pageSize = size
@@ -1977,6 +1907,7 @@ const loadApplicationToForm = async (row) => {
if (detail.labApplyItemList && detail.labApplyItemList.length > 0) { if (detail.labApplyItemList && detail.labApplyItemList.length > 0) {
// Bug #326修复: 直接使用后端返回的数据,不再从本地缓存查找匹配项 // Bug #326修复: 直接使用后端返回的数据,不再从本地缓存查找匹配项
// 后端已返回完整关联信息activityId、feePackageId、inspectionTypeId、sampleType、unit // 后端已返回完整关联信息activityId、feePackageId、inspectionTypeId、sampleType、unit
// Bug #387修复: 套餐项目默认展开,并自动加载明细
selectedInspectionItems.value = detail.labApplyItemList.map(item => { selectedInspectionItems.value = detail.labApplyItemList.map(item => {
const itemId = item.activityId || item.itemId || item.id || Math.random().toString(36).substring(2, 11) const itemId = item.activityId || item.itemId || item.id || Math.random().toString(36).substring(2, 11)
const isPackage = item.feePackageId != null || item.itemName?.includes('套餐') const isPackage = item.feePackageId != null || item.itemName?.includes('套餐')
@@ -1995,12 +1926,25 @@ const loadApplicationToForm = async (row) => {
feePackageId: item.feePackageId || null, feePackageId: item.feePackageId || null,
activityId: item.activityId || itemId, activityId: item.activityId || itemId,
inspectionTypeId: item.inspectionTypeId || null, inspectionTypeId: item.inspectionTypeId || null,
expanded: false, expanded: isPackage, // Bug #387: 套餐默认展开
children: [], children: [],
childrenLoaded: false, childrenLoaded: !isPackage, // Bug #387: 套餐需加载明细
loading: false loading: false
} }
}) })
// Bug #387修复: 自动加载套餐明细
for (const pkgItem of selectedInspectionItems.value) {
if (pkgItem.isPackage && pkgItem.feePackageId) {
pkgItem.loading = true
pkgItem.children = await fetchPackageDetails(pkgItem.feePackageId)
pkgItem.childrenLoaded = true
pkgItem.loading = false
}
}
// Bug #387修复: 同步分类勾选状态
syncCategoryChecked()
} else if (detail.inspectionItem || detail.itemName) { } else if (detail.inspectionItem || detail.itemName) {
// 如果只有项目名称,尝试从本地分类中查找匹配项 // 如果只有项目名称,尝试从本地分类中查找匹配项
const itemNames = (detail.inspectionItem || detail.itemName).split(/[+,]/) const itemNames = (detail.inspectionItem || detail.itemName).split(/[+,]/)

View File

@@ -181,6 +181,7 @@ function handleGetPrescription() {
getPrescriptionList({ getPrescriptionList({
encounterIds: encounterIds, encounterIds: encounterIds,
requestStatus: props.requestStatus, requestStatus: props.requestStatus,
therapyEnum: type.value === 1 ? undefined : type.value === 2 ? 1 : 2, // 1=全部(不传), 2=长期(1), 3=临时(2)
pageSize: 10000, pageSize: 10000,
pageNo: 1, pageNo: 1,
}).then((res) => { }).then((res) => {