Files
his/openhis-ui-vue3/src/views/doctorstation/components/examination/examinationApplication.vue
wangjian963 0aa7dd9b82 Revert "Merge remote-tracking branch 'origin/develop' into develop"
This reverts commit 5946c1ea4b, reversing
changes made to 8d905c9844.
2026-05-15 09:33:35 +08:00

2138 lines
72 KiB
Vue
Executable File
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" style="width: 100%; max-width: 1200px;">
<!-- ====== 顶部卡片申请单列表 ====== -->
<div class="top-section">
<div class="section-header">
<span class="section-title">检查项目 ({{ filteredApplicationList.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>
<!-- Bug #499: 查询过滤工具栏 -->
<div class="search-toolbar">
<el-form :inline="true" size="small">
<el-form-item label="日期范围">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable style="width: 140px">
<el-option
v-for="opt in statusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关键字">
<el-input
v-model="searchForm.keyword"
placeholder="申请单号 / 检查项目"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" icon="Search">搜索</el-button>
<el-button @click="handleResetSearch" icon="Refresh">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table
v-loading="loading"
:data="filteredApplicationList"
: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="160">
<template #default="scope">
<el-select
v-if="scope.row.methods && scope.row.methods.length > 1"
:model-value="scope.row.selectedMethod"
value-key="id"
size="small"
style="width: 100%"
placeholder="选择方法"
@update:model-value="(val) => onDetailMethodChange(scope.row, val)"
>
<el-option
v-for="meth in scope.row.methods"
:key="meth.id"
:label="`${meth.name}${meth.packagePrice != null ? ' ¥' + formatDetailAmount(meth.packagePrice) : ''}`"
:value="meth"
/>
</el-select>
<template v-else>
<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>
</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"
accordion
@change="handleCollapseChange"
>
<el-collapse-item
v-for="cat in filteredCategoryList"
:key="cat.typeId"
:name="cat.typeId"
>
<template #title>
<span class="cat-title">{{ cat.categoryName }}</span>
<span v-if="categoryLoadingSet.has(cat.typeId)" class="loading-dot"></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>
<div v-if="categoryLoadingSet.has(cat.typeId)" class="category-loading-hint">
加载中...
</div>
<!-- Bug #428修复: 渲染分类联动加载的检查方法列表 -->
<!-- Bug #500修复: v-if 改为 v-show避免方法列表加载时 DOM 突然插入导致高度跳变 -->
<div
v-show="cat.methods && cat.methods.length > 0"
class="method-section"
>
<div class="method-section-title">检查方法</div>
<div
v-for="method in cat.methods"
:key="method.id"
class="method-row"
>
<el-checkbox
:model-value="isMethodSelected(method, cat)"
@change="(val) => handleMethodSelect(val, method, cat)"
class="method-checkbox"
>
{{ method.name }}
</el-checkbox>
<span class="method-price-tag">¥{{ method.packagePrice || method.price || 0 }}</span>
</div>
</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"
:class="{ 'is-expanded': item.expanded }"
>
<!-- Bug #384修复 + #426修复: 项目卡片头部,可展开/收起 -->
<div class="card-header" @click="toggleItemExpand(item)">
<el-tag v-if="item.isPackage || item.packageName" size="small" type="warning" style="margin-right: 4px; flex-shrink: 0;">套餐</el-tag>
<el-tooltip :content="item.name" placement="top" :show-after="400">
<span class="card-name">{{ item.name }}</span>
</el-tooltip>
<span class="card-price">¥{{ formatDetailAmount(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 #428: 有套餐 ID 时默认展开;加载中/空/明细均在本区域展示 -->
<div v-if="item.expanded && shouldShowPackageBody(item)" class="selected-card-body">
<div v-if="item.packageDetailsLoading" class="package-details-loading">加载中...</div>
<template v-else>
<div v-if="getPackageDetailsList(item).length === 0" class="package-details-empty">
暂无套餐明细
</div>
<div v-else class="package-details-list">
<div class="package-details-head">套餐明细</div>
<div
v-for="(detail, dIdx) in getPackageDetailsList(item)"
:key="detail.id ?? detail.itemCode ?? `d-${dIdx}`"
class="detail-row"
>
<el-tooltip :content="detail.name" placement="top" :show-after="500">
<span class="detail-name">{{ detail.name }}</span>
</el-tooltip>
<div class="detail-meta">
<span class="detail-qty">×{{ detail.quantity || 1 }}</span>
<span class="detail-price">¥{{ formatDetailAmount(detail.price) }}</span>
</div>
</div>
</div>
</template>
</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, listCheckPackage } 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([]);
// Bug #499: 查询过滤状态
const searchForm = reactive({
dateRange: [],
applyStatus: '',
keyword: ''
});
// 申请单状态选项
const statusOptions = [
{ label: '已开单', value: 0 },
{ label: '已收费', value: 1 },
{ label: '已预约', value: 2 },
{ label: '已签到', value: 3 },
{ label: '部分报告', value: 4 },
{ label: '已完告', value: 5 },
{ label: '已作废', value: 6 }
];
// Bug #499: 过滤后的申请单列表
const filteredApplicationList = computed(() => {
let result = applicationList.value;
// 日期范围过滤
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
const start = searchForm.dateRange[0];
const end = searchForm.dateRange[1];
result = result.filter(item => {
const d = item.applyTime;
if (!d) return false;
const dateStr = d.length > 10 ? d.substring(0, 10) : d;
return dateStr >= start && dateStr <= end;
});
}
// 状态过滤
if (searchForm.applyStatus !== '' && searchForm.applyStatus !== null && searchForm.applyStatus !== undefined) {
result = result.filter(item => item.applyStatus === searchForm.applyStatus);
}
// 关键字过滤(申请单号、申检部位、检查项目名)
if (searchForm.keyword) {
const kw = searchForm.keyword.toLowerCase();
result = result.filter(item => {
return (item.applyNo || '').toLowerCase().includes(kw)
|| (item.inspectionArea || '').toLowerCase().includes(kw);
});
}
return result;
});
// Bug #499: 搜索与重置
function handleSearch() {
// 过滤逻辑由 computed 自动处理
}
function handleResetSearch() {
const now = new Date();
const end = now.toISOString().substring(0, 10);
const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().substring(0, 10);
searchForm.dateRange = [start, end];
searchForm.applyStatus = '';
searchForm.keyword = '';
}
// 初始化默认日期范围为近一周
handleResetSearch();
// 🔧 BugFix#426: 懒加载套餐明细
async function loadPackageDetails(row, treeNode, resolve) {
if (!row.packageId) {
resolve([]);
return;
}
try {
const res = await request({
url: `/system/check-type/package/${row.packageId}/details`,
method: 'get'
});
if (res.code === 200) {
const list = parsePackageDetailsPayload(res);
const children = list.map((child) => ({
...child,
name: child.name || child.itemName,
unit: child.unit || '次',
price: child.price ?? child.unitPrice ?? child.itemPrice ?? 0,
quantity: row.quantity || 1,
isPackageDetail: true
}));
resolve(children);
} else {
resolve([]);
}
} catch (err) {
console.error('加载套餐明细失败:', err);
resolve([]);
}
}
// #428修复 + #426修复: 为已选择项目加载套餐明细通过packageId或packageName查询
/** 套餐明细挂在「部位」或已选的「检查方法」上(方法可带 packageId */
function getPackageCarrier(item) {
return item?.selectedMethod?.packageId ? item.selectedMethod : item;
}
function getPackageDetailsList(item) {
// 明细挂在行对象上,避免仅写入 methods 内嵌对象时首帧不触发视图更新(体感需点两次才展开)
if (Array.isArray(item?.packageDetailsDisplay)) {
return item.packageDetailsDisplay;
}
const carrier = getPackageCarrier(item);
return Array.isArray(carrier?.packageDetails) ? carrier.packageDetails : [];
}
/** 有套餐 ID 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
function shouldShowPackageBody(item) {
return !!getPackageCarrier(item)?.packageId;
}
/** 金额展示:统一两位小数 */
function formatDetailAmount(value) {
const n = Number(value ?? 0);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
}
/** 默认检查方法:优先与部位 packageId 一致的方法,否则取首个带套餐的方法,否则取第一个 */
function pickDefaultMethod(methods, partItem) {
if (!methods?.length) return null;
if (methods.length === 1) return methods[0];
const pid = partItem?.packageId ?? null;
if (pid != null && pid !== '') {
const matched = methods.find(
(x) => x.packageId != null && String(x.packageId) === String(pid)
);
if (matched) return matched;
}
const withPkg = methods.find((x) => x.packageId != null);
if (withPkg) return withPkg;
return methods[0];
}
function parsePackageDetailsPayload(res) {
const raw =
res?.data?.data ??
res?.data?.records ??
res?.data ??
res?.rows ??
res;
if (!Array.isArray(raw)) return [];
return raw;
}
// #428: 为已选择项目加载套餐明细后端CheckTypeController /system/check-type/package/{id}/details
async function loadPackageDetailsForItem(item) {
const carrier = getPackageCarrier(item);
let packageId = item.packageId || carrier?.packageId;
if (!packageId && !item.packageName) {
return;
}
item.packageDetailsLoading = true;
try {
if (!packageId && item.packageName) {
const pkgRes = await listCheckPackage({ packageName: item.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
item.packageDetails = [];
item.packageDetailsDisplay = [];
return;
}
packageId = packages[0].id;
item.packageId = packageId;
}
if (!packageId) {
item.packageDetails = [];
item.packageDetailsDisplay = [];
return;
}
const res = await request({
url: `/system/check-type/package/${packageId}/details`,
method: 'get'
});
const list = parsePackageDetailsPayload(res);
const mapped = list.map((detail) => ({
...detail,
name: detail.name || detail.itemName,
unit: detail.unit || '次',
price: detail.price ?? detail.unitPrice ?? detail.itemPrice ?? 0,
quantity: detail.quantity || 1
}));
item.packageDetailsDisplay = mapped;
carrier.packageDetails = mapped;
if (res.code === 200 && res.data) {
item.packageDetails = Array.isArray(res.data)
? res.data.map((detail) => ({
...detail,
name: detail.name || detail.itemName,
unit: detail.unit || '次',
price: detail.price || detail.unitPrice || 0,
quantity: detail.quantity || 1
}))
: mapped;
} else {
item.packageDetails = mapped;
}
} catch (err) {
console.error('加载套餐明细失败:', err);
item.packageDetailsDisplay = [];
carrier.packageDetails = [];
item.packageDetails = [];
} finally {
item.packageDetailsLoading = false;
}
}
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 categoryLoadingSet = ref(new Set()); // Bug #500: 正在加载方法的分类集合
const currentActiveCategory = ref(null); // Bug #500: 记录当前激活的分类,忽略过期请求响应
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;
});
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
// #428: 分类展开时联动加载检查方法
// Bug #500: 使用 categoryLoadingSet 替代 dictLoading避免切换分类时整个区域闪烁
// Bug #500: 添加 currentActiveCategory 守卫,忽略过期请求响应,防止快速切换时数据闪烁
async function handleCategoryExpand(cat) {
if (!cat || !cat.typeName) return;
if ((cat.methods && cat.methods.length > 0) || categoryLoadingSet.value.has(cat.typeId)) return;
categoryLoadingSet.value.add(cat.typeId);
currentActiveCategory.value = cat.typeId;
try {
const res = await searchCheckMethod({ checkType: cat.typeName });
// 忽略过期请求:用户已切换到其他分类,丢弃当前响应
if (currentActiveCategory.value !== cat.typeId) return;
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) && data.length > 0) {
cat.methods = data.map(m => ({
id: m.id,
name: m.name,
code: m.code,
price: m.price || 0,
packageName: m.packageName || '',
packageId: m.packageId || null,
packagePrice: m.packagePrice || null,
serviceFee: m.serviceFee || null
}));
}
} catch (err) {
if (currentActiveCategory.value !== cat.typeId) return;
console.error('加载分类检查方法失败', err);
} finally {
categoryLoadingSet.value.delete(cat.typeId);
}
}
// Bug #500修复: 不阻塞 accordion 状态更新,仅防止重复加载同一分类的方法
function handleCollapseChange(activeName) {
// 始终记录当前激活的分类,确保 handleCategoryExpand 能正确忽略过期请求
currentActiveCategory.value = activeName || null;
if (activeName) {
// Bug #428修复: 直接从 categoryList原始响应式数组查找分类
// 确保后续 handleCategoryExpand 对 cat.methods 的赋值能正确触发 Vue 响应式更新
const cat = categoryList.value.find(c => c.typeId == activeName);
if (cat && (!cat.methods || cat.methods.length === 0)) {
handleCategoryExpand(cat); // 异步加载,不 await
}
}
}
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: [],
methods: [] // #428修复: 初始化 methods 数组,确保 Vue 响应式追踪
});
}
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 || '',
packageId: p.packageId || null,
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;
}
// 检查每个项目是否已选择检查方法
const itemsWithoutMethod = selectedItems.value.filter(item => !item.selectedMethod);
if (itemsWithoutMethod.length > 0) {
const names = itemsWithoutMethod.map(item => item.name).join('、');
ElMessage.warning(`以下项目未选择检查方法:${names},请在右侧勾选后再保存`);
return;
}
// 从已选项目推导检查类型编码(取第一个项目的 checkType如 CT / ECG / GI
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
form.examTypeCode = firstCheckType;
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 => {
// 响应结构: Axios拦截器对code===200返回res.dataAjaxResult体
// 结构为 { code: 200, data: examApply实体, items: [明细数组] }
const items = Array.isArray(res.items) ? res.items : [];
const dataObj = res.data || {};
// 先填充表单字段
if (dataObj && typeof dataObj === 'object') Object.assign(form, dataObj);
if (items.length > 0) {
try {
// 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(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,
packageDetailsLoading: false,
isPackage: false,
packageId: null
};
// 加载该项目的检查方法
if (m.bodyPartCode) {
try {
const methodRes = await searchCheckMethod({ checkType: m.bodyPartCode });
// 正确解析 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,
packageName: md.packageName || '',
packageId: md.packageId || null,
packagePrice: md.packagePrice || null,
serviceFee: md.serviceFee || null
}));
// 回充已保存的检查方法
if (m.checkMethodId) {
item.selectedMethod = item.methods.find(md => String(md.id) === String(m.checkMethodId)) || null;
if (item.selectedMethod?.packageId) {
item.isPackage = true;
item.packageId = item.selectedMethod.packageId;
}
}
if (!item.selectedMethod && item.methods.length) {
item.selectedMethod = pickDefaultMethod(item.methods, { packageId: item.packageId });
}
if (item.selectedMethod?.packageId) {
item.packageId = item.selectedMethod.packageId;
item.isPackage = true;
}
}
} catch (err) {
console.error('加载检查方法失败', err);
}
}
return item;
}));
// Bug #408修复: 确保明细数据正确加载到selectedItems
selectedItems.value = itemsWithMethods;
// 加载套餐明细(单个失败不影响其他项目和明细显示)
for (const it of selectedItems.value) {
if (getPackageCarrier(it)?.packageId) {
try {
await loadPackageDetailsForItem(it);
} catch (e) {
console.error('加载套餐明细失败:', it.name, e);
}
}
it.expanded = !!getPackageCarrier(it)?.packageId;
}
syncCategoryChecked();
// Bug #384修复: 回充后更新检查方法显示
updateMethodDisplay();
// Bug #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();
});
});
}
// Bug #428修复: 判断某个检查方法是否已被选中(任意项目关联了该方法)
function isMethodSelected(method, cat) {
return selectedItems.value.some(item =>
item.selectedMethod?.id === method.id && item.checkType === cat.typeName
);
}
// Bug #428修复: 勾选检查方法
async function handleMethodSelect(checked, method, cat) {
if (checked) {
// 找到该方法所属的第一个检查项目
const targetItem = cat.items[0];
if (!targetItem) {
// 如果分类下没有项目,尝试从其他分类找同名项目或创建
console.warn('分类下没有检查项目,无法关联方法');
return;
}
// 如果该项目已存在,只更新 selectedMethod
const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
if (existingItem) {
existingItem.selectedMethod = method;
// 从方法中获取套餐信息
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId;
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
// 预加载套餐明细
loadPackageDetailsForItem(existingItem);
}
updateMethodDisplay();
return;
}
// 如果该项目不存在,创建一个并关联方法
if (selectedItems.value.length > 0) {
const currentCategory = selectedItems.value[0].checkType;
// Bug #428修复: 使用 cat.typeName 进行比较(与 newItem.checkType 保持一致)
const newCategory = cat.typeName || '';
if (currentCategory !== newCategory) {
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
return;
}
}
const newItem = {
id: targetItem.id, name: targetItem.name,
price: targetItem.price, quantity: 1,
serviceFee: targetItem.serviceFee || 0,
unit: targetItem.unit || '次',
applyPart: targetItem.name,
checkType: cat.typeName,
nationalCode: targetItem.nationalCode || '',
checked: true,
methods: cat.methods || [method], // #428修复: 复制分类下全部方法,允许用户切换
selectedMethod: method,
expanded: false,
// 从方法或项目中获取套餐信息
isPackage: !!method.packageId || !!targetItem.packageName,
packageId: method.packageId || targetItem.packageId || null,
packageName: method.packageName || targetItem.packageName || null // #428修复: 复制 packageName确保套餐明细可加载
};
selectedItems.value.push(newItem);
// 如果是套餐,预加载套餐明细
if (newItem.isPackage && newItem.packageId) {
loadPackageDetailsForItem(newItem);
}
// 自动回填执行科室
if (selectedItems.value.length === 1 && cat?.performDeptName) {
form.performDeptCode = cat.performDeptName;
}
// 同时勾选左侧项目的 checkbox
targetItem.checked = true;
} else {
// 取消选择方法:将 selectedItems 中关联该方法的项的 selectedMethod 清空
const itemsWithMethod = selectedItems.value.filter(
item => item.selectedMethod?.id === method.id
);
for (const item of itemsWithMethod) {
item.selectedMethod = null;
}
}
updateMethodDisplay();
}
// ====== 勾选逻辑 ======
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 || '',
packageId: m.packageId || null,
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;
// Bug #428修复: 使用 cat.typeName 进行比较(与 effectiveCheckType 保持一致)
const newCategory = cat.typeName || '';
if (currentCategory !== newCategory) {
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
item.checked = false;
return;
}
}
const newRow = {
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,
isPackage: !!(item.packageId || item.packageName),
packageName: item.packageName || null,
packageDetailsLoading: false,
packageId: item.packageId || null
};
selectedItems.value.push(newRow);
// 必须用数组里的响应式行,不能继续改局部 newRowpush 后列表内是 proxy改 raw 对象不会触发右侧卡片更新(会一直卡在「加载中」)
const row = selectedItems.value[selectedItems.value.length - 1];
// 右侧不再展示「检查方法」列表:自动选默认方法(保存、计价仍依赖 selectedMethod
if (methods.length >= 1) {
row.selectedMethod = pickDefaultMethod(methods, item);
}
updateMethodDisplay();
// 有套餐 ID 时默认展开(先显示加载区,明细写入行对象 packageDetailsDisplay
row.expanded = !!getPackageCarrier(row)?.packageId;
if (getPackageCarrier(row)?.packageId) {
await loadPackageDetailsForItem(row);
}
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
if (selectedItems.value.length === 1 && cat?.performDeptName) {
form.performDeptCode = cat.performDeptName;
} else if (!form.performDeptCode && cat?.performDeptName) {
form.performDeptCode = cat.performDeptName;
}
} 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修复 + #426修复: 展开/收起项目卡片
async function toggleItemExpand(item) {
item.expanded = !item.expanded;
if (item.expanded && (item.isPackage || item.packageName) && (!item.packageDetails || item.packageDetails.length === 0) && !item.packageDetailsLoading) {
await loadPackageDetailsForItem(item);
}
if (item.expanded && shouldShowPackageBody(item)) {
if (getPackageDetailsList(item).length === 0 && !item.packageDetailsLoading) {
await loadPackageDetailsForItem(item);
}
}
}
// Bug #384修复: 勾选框选择检查方法(单选逻辑)
async function selectMethodCheckbox(checked, item, method) {
if (checked) {
item.selectedMethod = method;
if (item.expanded && method.packageId) {
loadPackageDetailsForItem(item);
}
// 动态加载该方法对应的套餐明细
await loadMethodPackageDetails(item, method);
} else {
item.selectedMethod = null;
item.methodPackageDetails = [];
}
// 联动更新表单检查方法显示字段
updateMethodDisplay();
// #430: 套餐金额实时同步到申请单
nextTick(() => {
form.totalAmount = totalAmountCalc.value;
});
}
// 根据检查方法的packageName加载对应的套餐明细
async function loadMethodPackageDetails(item, method) {
item.methodPackageLoading = true;
item.methodPackageDetails = [];
try {
if (!method.packageName) {
item.methodPackageLoading = false;
return;
}
// 通过packageName查询套餐获取packageId
const pkgRes = await listCheckPackage({ packageName: method.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
item.methodPackageLoading = false;
return;
}
const packageId = packages[0].id;
// 查询套餐明细
const detailRes = await request({
url: `/system/package/${packageId}/details`,
method: 'get'
});
if (detailRes.code === 200 && detailRes.data) {
item.methodPackageDetails = detailRes.data.map(d => ({
id: d.id,
name: d.itemName || d.name,
quantity: d.quantity || 1,
unit: d.unit || '次',
price: d.unitPrice || d.price || 0,
amount: d.amount || d.total || 0,
checked: true // 默认勾选
}));
}
} catch (err) {
console.error('加载方法套餐明细失败:', err);
item.methodPackageDetails = [];
} finally {
item.methodPackageLoading = false;
}
}
/** 检查明细表格中切换检查方法 */
async function onDetailMethodChange(row, val) {
row.selectedMethod = val || null;
if (val?.packageId) {
row.packageId = val.packageId;
row.isPackage = true;
}
row.packageDetailsDisplay = undefined;
const carrier = getPackageCarrier(row);
if (carrier) {
carrier.packageDetails = undefined;
}
updateMethodDisplay();
row.expanded = !!getPackageCarrier(row)?.packageId;
if (getPackageCarrier(row)?.packageId) {
await loadPackageDetailsForItem(row);
}
nextTick(() => {
form.totalAmount = totalAmountCalc.value;
});
}
// 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;
}
/* Bug #499: 查询过滤工具栏 */
.search-toolbar {
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
}
.search-toolbar :deep(.el-form-item) {
margin-bottom: 8px;
}
.search-toolbar :deep(.el-form-item__label) {
font-size: 12px;
}
/* 底部区域:左表单 + 右分类 */
.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: 420px;
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;
overflow-x: hidden; /* Bug #500: 防止切换时水平方向溢出导致抖动 */
min-height: 120px; /* Bug #500: 固定最小高度,避免分类切换时 flex 容器高度突变 */
}
.empty-hint {
color: #909399;
font-size: 12px;
text-align: center;
padding: 20px 0;
}
/* 检查项目分类折叠 */
.cat-title {
font-size: 13px;
font-weight: 500;
color: #303133;
}
/* Bug #500: 分类加载中的小圆点动画 */
.loading-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #409eff;
margin-left: 6px;
animation: dotPulse 1s ease-in-out infinite;
}
@keyframes dotPulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.item-row {
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;
}
/* Bug #428修复: 分类下检查方法区域样式 */
.method-section {
padding: 6px 8px;
background: #f0f7ff;
border-radius: 4px;
margin-top: 6px;
}
.method-section-title {
font-size: 12px;
font-weight: 600;
color: #409eff;
margin-bottom: 4px;
padding-bottom: 3px;
border-bottom: 1px dashed #d9ecff;
}
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 4px;
border-radius: 3px;
}
.method-row:hover {
background: #e8f4ff;
}
.method-checkbox {
flex: 1;
overflow: hidden;
}
.method-checkbox :deep(.el-checkbox__label) {
font-size: 12px;
color: #303133;
}
.method-price-tag {
font-size: 11px;
color: #e6a23c;
font-weight: 500;
flex-shrink: 0;
margin-left: 6px;
}
/* 已选择 tags */
/* 已选择:加宽,避免套餐明细挤成一团 */
.selected-panel {
width: 220px;
min-width: 200px;
max-width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.selected-tags {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 2px;
}
.selected-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-selected {
color: #c0c4cc;
font-size: 12px;
}
/* 已选择项目卡片 */
.selected-item-card {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 6px;
border: 1px solid #e4e7ed;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.selected-item-card .card-header {
display: flex;
align-items: center;
padding: 10px 10px;
cursor: pointer;
gap: 8px;
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
border-bottom: 1px solid transparent;
}
.selected-item-card .card-header:hover {
background: linear-gradient(180deg, #ecf5ff 0%, #e3eef8 100%);
}
.selected-item-card.is-expanded .card-header {
border-bottom-color: #ebeef5;
}
.card-name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-price {
font-size: 13px;
color: #409eff;
font-weight: 600;
flex-shrink: 0;
}
.expand-icon {
font-size: 14px;
color: #909399;
transition: transform 0.2s ease;
flex-shrink: 0;
transition: transform 0.2s;
transform: rotate(0deg);
}
.expand-icon.expanded {
transform: rotate(90deg);
}
/* Bug #428修复: 套餐明细列表样式 */
.package-details-list {
padding: 6px 10px;
background: #fffbe6;
border-top: 1px solid #ffe58f;
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
background: #fff;
}
.detail-row:hover {
background: #fff9e6;
}
.detail-name {
color: #303133;
font-weight: 500;
}
.detail-info {
color: #909399;
font-size: 10px;
white-space: nowrap;
}
/* 展开区域 */
.selected-card-body {
background: #fafbfc;
}
.package-details-loading,
.package-details-empty {
padding: 12px 10px;
font-size: 12px;
color: #909399;
text-align: center;
}
.package-details-empty {
color: #c0c4cc;
}
.package-details-list {
padding: 10px 10px 12px;
}
.package-details-head {
font-size: 11px;
font-weight: 600;
color: #909399;
letter-spacing: 0.02em;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px dashed #dcdfe6;
}
.detail-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px 12px;
align-items: start;
padding: 10px 0;
border-bottom: 1px solid #ebeef5;
}
.detail-row:last-of-type {
border-bottom: none;
padding-bottom: 2px;
}
.detail-name {
font-size: 12px;
color: #303133;
line-height: 1.5;
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.detail-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
text-align: right;
}
.detail-qty {
font-size: 11px;
color: #909399;
font-variant-numeric: tabular-nums;
}
.detail-price {
font-size: 12px;
font-weight: 600;
color: #e6a23c;
font-variant-numeric: tabular-nums;
}
/* 折叠组件细节 */
:deep(.el-collapse) {
border: none;
}
:deep(.el-collapse-item__header) {
font-size: 13px;
padding: 6px 0;
height: auto;
line-height: 1.5;
}
/* Bug #500修复: 折叠内容使用明确属性过渡,避免 transition: all 导致子元素意外动画 */
:deep(.el-collapse-item__content) {
padding-bottom: 4px;
transition: height 0.3s ease, max-height 0.3s ease;
}
/* Bug #500: 折叠面板动画容器,添加 overflow:hidden 防止展开时内容溢出导致闪烁 */
:deep(.el-collapse-item__wrap) {
border: none;
overflow: hidden;
}
:deep(.el-collapse-item) {
transition: margin 0.2s ease;
}
/* Bug #500: 分类加载中提示样式 */
.category-loading-hint {
color: #909399;
font-size: 12px;
text-align: center;
padding: 8px 0;
}
</style>