Files
his/openhis-ui-vue3/src/views/doctorstation/components/examination/examinationApplication.vue

1380 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="exam-app-container">
<!-- ====== 顶部卡片申请单列表 ====== -->
<div class="top-section">
<div class="section-header">
<span class="section-title">检查项目 ({{ applicationList.length }})</span>
<div class="header-actions">
<el-button type="primary" @click="handleAdd" icon="Plus">新增</el-button>
<el-button type="success" @click="handleSave" icon="Finished">保存</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="applicationList"
:max-height="200"
highlight-current-row
@row-click="handleRowClick"
border
size="small"
:header-cell-style="{ background: '#f5f5f5', color: '#303133', fontWeight: '600' }"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column label="申请ID" prop="id" width="80" align="center" />
<el-table-column label="申请单号" prop="applyNo" min-width="140" align="center" />
<el-table-column label="申检部位" prop="inspectionArea" min-width="100" align="center" />
<el-table-column label="申请医生" prop="applyDocCode" min-width="90" align="center" />
<el-table-column label="急" prop="isUrgent" width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.isUrgent" :true-label="1" :false-label="0" disabled />
</template>
</el-table-column>
<el-table-column label="收费" prop="isCharged" width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.isCharged" :true-label="1" :false-label="0" disabled />
</template>
</el-table-column>
<el-table-column label="退费" prop="isRefunded" width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.isRefunded" :true-label="1" :false-label="0" disabled />
</template>
</el-table-column>
<el-table-column label="执行" prop="isExecuted" width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.isExecuted" :true-label="1" :false-label="0" disabled />
</template>
</el-table-column>
<el-table-column label="金额" prop="totalAmount" width="90" align="right">
<template #default="{ row }">
{{ (row.totalAmount || 0).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link @click.stop="handlePrint(row)" title="打印">
<el-icon><Printer /></el-icon>
</el-button>
<el-button link type="danger" @click.stop="handleDelete(row)" title="删除">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- ====== 底部主区左表单 + 右分类 ====== -->
<div class="bottom-section">
<!-- 表单区 -->
<div class="form-panel">
<el-tabs v-model="activeDetailTab" class="form-tabs">
<!-- TAB1检查申请单 -->
<el-tab-pane label="检查申请单" name="applyForm">
<el-form ref="formRef" :model="form" :rules="rules" size="small" class="apply-form">
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="申请单号" prop="applyNo">
<el-input v-model="form.applyNo" readonly placeholder="自动生成" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="姓名" prop="patientName">
<el-input v-model="form.patientName" readonly />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="就诊卡号" prop="medicalrecordNumber">
<el-input v-model="form.medicalrecordNumber" readonly />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="费用性质" prop="natureofCost">
<el-select v-model="form.natureofCost" style="width:100%">
<el-option label="自费医疗" value="自费医疗" />
<el-option label="医保报销" value="医保报销" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请日期" prop="applyTime">
<el-date-picker v-model="form.applyTime" type="date" style="width:100%"
format="YYYY-MM-DD" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请科室" prop="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" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="执行科室" prop="performDeptCode">
<el-select
v-model="form.performDeptCode"
style="width: 100%"
filterable
remote
reserve-keyword
clearable
placeholder="请选择执行科室(支持模糊查询)"
:remote-method="handleOrgRemoteSearch"
:loading="orgLoading"
>
<el-option
v-for="opt in orgFilteredOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
<template #empty>
<div style="padding: 10px 0; color: #909399; text-align: center;">
{{ orgLoading ? '加载中...' : '暂无匹配科室' }}
</div>
</template>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="诊断描述" prop="clinicDesc">
<el-input v-model="form.clinicDesc" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="禁忌症" prop="contraindication">
<el-input v-model="form.contraindication" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="临床诊断" prop="clinicalDiag">
<el-input v-model="form.clinicalDiag" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="病史摘要" prop="medicalHistorySummary">
<el-input v-model="form.medicalHistorySummary" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="检查目的" prop="purposeDesc">
<el-input v-model="form.purposeDesc" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="体格检查">
<el-input v-model="form.purposeofInspection" placeholder="T(摄氏度) P次/分 R次/分 BF" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申检部位">
<el-input v-model="form.inspectionArea" readonly />
</el-form-item>
</el-col>
<!-- Bug #384修复: 添加检查方法只读输入框联动显示选中的检查方法 -->
<el-col :span="8">
<el-form-item label="检查方法">
<el-input v-model="form.selectedMethodDisplay" readonly placeholder="请在右侧选择" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="备注" prop="applyRemark">
<el-input v-model="form.applyRemark" />
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="状态">
<el-checkbox v-model="form.isUrgent" :true-label="1" :false-label="0"></el-checkbox>
<el-checkbox v-model="form.isCharged" :true-label="1" :false-label="0" disabled>收费</el-checkbox>
<el-checkbox v-model="form.isRefunded" :true-label="1" :false-label="0" disabled>退费</el-checkbox>
<el-checkbox v-model="form.isExecuted" :true-label="1" :false-label="0" disabled>执行</el-checkbox>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<!-- TAB2检查明细 -->
<el-tab-pane label="检查明细" name="applyDetail">
<!-- 🔧 BugFix#426: 支持树形展开显示套餐明细 -->
<el-table
ref="detailTableRef"
:data="selectedItems"
row-key="id"
border
size="small"
style="width:100%"
:max-height="350"
:header-cell-style="{ background: '#f5f5f5', color: '#303133' }"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:load="loadPackageDetails"
lazy
>
<el-table-column label="行" type="index" width="45" align="center" />
<el-table-column label="检查项目" prop="name" min-width="120">
<template #default="scope">
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="部位" prop="applyPart" min-width="90">
<template #default="scope">
<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>
<!-- 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.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" />
<el-table-column label="国码" prop="nationalCode" width="70" align="center" />
<el-table-column label="自费" width="50" align="center">
<template #default>
<el-checkbox disabled />
</template>
</el-table-column>
</el-table>
<div class="total-row">
合计<span class="total-amount">{{ totalAmountCalc }}</span>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 检查项目分类面板 -->
<div class="category-panel">
<div class="panel-top">
<!-- 左侧分类搜索 + 折叠树 -->
<div class="category-left">
<div class="panel-label">检查项目分类</div>
<el-input
v-model="dictSearchKey"
placeholder="搜索检查项目(支持拼音首字母)"
prefix-icon="Search"
clearable
size="small"
class="search-input"
/>
<!-- 分类折叠列表 -->
<div class="collapse-scroll" v-loading="dictLoading">
<div v-if="filteredCategoryList.length === 0" class="empty-hint">
{{ dictLoading ? '' : '暂无检查项目请在"检查项目设置"中配置' }}
</div>
<el-collapse v-else v-model="activeNames">
<el-collapse-item
v-for="cat in filteredCategoryList"
:key="cat.typeId"
:name="cat.typeId"
>
<template #title>
<span class="cat-title">{{ cat.categoryName }}</span>
</template>
<div
v-for="item in cat.items"
:key="item.id"
class="item-row"
>
<el-checkbox
v-model="item.checked"
@change="(val) => handleItemSelect(val, item, cat)"
class="item-checkbox"
>
{{ item.name }}
</el-checkbox>
<span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
<!-- 右侧已选择 项目卡片可展开显示检查方法 -->
<div class="selected-panel">
<div class="panel-label">已选择:</div>
<div class="selected-tags">
<div v-if="selectedItems.length === 0" class="empty-selected"></div>
<div
v-else
v-for="(item, idx) in selectedItems"
:key="idx"
class="selected-item-card"
>
<!-- 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>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user';
import request from '@/utils/request';
import { listCheckMethod, searchCheckMethod } from '@/api/system/checkType';
import { getEncounterDiagnosis } from '../api.js';
const props = defineProps({
patientInfo: { type: Object, default: () => ({}) },
activeTab: { type: String, default: '' }
});
// 保存成功后通知父组件刷新医嘱列表
const emit = defineEmits(['saved']);
const userStore = useUserStore();
const loading = ref(false);
const dictLoading = ref(false);
const activeDetailTab = ref('applyForm');
const applicationList = ref([]);
const selectedItems = ref([]);
// 🔧 BugFix#426: 懒加载套餐明细
async function loadPackageDetails(row, treeNode, resolve) {
if (!row.isPackage || !row.packageId) {
resolve([]);
return;
}
try {
const res = await request({
url: `/exam/package/${row.packageId}/details`,
method: 'get'
});
if (res.code === 200 && res.data) {
const children = res.data.map(item => ({
...item,
name: item.name || item.itemName,
unit: item.unit || '次',
price: item.price || item.itemPrice || 0,
quantity: row.quantity || 1,
isPackageDetail: true
}));
resolve(children);
} else {
resolve([]);
}
} catch (err) {
console.error('加载套餐明细失败:', err);
resolve([]);
}
}
const detailTableRef = ref(null);
const formRef = ref(null);
// ====== 表单数据 ======
const form = reactive({
applyNo: '',
patientName: '',
patientId: '',
visitNo: '',
applyDeptCode: '',
performDeptCode: '',
applyDocCode: '',
applyTime: '',
medicalrecordNumber: '',
natureofCost: '自费医疗',
clinicDesc: '',
contraindication: '',
medicalHistorySummary: '',
purposeofInspection: '',
inspectionArea: '',
inspectionMethod: '',
applyRemark: '',
clinicalDiag: '',
purposeDesc: '',
isUrgent: 0,
pregnancyState: 0,
allergyDesc: '',
applyStatus: 0,
isCharged: 0,
isRefunded: 0,
isExecuted: 0,
examTypeCode: '', // 检查类型编码,必填字段,保存时从已选项目自动推导
selectedMethodDisplay: '' // Bug #384修复: 检查方法显示字段(联动)
});
const rules = {
natureofCost: [{ required: true, message: '请选择费用性质', trigger: 'change' }],
applyDeptCode: [{ required: true, message: '请输入申请科室', trigger: 'blur' }],
applyDocCode: [{ required: true, message: '请输入申请医生', trigger: 'blur' }],
performDeptCode: [{ required: true, message: '请输入执行科室', trigger: 'blur' }],
clinicDesc: [{ required: true, message: '请输入诊断描述', trigger: 'blur' }],
clinicalDiag: [{ required: true, message: '请输入临床诊断', trigger: 'blur' }],
medicalHistorySummary: [{ required: true, message: '请输入病史摘要', trigger: 'blur' }],
purposeDesc: [{ required: true, message: '请输入检查目的', trigger: 'blur' }]
};
// ====== 检查项目分类 ======
const categoryList = ref([]); // 原始分类+项目数据
const dictSearchKey = ref('');
const activeNames = ref([]); // 当前展开的折叠项
const allMethods = ref([]);
// ====== 科室下拉(来源:科室管理)======
const orgLoading = ref(false);
const orgOptions = ref([]); // { label, value }
const orgFilteredOptions = ref([]); // 展示用截断前200条
// 加载所有检查方法
async function loadAllMethods() {
try {
const res = await listCheckMethod(); // 使用已导入的或者直接利用 request 请求
let methods = [];
if (res && res.data) {
if (Array.isArray(res.data)) {
methods = res.data;
} else if (res.data.records) {
methods = res.data.records;
} else if (res.data.data && Array.isArray(res.data.data)) {
methods = res.data.data;
}
} else if (Array.isArray(res)) {
methods = res;
} else if (res && res.rows) {
methods = res.rows;
}
allMethods.value = methods;
} catch (err) {
console.error('加载检查方法失败', err);
}
}
onMounted(async () => {
await loadOrgOptions();
await loadAllMethods();
await loadCategoryList();
});
async function loadOrgOptions() {
orgLoading.value = true;
try {
const res = await request({
url: '/base-data-manage/organization/organization',
method: 'get',
});
const records = res?.data?.records || res?.data || [];
const flat = [];
const walk = (nodes) => {
if (!Array.isArray(nodes)) return;
for (const n of nodes) {
if (!n) continue;
// 约定typeEnum=2 为科室;若没有 typeEnum 也兜底收集
if (n.name && (n.typeEnum === 2 || n.typeEnum === '2' || n.typeEnum == null)) {
flat.push({ label: n.name, value: n.name });
}
if (Array.isArray(n.children) && n.children.length > 0) walk(n.children);
}
};
walk(records);
// 去重 + 排序
const uniq = Array.from(new Map(flat.map(o => [o.value, o])).values())
.filter(o => o?.value)
.sort((a, b) => (a.label || '').localeCompare(b.label || '', 'zh-CN'));
orgOptions.value = uniq;
orgFilteredOptions.value = uniq.slice(0, 200);
} catch (e) {
console.error('加载科室列表失败', e);
orgOptions.value = [];
orgFilteredOptions.value = [];
} finally {
orgLoading.value = false;
}
}
function handleOrgRemoteSearch(keyword) {
const key = (keyword || '').trim().toLowerCase();
if (!key) {
orgFilteredOptions.value = orgOptions.value.slice(0, 200);
return;
}
orgFilteredOptions.value = orgOptions.value
.filter((o) => (o.label || '').toLowerCase().includes(key))
.slice(0, 200);
}
// 动态可用的检查方法(根据已选部位所属的检查类型进行过滤)
const normalizeTypeValue = value => String(value ?? '').trim().toLowerCase();
const availableMethods = computed(() => {
// 获取当前已选部位的检查类型(可取第一个选中的部位的 checkType
const currentType = form.examTypeCode || (selectedItems.value.length > 0 ? selectedItems.value[0].checkType : '');
const normalizedCurrentType = normalizeTypeValue(currentType);
if (normalizedCurrentType) {
// 兼容脏数据method 的类型可能落在 checkType/type/typeCode/code/typeName/categoryName 中
const filtered = allMethods.value.filter(m => {
const typeCandidates = [
m.checkType,
m.type,
m.typeCode,
m.code,
m.typeName,
m.categoryName
].map(normalizeTypeValue).filter(Boolean);
return typeCandidates.includes(normalizedCurrentType);
});
return filtered.length > 0 ? filtered : allMethods.value;
}
return allMethods.value;
});
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
watch(availableMethods, (newMethods) => {
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
form.inspectionMethod = '';
}
});
/**
* 加载检查类型(分类)和检查项目(部位/项目),按类型分组展示
*/
async function loadCategoryList() {
dictLoading.value = true;
try {
// 1. 加载检查类型(分类名称),只取父级
const typeRes = await request({
url: '/system/check-type/list',
method: 'get',
params: { pageNo: 1, pageSize: 500 } // 取全量分类数据
});
let types = [];
if (typeRes && typeRes.data) {
if (Array.isArray(typeRes.data)) {
types = typeRes.data;
} else if (typeRes.data.records) {
types = typeRes.data.records;
} else if (typeRes.data.data && Array.isArray(typeRes.data.data)) {
types = typeRes.data.data;
}
} else if (Array.isArray(typeRes)) {
types = typeRes;
} else if (typeRes && typeRes.rows) {
types = typeRes.rows;
}
// 2. 加载检查项目(检查部位项目)
const partRes = await request({ url: '/check/part/list', method: 'get' });
let parts = [];
if (partRes && partRes.data) {
if (Array.isArray(partRes.data)) {
parts = partRes.data;
} else if (partRes.data.records) {
parts = partRes.data.records;
} else if (partRes.data.data && Array.isArray(partRes.data.data)) {
parts = partRes.data.data;
}
} else if (Array.isArray(partRes)) {
parts = partRes;
} else if (partRes && partRes.rows) {
parts = partRes.rows;
}
// 3. 按 checkType 归类
const dict = [];
for (const t of types) {
dict.push({
typeId: t.id,
typeCode: t.code, // 保存 code 用于后备匹配
orgType: t.type, // 保存 type 用于后备匹配
typeName: t.name, // 保存 name
categoryName: t.name,
// “检查类型管理”里配置的执行科室(图三)
performDeptName: t.department || '',
items: []
});
}
const unclassified = [];
for (const p of parts) {
const mapped = {
id: p.id,
name: p.name,
price: p.price || 0,
serviceFee: p.serviceFee || 0,
unit: '次',
checkType: p.checkType || '',
nationalCode: p.nationalCode || '',
packageName: p.packageName || '',
checked: false
};
// 增强匹配逻辑:部位的 checkType (如 'ECG', 'CT') 优先去匹配大类的 orgType
// 如果大类的 type 字段脏了(比如填了中文),则尝试匹配 code甚至是分类名称
const target = dict.find(d =>
d.orgType === p.checkType ||
d.typeCode === p.checkType ||
d.typeName === p.checkType
);
if (target) target.items.push(mapped);
else unclassified.push(mapped);
}
if (unclassified.length > 0) {
dict.push({ typeId: 'uncls', typeCode: '', categoryName: '其他', items: unclassified });
}
categoryList.value = dict.filter(d => d.items.length > 0);
// 默认展开第一个
if (categoryList.value.length > 0) {
activeNames.value = [categoryList.value[0].typeId];
}
} catch (err) {
console.error('加载检查项目分类失败', err);
} finally {
dictLoading.value = false;
}
}
/** 关键词过滤后的分类列表 */
const filteredCategoryList = computed(() => {
if (!dictSearchKey.value) return categoryList.value;
const key = dictSearchKey.value.toLowerCase();
return categoryList.value.map(cat => ({
...cat,
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key))
})).filter(cat => cat.items.length > 0);
});
// ====== 合计 ======
// Bug #384修复: 如果选中了检查方法,使用套餐价格;否则使用部位价格
const totalAmountCalc = computed(() => {
const total = selectedItems.value.reduce((sum, item) => {
const effectivePrice = item.selectedMethod?.packagePrice || item.price;
return sum + (effectivePrice * (item.quantity || 1));
}, 0);
return total.toFixed(2);
});
// 监听已选项:自动更新申检部位
watch(selectedItems, () => {
form.inspectionArea = selectedItems.value.map(i => i.name).join('+');
form.isCharged = selectedItems.value.length > 0 ? 1 : 0;
}, { deep: true });
// 监听患者变化
watch(() => props.patientInfo, (newVal) => {
if (newVal?.encounterId) {
initPatientForm(newVal);
getList();
}
}, { immediate: true, deep: true });
watch(() => props.activeTab, async (val) => {
if (val === 'examination') {
getList();
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
if (props.patientInfo?.encounterId) {
await loadClinicalDiag();
}
}
});
function initPatientForm(patient) {
form.patientName = patient.patientName || '';
// 就诊卡号应取值于 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;
request({
url: '/exam/apply/list',
method: 'get',
// 默认只展示本次就诊encounterId产生的检查申请单
params: { encounterId: props.patientInfo?.encounterId || '' }
}).then(res => {
applicationList.value = res.rows || res.data || [];
}).catch(err => console.error('获取申请单列表失败', err))
.finally(() => { loading.value = false; });
}
function handleAdd() {
formRef.value?.resetFields();
Object.assign(form, {
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',
natureofCost: '自费医疗',
clinicDesc: '', contraindication: '', medicalHistorySummary: '',
purposeofInspection: '', inspectionArea: '', inspectionMethod: '',
applyRemark: '', clinicalDiag: '', purposeDesc: '',
isUrgent: 0, pregnancyState: 0, allergyDesc: '',
applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0,
examTypeCode: '',
selectedMethodDisplay: '' // Bug #384修复: 重置检查方法显示
});
selectedItems.value = [];
resetCategoryChecked();
activeDetailTab.value = 'applyForm';
// 自动加载临床诊断
loadClinicalDiag();
}
function handleSave() {
formRef.value.validate(valid => {
if (!valid) return;
if (selectedItems.value.length === 0) {
ElMessage.warning('请至少选择一个检查明细项目');
return;
}
// 从已选项目推导检查类型编码(取第一个项目的 checkType如 CT / ECG / GI
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,
patientIdNum: props.patientInfo?.patientId || null,
items: selectedItems.value.map((item, index) => ({
itemCode: String(item.id),
itemName: item.name,
bodyPartCode: item.checkType || 'unknown',
// Bug #384修复: 如果选中了检查方法且有套餐价格,使用套餐价格;否则使用部位价格
itemFee: item.selectedMethod?.packagePrice || item.price,
performDeptCode: form.performDeptCode || '',
itemStatus: 0,
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({
url: '/exam/apply',
method: payload.applyNo ? 'put' : 'post',
data: payload
}).then(res => {
ElMessage.success('保存成功');
getList();
if (res.data) form.applyNo = res.data;
// 通知父组件刷新医嘱列表
emit('saved');
});
});
}
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(async res => {
const d = res.data || res;
if (d.data) Object.assign(form, d.data);
if (d.items && Array.isArray(d.items)) {
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();
// 修复【#408】加载申请单详情后自动切换到检查明细页签确保已加载的明细数据可见
activeDetailTab.value = 'applyDetail';
} catch (err) {
console.error('加载申请单详情失败', err);
ElMessage.error('加载申请单详情失败');
}
}
}).catch(err => {
console.error('获取申请单详情失败', err);
ElMessage.error('获取申请单详情失败');
});
}
function handlePrint(row) { ElMessage.info('打印申请单:' + row.applyNo); }
function handleDelete(row) {
ElMessageBox.confirm('确认删除该检查申请单吗?', '警告', { type: 'warning' }).then(() => {
request({ url: `/exam/apply/${row.applyNo}`, method: 'delete' }).then(() => {
ElMessage.success('删除成功');
getList();
if (form.applyNo === row.applyNo) handleAdd();
});
});
}
// ====== 勾选逻辑 ======
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 || '';
if (currentCategory !== newCategory) {
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
item.checked = false;
return;
}
}
selectedItems.value.push({
id: item.id, name: item.name,
price: item.price, quantity: 1,
serviceFee: item.serviceFee || 0,
unit: item.unit || '次',
applyPart: item.name,
checkType: effectiveCheckType, // Bug #384修复: 使用有效的 checkType
nationalCode: item.nationalCode || '',
checked: true,
methods: methods,
selectedMethod: null,
expanded: false // Bug #384修复: 新增展开状态,默认不展开
});
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
if (selectedItems.value.length === 1 && cat?.performDeptName) {
form.performDeptCode = cat.performDeptName;
} 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);
if (selectedItems.value.length === 0) {
form.performDeptCode = '';
form.examTypeCode = '';
}
}
// 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) {
selectedItems.value.splice(idx, 1);
// 取消对应 category 中的 checkbox
for (const cat of categoryList.value) {
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();
}
}
function resetCategoryChecked() {
for (const cat of categoryList.value)
for (const item of cat.items) item.checked = false;
}
function syncCategoryChecked() {
resetCategoryChecked();
const ids = new Set(selectedItems.value.map(s => s.id));
for (const cat of categoryList.value)
for (const item of cat.items)
if (ids.has(item.id)) item.checked = true;
}
defineExpose({ getList });
</script>
<style scoped>
.exam-app-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 8px;
height: 100%;
background: #f0f2f5;
}
/* 顶部申请单列表 */
.top-section {
background: #fff;
border-radius: 4px;
padding: 10px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
/* 底部区域:左表单 + 右分类 */
.bottom-section {
display: flex;
gap: 10px;
flex: 1;
min-height: 0;
}
/* 左:表单面板 */
.form-panel {
flex: 1;
background: #fff;
border-radius: 4px;
padding: 10px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
overflow-y: auto;
min-width: 0;
}
.form-tabs :deep(.el-tabs__header) {
margin-bottom: 10px;
}
.apply-form :deep(.el-form-item) {
margin-bottom: 10px;
}
.apply-form :deep(.el-form-item__label) {
font-size: 12px;
}
.total-row {
margin-top: 8px;
text-align: right;
font-size: 13px;
color: #606266;
}
.total-amount {
font-size: 15px;
font-weight: bold;
color: #f56c6c;
margin-left: 4px;
}
/* 右:分类面板 */
.category-panel {
width: 380px;
flex-shrink: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
padding: 10px 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-top {
display: flex;
gap: 12px;
flex: 1;
min-height: 0;
}
.category-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.panel-label {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.search-input {
margin-bottom: 8px;
}
.collapse-scroll {
flex: 1;
overflow-y: auto;
}
.empty-hint {
color: #909399;
font-size: 12px;
text-align: center;
padding: 20px 0;
}
/* 检查项目分类折叠 */
.cat-title {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 4px;
}
.item-row:hover {
background: #f5f7fa;
}
.item-checkbox {
flex: 1;
overflow: hidden;
}
.item-checkbox :deep(.el-checkbox__label) {
font-size: 12px;
color: #303133;
}
.item-price {
font-size: 12px;
color: #1890FF;
flex-shrink: 0;
margin-left: 6px;
}
/* 已选择 tags */
.selected-panel {
width: 140px; /* Bug #384修复: 加宽以适应展开内容 */
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.selected-tags {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.selected-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-selected {
color: #c0c4cc;
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;
}
:deep(.el-collapse-item__header) {
font-size: 13px;
padding: 6px 0;
height: auto;
line-height: 1.5;
}
:deep(.el-collapse-item__content) {
padding-bottom: 4px;
}
:deep(.el-collapse-item__wrap) {
border: none;
}
</style>