Files
his/openhis-ui-vue3/src/views/doctorstation/components/examination/examinationApplication.vue
chenqi a9ed53a949 refactor(examination): 优化检查申请界面结构和数据传输对象
- 移除检查项目套餐明细的冗余代码块
- 修复检查方法套餐明细显示逻辑中的重复条件判断
- 修正界面组件结构层级以改善渲染性能
- 更新仪器管理初始化数据传输对象的注解配置
- 替换 Lombok 注解从 @Data 为 @Getter/@Setter
- 修复数据库映射文件中字段定义的语法错误
- 统一 SQL 查询语句的格式化风格
2026-05-29 11:40:18 +08:00

2928 lines
89 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"
icon="Plus"
@click="handleAdd"
>
新增
</el-button>
<el-button
type="success"
icon="Finished"
@click="handleSave"
>
保存
</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"
icon="Search"
@click="handleSearch"
>
搜索
</el-button>
<el-button
icon="Refresh"
@click="handleResetSearch"
>
重置
</el-button>
</el-form-item>
</el-form>
</div>
<el-table
v-loading="loading"
:data="filteredApplicationList"
:max-height="200"
highlight-current-row
border
size="small"
:header-cell-style="{ background: '#f5f5f5', color: '#303133', fontWeight: '600' }"
@row-click="handleRowClick"
>
<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
title="打印"
@click.stop="handlePrint(row)"
>
<el-icon><Printer /></el-icon>
</el-button>
<el-button
link
type="danger"
title="删除"
@click.stop="handleDelete(row)"
>
<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">
{{ formatDetailAmount(getSelectedItemAmount(scope.row)) }}
</template>
</el-table-column>
<!-- Bug #384修复: 金额使用有效价格计算 -->
<el-table-column
label="金额"
width="80"
align="right"
>
<template #default="scope">
{{ formatDetailAmount(getSelectedItemAmount(scope.row) * (scope.row.quantity || 1)) }}
</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
v-loading="dictLoading"
class="collapse-scroll"
>
<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"
/>
</template>
<!-- 检查项目(部位/项目列表) -->
<div
v-for="item in cat.items"
:key="item.id"
class="item-row"
>
<el-checkbox
v-model="item.checked"
class="item-checkbox"
@change="(val) => handleItemSelect(val, item, cat)"
>
{{ 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>
</el-collapse-item>
</el-collapse>
</div>
</div>
<div class="right-column">
<!-- 右侧:已选择(检查项目、检查方法为两类独立选择结果) -->
<div class="selected-panel">
<div class="panel-label">已选择:</div>
<div class="selected-tags">
<template v-if="selectedItems.length === 0 && selectedMethods.length === 0">
<div class="empty-selected"></div>
</template>
<template v-else>
<div
v-for="(item, idx) in selectedItems"
:key="'project-' + item.id"
class="selected-item-card"
:class="{ 'is-expanded': item.projectFoldExpanded }"
>
<div
class="fold-strip fold-strip-project"
:class="{ 'is-open': item.projectFoldExpanded }"
>
<div class="fold-strip-header" :class="{ 'no-chevron': !hasItemPackage(item) }" @click="hasItemPackage(item) && toggleProjectFold(item)">
<el-icon v-if="hasItemPackage(item)" :class="['fold-chevron', { open: item.projectFoldExpanded }]">
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查项目</span>
<el-tooltip :content="getDisplayItemName(item)" placement="top" :show-after="400">
<span class="fold-title line-clamp-2">{{ getDisplayItemName(item) }}</span>
</el-tooltip>
</div>
<span class="fold-price-strong">¥{{ formatDetailAmount(item.price || 0) }}</span>
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 仅当项目有套餐时展示明细区域,普通项目无明细可展示 -->
<div
v-if="hasItemPackage(item) && item.projectFoldExpanded"
class="fold-strip-body"
>
<div class="fold-package-wrap">
<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
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
v-for="(method, idx) in selectedMethods"
:key="'method-' + method.id"
class="selected-item-card"
:class="{ 'is-expanded': method.expanded }"
>
<div
class="fold-strip fold-strip-method"
:class="{ 'is-open': method.expanded }"
>
<div class="fold-strip-header" :class="{ 'no-chevron': !hasStandaloneMethodPackage(method) }" @click="hasStandaloneMethodPackage(method) && toggleSelectedMethodFold(method)">
<el-icon v-if="hasStandaloneMethodPackage(method)" :class="['fold-chevron', { open: method.expanded }]">
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查方法</span>
<span
class="fold-title fold-title-plain line-clamp-2"
:title="getDisplayMethodName(method)"
>
{{ getDisplayMethodName(method) }}
</span>
</div>
<span
v-if="hasStandaloneMethodPackage(method)"
class="fold-price-strong warn"
>
¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}
</span>
<el-button link type="danger" size="small" @click.stop="handleRemoveMethod(idx)">
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 仅当检查方法有套餐时展示明细 -->
<div v-if="hasStandaloneMethodPackage(method) && method.expanded" class="fold-strip-body">
<div class="fold-package-wrap fold-method-package-wrap">
<div v-if="method.packageLoading" class="package-details-loading">加载中...</div>
<template v-else>
<div v-if="getStandaloneMethodPackageDetailsList(method).length === 0" class="package-details-empty">
暂无检查方法套餐明细
</div>
<div
v-else
class="package-details-list method-package-list"
>
<div
v-for="(detail, dIdx) in getStandaloneMethodPackageDetailsList(method)"
:key="detail.id ?? detail.itemCode ?? `md-${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>
</template>
</div>
</div>
</div>
</div>
<!-- 独立检查方法勾选区:与"已选择"区域解耦,支持分别手动勾选 -->
<div class="method-picker-section">
<div
v-if="methodsForActiveCategory.length > 0"
class="selected-global-method-picker"
@click.stop
>
<div class="method-picker-collapse-title" @click="methodPickerExpanded = !methodPickerExpanded">
<span class="method-picker-title-main">检查方法</span>
<span v-if="activeCategoryName" class="global-method-picker-scope">{{ activeCategoryName }}</span>
<el-icon :class="['method-picker-arrow', { expanded: methodPickerExpanded }]">
<ArrowDown />
</el-icon>
</div>
<div v-show="methodPickerExpanded" class="global-method-picker-list">
<div
v-for="method in methodsForActiveCategory"
:key="'g-m-' + method.id"
class="item-row method-picker-row"
>
<el-checkbox
:model-value="isStandaloneMethodSelected(method)"
@change="(val) => onStandaloneMethodChange(!!val, method)"
class="item-checkbox"
>
<span class="method-label-inner">{{ formatExamMethodCaption(method.name) }}</span>
</el-checkbox>
<span class="item-price">¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}</span>
</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, 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([]);
const selectedMethods = ref([]);
const methodPickerExpanded = ref(true);
// 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/#430: 懒加载套餐明细(支持 packageName 解析)
async function loadPackageDetails(row, treeNode, resolve) {
let packageId = row.packageId;
if (!packageId && row.packageName) {
try {
const pkgRes = await listCheckPackage({ packageName: row.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length > 0) {
packageId = packages[0].id;
}
} catch (err) {
console.error('套餐名称解析失败:', err);
}
}
if (!packageId) {
resolve([]);
return;
}
try {
const res = await request({
url: `/system/check-type/package/${packageId}/details`,
method: 'get'
});
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);
} catch (err) {
console.error('加载套餐明细失败:', err);
resolve([]);
}
}
function getPackageDetailsList(item) {
// 明细挂在行对象上,避免仅写入 methods 内嵌对象时首帧不触发视图更新(体感需点两次才展开)
if (Array.isArray(item?.packageDetailsDisplay)) {
return item.packageDetailsDisplay;
}
return Array.isArray(item?.packageDetails) ? item.packageDetails : [];
}
function getMethodPackageDetailsList(item) {
if (Array.isArray(item?.methodPackageDetails)) {
return item.methodPackageDetails;
}
return Array.isArray(item?.selectedMethod?.packageDetails) ? item.selectedMethod.packageDetails : [];
}
/** 有套餐 ID 或 packageName 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
function shouldShowPackageBody(item) {
return shouldShowItemPackageBody(item) || shouldShowMethodPackageBody(item);
}
function hasItemPackage(item) {
return !!(item?.packageId || item?.packageName);
}
function hasMethodPackage(item) {
return !!(item?.selectedMethod?.packageId || item?.selectedMethod?.packageName);
}
function isSamePackage(item) {
if (!hasItemPackage(item) || !hasMethodPackage(item)) return false;
if (item.packageId && item.selectedMethod?.packageId) {
return String(item.packageId) === String(item.selectedMethod.packageId);
}
return String(item.packageName || '') === String(item.selectedMethod?.packageName || '');
}
function shouldShowItemPackageBody(item) {
return hasItemPackage(item);
}
function shouldShowMethodPackageBody(item) {
return hasMethodPackage(item) && !isSamePackage(item);
}
/** 金额展示:统一两位小数 */
function formatDetailAmount(value) {
const n = Number(value ?? 0);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
}
/** 已选卡片名称:去掉 UI 上冗余的“套餐”前缀,完整名称通过 tooltip 展示 */
function getDisplayItemName(item) {
return String(item?.name || '').replace(/^套餐[:\-\s]*/, '');
}
/** 检查方法展示:避免与后端文案重复出现「(方法)(方法)」 */
function formatExamMethodCaption(name) {
const raw = String(name || '').trim();
if (!raw) return '';
if (/^\(方法\)/.test(raw) || /^(方法)/.test(raw)) {
return raw;
}
return `(方法) ${raw}`;
}
/** 已选方法纯文本(用于标题下级展示,不包含「勾选」前缀,去掉后端自带的 (方法) 前缀) */
function getDisplaySelectedMethodName(item) {
const raw = String(item?.selectedMethod?.name || '').trim();
if (!raw) return '';
return raw.replace(/^\(方法\)\s*/, '').replace(/^(方法)\s*/, '').trim();
}
function getSelectedItemAmount(item) {
return Number(item?.price || 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) {
let packageId = item.packageId;
const packageName = item.packageName;
if (!packageId && !packageName) {
return;
}
item.packageDetailsLoading = true;
try {
if (!packageId && packageName) {
const pkgRes = await listCheckPackage({ 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;
item.packageName = item.packageName || packageName;
}
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;
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 = [];
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 activeCategory = computed(() => {
const id = activeNames.value;
if (id === '' || id === null || id === undefined) return null;
return categoryList.value.find((cat) => String(cat.typeId) === String(id)) || null;
});
const activeCategoryName = computed(() => activeCategory.value?.typeName || activeCategory.value?.categoryName || '');
const methodsForActiveCategory = computed(() => {
const arr = activeCategory.value?.methods;
return Array.isArray(arr) ? arr : [];
});
// ====== 科室下拉(来源:科室管理)======
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;
});
function isStandaloneMethodSelected(method) {
return selectedMethods.value.some((m) => String(m.id) === String(method?.id));
}
function getDisplayMethodName(method) {
const raw = String(method?.name || '').trim();
if (!raw) return '';
return raw.replace(/^\(方法\)\s*/, '').replace(/^(方法)\s*/, '').trim();
}
function hasStandaloneMethodPackage(method) {
return !!(method?.packageId || method?.packageName);
}
function getStandaloneMethodPackageDetailsList(method) {
return Array.isArray(method?.packageDetails) ? method.packageDetails : [];
}
async function onStandaloneMethodChange(checked, method) {
if (!method) return;
if (checked) {
if (!isStandaloneMethodSelected(method)) {
selectedMethods.value.push({
...method,
expanded: false,
packageLoading: false,
packageDetails: []
});
}
} else {
const idx = selectedMethods.value.findIndex((m) => String(m.id) === String(method.id));
if (idx > -1) selectedMethods.value.splice(idx, 1);
}
updateMethodDisplay();
await nextTick();
form.totalAmount = totalAmountCalc.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;
// 切换分类时自动展开方法选择器,使关联的检查方法对医生可见
methodPickerExpanded.value = true;
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
}
}
updateMethodDisplay();
}
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) {
const firstCat = categoryList.value[0];
activeNames.value = firstCat.typeId;
await nextTick();
await handleCategoryExpand(firstCat);
}
} 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)),
methods: cat.methods || []
})).filter(cat => cat.items.length > 0);
});
// ====== 合计 ======
const totalAmountCalc = computed(() => {
const itemTotal = selectedItems.value.reduce((sum, item) => {
const effectivePrice = getSelectedItemAmount(item);
return sum + (effectivePrice * (item.quantity || 1));
}, 0);
const methodTotal = selectedMethods.value.reduce((sum, method) => {
return sum + Number(method?.packagePrice ?? method?.price ?? 0);
}, 0);
const total = itemTotal + methodTotal;
return total.toFixed(2);
});
// 监听已选项:自动更新申检部位
watch(selectedItems, () => {
form.inspectionArea = selectedItems.value.map(i => i.name).join('+');
form.isCharged = selectedItems.value.length > 0 || selectedMethods.value.length > 0 ? 1 : 0;
}, { deep: true });
watch(selectedMethods, () => {
form.isCharged = selectedItems.value.length > 0 || selectedMethods.value.length > 0 ? 1 : 0;
updateMethodDisplay();
}, { 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 = [];
selectedMethods.value = [];
resetCategoryChecked();
activeDetailTab.value = 'applyForm';
// 自动加载临床诊断
loadClinicalDiag();
}
function handleSave() {
formRef.value.validate(valid => {
if (!valid) return;
if (selectedItems.value.length === 0) {
ElMessage.warning('请至少选择一个检查明细项目');
return;
}
if (selectedMethods.value.length === 0) {
ElMessage.warning('请选择检查方法');
return;
}
// 从已选项目推导检查类型编码(取第一个项目的 checkType如 CT / ECG / GI
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
form.examTypeCode = firstCheckType;
form.totalAmount = totalAmountCalc.value;
const primaryMethod = selectedMethods.value[0] || null;
const payload = {
...form,
encounterId: props.patientInfo?.encounterId || null,
patientIdNum: props.patientInfo?.patientId || null,
checkMethods: selectedMethods.value.map((method) => ({
checkMethodId: method.id || null,
checkMethodName: method.name || null,
checkMethodCode: method.code || null,
checkMethodPackageName: method.packageName || null
})),
items: selectedItems.value.map((item, index) => ({
itemCode: String(item.id),
itemName: item.name,
bodyPartCode: item.checkType || 'unknown',
itemFee: getSelectedItemAmount(item),
performDeptCode: form.performDeptCode || '',
itemStatus: 0,
itemSeq: index + 1,
// 检查方法信息
checkMethodId: primaryMethod?.id || null,
checkMethodName: primaryMethod?.name || null,
checkMethodCode: primaryMethod?.code || null,
checkMethodPackageName: primaryMethod?.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 = [];
selectedMethods.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,
projectFoldExpanded: false,
methodFoldExpanded: false,
methodPackageExpanded: false,
packageDetailsLoading: false,
isPackage: false,
packageId: null,
hasChildren: false // #426修复: 树形表格懒加载展开标记后续根据packageId动态设置
};
// 加载该项目的检查方法
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.hasChildren = true; // #426修复
}
}
if (item.selectedMethod?.packageId) {
item.hasChildren = true; // #426修复
}
}
} catch (err) {
console.error('加载检查方法失败', err);
}
}
return item;
}));
// Bug #408修复: 确保明细数据正确加载到selectedItems
const methodMap = new Map();
for (const item of itemsWithMethods) {
if (item.selectedMethod && !methodMap.has(String(item.selectedMethod.id))) {
methodMap.set(String(item.selectedMethod.id), {
...item.selectedMethod,
expanded: false,
packageLoading: false,
packageDetails: []
});
}
item.selectedMethod = null;
item.methodPackageDetails = [];
}
selectedItems.value = itemsWithMethods;
selectedMethods.value = Array.from(methodMap.values());
// 加载套餐明细(单个失败不影响其他项目和明细显示)
for (const it of selectedItems.value) {
if (hasItemPackage(it)) {
try {
await loadPackageDetailsForItem(it);
} catch (e) {
console.error('加载套餐明细失败:', it.name, e);
}
}
it.methodFoldExpanded = false;
syncItemExpandedFlag(it);
}
for (const method of selectedMethods.value) {
if (hasStandaloneMethodPackage(method)) {
try {
await loadStandaloneMethodPackageDetails(method);
} catch (e) {
console.error('加载检查方法套餐明细失败:', method.name, e);
}
}
}
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();
});
});
}
// ====== 勾选逻辑 ======
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,
projectFoldExpanded: false,
methodFoldExpanded: false,
methodPackageExpanded: false,
isPackage: !!(item.packageId || item.packageName),
packageName: item.packageName || null,
packageDetailsLoading: false,
packageId: item.packageId || null,
hasChildren: !!(item.packageId || item.packageName) // #426修复: 树形表格懒加载展开标记,支持 packageName 解析
};
selectedItems.value.push(newRow);
// 必须用数组里的响应式行,不能继续改局部 newRowpush 后列表内是 proxy改 raw 对象不会触发右侧卡片更新(会一直卡在「加载中」)
const rowJustAdded = selectedItems.value[selectedItems.value.length - 1];
syncItemExpandedFlag(rowJustAdded);
updateMethodDisplay();
await nextTick();
form.totalAmount = totalAmountCalc.value;
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
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 修复:移除自动切换页签逻辑,保持当前页签状态
}
/** expanded 与各折叠条保持一致(明细表等仍可依赖 expanded */
function syncItemExpandedFlag(row) {
if (!row) return;
row.expanded = !!(row.projectFoldExpanded || row.methodFoldExpanded);
}
async function toggleProjectFold(item) {
item.projectFoldExpanded = !item.projectFoldExpanded;
syncItemExpandedFlag(item);
if (item.projectFoldExpanded && hasItemPackage(item) && getPackageDetailsList(item).length === 0 && !item.packageDetailsLoading) {
await loadPackageDetailsForItem(item);
}
}
async function toggleMethodPackageExpand(item) {
item.methodPackageExpanded = !item.methodPackageExpanded;
if (
item.methodPackageExpanded &&
item.selectedMethod &&
getMethodPackageDetailsList(item).length === 0 &&
!item.methodPackageLoading
) {
await loadMethodPackageDetails(item, item.selectedMethod);
}
}
async function toggleSelectedMethodFold(method) {
method.expanded = !method.expanded;
if (
method.expanded &&
hasStandaloneMethodPackage(method) &&
getStandaloneMethodPackageDetailsList(method).length === 0 &&
!method.packageLoading
) {
await loadStandaloneMethodPackageDetails(method);
}
}
function handleRemoveMethod(idx) {
selectedMethods.value.splice(idx, 1);
updateMethodDisplay();
}
async function loadStandaloneMethodPackageDetails(method) {
method.packageLoading = true;
method.packageDetails = [];
try {
let packageId = method.packageId;
if (!packageId && !method.packageName) {
method.packageLoading = false;
return;
}
if (!packageId && method.packageName) {
const pkgRes = await listCheckPackage({ packageName: method.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
method.packageLoading = false;
return;
}
packageId = packages[0].id;
method.packageId = packageId;
}
const detailRes = await request({
url: `/system/check-type/package/${packageId}/details`,
method: 'get'
});
method.packageDetails = parsePackageDetailsPayload(detailRes).map(d => ({
id: d.id,
name: d.name || d.itemName,
quantity: d.quantity || 1,
unit: d.unit || '次',
price: d.price ?? d.unitPrice ?? d.itemPrice ?? 0,
amount: d.amount || d.total || 0,
checked: true
}));
} catch (err) {
console.error('加载检查方法套餐明细失败:', err);
method.packageDetails = [];
} finally {
method.packageLoading = false;
}
}
// 根据检查方法的packageName加载对应的套餐明细
async function loadMethodPackageDetails(item, method) {
item.methodPackageLoading = true;
item.methodPackageDetails = [];
try {
let packageId = method.packageId;
if (!packageId && !method.packageName) {
item.methodPackageLoading = false;
return;
}
// 通过packageName查询套餐获取packageId
if (!packageId && method.packageName) {
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;
}
packageId = packages[0].id;
method.packageId = packageId;
}
// 查询套餐明细
const detailRes = await request({
url: `/system/check-type/package/${packageId}/details`,
method: 'get'
});
const detailList = parsePackageDetailsPayload(detailRes);
if (detailList.length > 0) {
const mapped = detailList.map(d => ({
id: d.id,
name: d.name || d.itemName,
quantity: d.quantity || 1,
unit: d.unit || '次',
price: d.price ?? d.unitPrice ?? d.itemPrice ?? 0,
amount: d.amount || d.total || 0,
checked: true // 默认勾选
}));
item.methodPackageDetails = mapped;
method.packageDetails = mapped;
}
} catch (err) {
console.error('加载方法套餐明细失败:', err);
item.methodPackageDetails = [];
} finally {
item.methodPackageLoading = false;
}
}
/** 检查明细表格中切换检查方法 */
async function onDetailMethodChange(row, val) {
row.selectedMethod = val || null;
if (val?.packageId || val?.packageName) {
row.hasChildren = true; // #426修复
}
row.methodPackageDetails = [];
updateMethodDisplay();
const open = shouldShowPackageBody(row);
row.expanded = open;
row.projectFoldExpanded = shouldShowItemPackageBody(row) && open;
row.methodFoldExpanded = shouldShowMethodPackageBody(row) && open;
row.methodPackageExpanded = false;
syncItemExpandedFlag(row);
if (hasItemPackage(row)) {
await loadPackageDetailsForItem(row);
}
if (val?.packageId || val?.packageName) {
await loadMethodPackageDetails(row, val);
}
nextTick(() => {
form.totalAmount = totalAmountCalc.value;
});
}
// Bug #384修复: 更新检查方法显示字段(取自独立已选检查方法)
function updateMethodDisplay() {
if (selectedMethods.value.length > 0) {
form.selectedMethodDisplay = selectedMethods.value.map((method) => method.name).join('、');
return;
}
form.selectedMethodDisplay = '';
}
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 = '';
updateMethodDisplay();
} 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: 560px;
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: 300px; /* Bug #500: 增大最小高度,避免折叠动画期间容器高度突变 */
}
.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;
min-height: 50px; /* Bug #500: 方法区域预留最小高度,减少加载完成后的高度突变 */
}
.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;
}
/* Bug #500修复: 方法列表骨架占位样式 */
.method-section-skeleton {
opacity: 0.6;
pointer-events: none;
}
.skeleton-method-row {
height: 22px;
border-radius: 3px;
background: linear-gradient(90deg, #e8f4ff 25%, #d0e8ff 50%, #e8f4ff 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
margin-bottom: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 已选择 tags */
/* 已选择:加宽,避免套餐明细挤成一团 */
.right-column {
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
}
.method-picker-section {
width: 260px;
min-width: 240px;
max-width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
margin-top: 8px;
}
.selected-panel {
width: 260px;
min-width: 240px;
max-width: 320px;
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;
}
/* 项目上 / 方法下:各自独立下拉条 */
.fold-strip {
border-bottom: 1px solid var(--el-border-color-lighter);
}
.fold-strip:last-child {
border-bottom: none;
}
.fold-strip-header {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 10px;
cursor: pointer;
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
}
.fold-strip-header:hover {
background: linear-gradient(180deg, #ecf5ff 0%, #e3eef8 100%);
}
.fold-strip-method.is-method-target .fold-strip-header {
background: linear-gradient(180deg, #e8f3ff 0%, #dceaff 100%);
}
.fold-chevron {
font-size: 14px;
color: #909399;
transition: transform 0.2s ease;
flex-shrink: 0;
margin-top: 2px;
transform: rotate(-90deg);
}
.fold-chevron.open {
transform: rotate(0deg);
}
/* 非套餐项目无展开箭头占位header 直接对齐 */
.fold-strip-header.no-chevron {
cursor: default;
}
.fold-strip-header.no-chevron:hover {
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
}
.fold-header-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.fold-kicker {
font-size: 11px;
font-weight: 600;
color: #909399;
letter-spacing: 0.03em;
}
.fold-title {
font-size: 13px;
font-weight: 600;
color: #303133;
line-height: 1.35;
word-break: break-word;
}
.fold-title-plain {
font-weight: 500;
color: #606266;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.fold-price-strong {
font-size: 13px;
color: #409eff;
font-weight: 600;
flex-shrink: 0;
margin-top: 2px;
}
.fold-price-strong.warn {
color: #e6a23c;
}
.fold-strip-body {
background: #fafbfc;
padding: 0 10px 10px 36px;
border-top: 1px dashed var(--el-border-color-lighter);
}
.fold-package-wrap {
padding-top: 6px;
}
.fold-strip-muted {
font-size: 12px;
color: #909399;
padding: 10px 0 4px;
}
.selected-global-method-picker {
flex-shrink: 0;
margin-top: 8px;
border-radius: 6px;
border: 1px solid #e4e7ed;
background: #fff;
overflow: hidden;
}
.method-picker-collapse-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 10px;
cursor: pointer;
background: #fff;
}
.method-picker-collapse-title:hover {
background: #f5f7fa;
}
.method-picker-title-main {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 600;
color: #303133;
}
.global-method-picker-scope {
font-size: 12px;
color: #909399;
flex-shrink: 0;
}
.method-picker-arrow {
font-size: 14px;
color: #909399;
transition: transform 0.2s ease;
flex-shrink: 0;
transform: rotate(-90deg);
}
.method-picker-arrow.expanded {
transform: rotate(0deg);
}
.global-method-picker-list {
display: flex;
flex-direction: column;
gap: 0;
padding: 6px 8px 8px;
border-top: 1px solid #ebeef5;
}
.method-picker-row {
padding: 6px 4px;
border-radius: 3px;
}
.expand-icon {
font-size: 14px;
color: #909399;
transition: transform 0.2s ease;
flex-shrink: 0;
transform: rotate(-90deg);
}
.expand-icon.expanded {
transform: rotate(0deg);
}
/* 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-section {
padding: 10px;
border-bottom: 1px solid #ebeef5;
}
.selected-section-title {
font-size: 12px;
font-weight: 600;
color: #409eff;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px dashed #d9ecff;
}
.selected-method-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 0;
}
.selected-method-option .method-checkbox {
flex: 1;
min-width: 0;
}
.selected-method-empty {
color: #c0c4cc;
font-size: 12px;
}
.package-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
font-size: 12px;
font-weight: 600;
color: #909399;
cursor: pointer;
border-bottom: 1px dashed #dcdfe6;
background: #fffbe6;
}
.package-toggle:hover {
color: #409eff;
}
/* 收起态:仅展示折叠箭头,不显示「套餐明细」等冗余标题 */
.package-toggle-minimal {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 8px 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
cursor: pointer;
border-bottom: 1px dashed #e4e7ed;
background: #fafafa;
}
.package-toggle-minimal:hover {
color: #409eff;
background: #f5f9ff;
}
.nested-empty-text {
font-size: 12px;
color: var(--el-text-color-placeholder);
padding-left: 2px;
}
.nested-label-row {
margin-bottom: 6px;
}
.nested-label {
font-size: 11px;
font-weight: 600;
color: var(--el-text-color-secondary);
letter-spacing: 0.03em;
}
.method-label-inner {
font-size: 13px;
}
.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>