- 移除检查项目套餐明细的冗余代码块 - 修复检查方法套餐明细显示逻辑中的重复条件判断 - 修正界面组件结构层级以改善渲染性能 - 更新仪器管理初始化数据传输对象的注解配置 - 替换 Lombok 注解从 @Data 为 @Getter/@Setter - 修复数据库映射文件中字段定义的语法错误 - 统一 SQL 查询语句的格式化风格
2928 lines
89 KiB
Vue
Executable File
2928 lines
89 KiB
Vue
Executable File
<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,而非 busNo(busNo 是病历号)
|
||
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.data(AjaxResult体)
|
||
// 结构为 { 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);
|
||
// 必须用数组里的响应式行,不能继续改局部 newRow:push 后列表内是 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>
|