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

@@ -104,14 +104,14 @@
</el-col>
<el-col :span="8">
<el-form-item label="申请科室" prop="applyDeptCode">
<el-input v-model="form.applyDeptCode" />
<el-input v-model="form.applyDeptCode" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="申请医生" prop="applyDocCode">
<el-input v-model="form.applyDocCode" />
<el-input v-model="form.applyDocCode" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
@@ -184,16 +184,10 @@
<el-input v-model="form.inspectionArea" readonly />
</el-form-item>
</el-col>
<!-- Bug #384修复: 添加检查方法只读输入框联动显示选中的检查方法 -->
<el-col :span="8">
<el-form-item label="检查方法">
<el-select v-model="form.inspectionMethod" placeholder="请选择" clearable filterable style="width: 100%;">
<el-option
v-for="method in availableMethods"
:key="method.id"
:label="method.name"
:value="method.name"
/>
</el-select>
<el-input v-model="form.selectedMethodDisplay" readonly placeholder="请在右侧选择" />
</el-form-item>
</el-col>
</el-row>
@@ -233,16 +227,34 @@
<el-input v-model="scope.row.applyPart" size="small" />
</template>
</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="quantity" width="70" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" />
</template>
</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">
<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>
</el-table-column>
<el-table-column label="类型" prop="checkType" width="70" align="center" />
@@ -307,22 +319,48 @@
</div>
</div>
<!-- 右侧已选择 tags -->
<!-- 右侧已选择 项目卡片可展开显示检查方法 -->
<div class="selected-panel">
<div class="panel-label">已选择:</div>
<div class="selected-tags">
<div v-if="selectedItems.length === 0" class="empty-selected"></div>
<el-tag
<div
v-else
v-for="(item, idx) in selectedItems"
:key="idx"
closable
size="small"
@close="handleRemoveItem(idx, item)"
class="selected-tag"
class="selected-item-card"
>
{{ item.name }} ¥{{ item.price }}
</el-tag>
<!-- Bug #384修复: 项目卡片头部可展开/收起 -->
<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>
@@ -334,10 +372,11 @@
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
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 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({
patientInfo: { type: Object, default: () => ({}) },
@@ -384,7 +423,8 @@ const form = reactive({
isCharged: 0,
isRefunded: 0,
isExecuted: 0,
examTypeCode: '' // 检查类型编码,必填字段,保存时从已选项目自动推导
examTypeCode: '', // 检查类型编码,必填字段,保存时从已选项目自动推导
selectedMethodDisplay: '' // Bug #384修复: 检查方法显示字段(联动)
});
const rules = {
@@ -590,6 +630,7 @@ async function loadCategoryList() {
unit: '次',
checkType: p.checkType || '',
nationalCode: p.nationalCode || '',
packageName: p.packageName || '',
checked: false
};
@@ -631,9 +672,11 @@ const filteredCategoryList = computed(() => {
});
// ====== 合计 ======
// Bug #384修复: 如果选中了检查方法,使用套餐价格;否则使用部位价格
const totalAmountCalc = computed(() => {
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);
return total.toFixed(2);
});
@@ -652,19 +695,49 @@ watch(() => props.patientInfo, (newVal) => {
}
}, { immediate: true, deep: true });
watch(() => props.activeTab, (val) => {
if (val === 'examination') getList();
watch(() => props.activeTab, async (val) => {
if (val === 'examination') {
getList();
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
if (props.patientInfo?.encounterId) {
await loadClinicalDiag();
}
}
});
function initPatientForm(patient) {
form.patientName = patient.patientName || '';
form.medicalrecordNumber = patient.busNo || patient.visitNo || '';
// 就诊卡号应取值于 identifierNo而非 busNobusNo 是病历号)
form.medicalrecordNumber = patient.identifierNo || patient.visitNo || '';
form.patientId = patient.patientId || '';
form.visitNo = patient.visitNo || '';
form.applyDeptCode = userStore.orgName || patient.organizationName || '';
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 ======
function getList() {
loading.value = true;
@@ -682,24 +755,30 @@ function getList() {
function handleAdd() {
formRef.value?.resetFields();
Object.assign(form, {
applyNo: '', patientId: props.patientInfo?.patientId || '',
applyNo: '',
patientId: props.patientInfo?.patientId || '',
visitNo: props.patientInfo?.visitNo || '',
// 保留患者姓名和就诊卡号,不应重置为空
patientName: props.patientInfo?.patientName || '',
medicalrecordNumber: props.patientInfo?.identifierNo || '',
applyDeptCode: userStore.orgName || '',
performDeptCode: '',
applyDocCode: userStore.nickName || '',
applyTime: new Date().toISOString().split('T')[0] + ' 12:00:00',
medicalrecordNumber: props.patientInfo?.busNo || '',
natureofCost: '自费医疗',
clinicDesc: '', contraindication: '', medicalHistorySummary: '',
purposeofInspection: '', inspectionArea: '', inspectionMethod: '',
applyRemark: '', clinicalDiag: '', purposeDesc: '',
isUrgent: 0, pregnancyState: 0, allergyDesc: '',
applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0,
examTypeCode: ''
examTypeCode: '',
selectedMethodDisplay: '' // Bug #384修复: 重置检查方法显示
});
selectedItems.value = [];
resetCategoryChecked();
activeDetailTab.value = 'applyForm';
// 自动加载临床诊断
loadClinicalDiag();
}
function handleSave() {
@@ -713,6 +792,12 @@ function handleSave() {
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
form.examTypeCode = firstCheckType;
// 如果有选中的检查方法,更新表单中的检查方法字段(取第一个选中项目的检查方法)
const firstItemWithMethod = selectedItems.value.find(item => item.selectedMethod);
if (firstItemWithMethod?.selectedMethod) {
form.inspectionMethod = firstItemWithMethod.selectedMethod.name;
}
const payload = {
...form,
encounterId: props.patientInfo?.encounterId || null,
@@ -721,10 +806,16 @@ function handleSave() {
itemCode: String(item.id),
itemName: item.name,
bodyPartCode: item.checkType || 'unknown',
itemFee: item.price,
// Bug #384修复: 如果选中了检查方法且有套餐价格,使用套餐价格;否则使用部位价格
itemFee: item.selectedMethod?.packagePrice || item.price,
performDeptCode: form.performDeptCode || '',
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({
@@ -743,22 +834,70 @@ function handleSave() {
function handleRowClick(row) {
Object.assign(form, row);
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
selectedItems.value = [];
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;
if (d.data) Object.assign(form, d.data);
if (d.items && Array.isArray(d.items)) {
selectedItems.value = d.items.map(m => ({
id: m.itemCode, name: m.itemName,
price: m.itemFee || 0, quantity: 1,
serviceFee: 0, unit: '次',
applyPart: m.itemName,
checkType: m.bodyPartCode || '',
nationalCode: '', checked: true
}));
syncCategoryChecked();
try {
// 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(d.items.map(async m => {
const item = {
id: m.itemCode, name: m.itemName,
price: m.itemFee || 0, quantity: 1,
serviceFee: 0, unit: '次',
applyPart: m.itemName,
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) {
// 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) {
const currentCategory = selectedItems.value[0].checkType;
const newCategory = cat.typeCode || '';
@@ -793,9 +961,12 @@ function handleItemSelect(checked, item, cat) {
serviceFee: item.serviceFee || 0,
unit: item.unit || '次',
applyPart: item.name,
checkType: cat.typeCode || '',
checkType: effectiveCheckType, // Bug #384修复: 使用有效的 checkType
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) {
form.performDeptCode = cat.performDeptName;
}
// 如果有且仅有一个检查方法,自动选中并更新显示
if (methods.length === 1) {
const lastIdx = selectedItems.value.length - 1;
selectedItems.value[lastIdx].selectedMethod = methods[0];
updateMethodDisplay(); // Bug #384修复: 联动更新显示
}
} else {
const idx = selectedItems.value.findIndex(s => s.id === item.id);
if (idx > -1) selectedItems.value.splice(idx, 1);
@@ -813,11 +991,45 @@ function handleItemSelect(checked, item, cat) {
form.examTypeCode = '';
}
}
// 有选项时切换到明细tab
if (selectedItems.value.length > 0) {
activeDetailTab.value = 'applyDetail';
nextTick(() => detailTableRef.value?.doLayout());
// Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
}
// 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) {
@@ -827,10 +1039,14 @@ function handleRemoveItem(idx, item) {
const found = cat.items.find(x => x.id === item.id);
if (found) { found.checked = false; break; }
}
if (selectedItems.value.length === 0) {
form.performDeptCode = '';
form.examTypeCode = '';
form.selectedMethodDisplay = ''; // Bug #384修复: 清空检查方法显示
} else {
// Bug #384修复: 移除后重新计算检查方法显示
updateMethodDisplay();
}
}
@@ -999,7 +1215,7 @@ defineExpose({ getList });
/* 已选择 tags */
.selected-panel {
width: 120px;
width: 140px; /* Bug #384修复: 加宽以适应展开内容 */
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -1009,7 +1225,7 @@ defineExpose({ getList });
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
}
.selected-tag {
max-width: 100%;
@@ -1022,6 +1238,86 @@ defineExpose({ getList });
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) {
border: none;