2138 lines
72 KiB
Vue
Executable File
2138 lines
72 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" @click="handleAdd" icon="Plus">新增</el-button>
|
||
<el-button type="success" @click="handleSave" icon="Finished">保存</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bug #499: 查询过滤工具栏 -->
|
||
<div class="search-toolbar">
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="日期范围">
|
||
<el-date-picker
|
||
v-model="searchForm.dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 240px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable style="width: 140px">
|
||
<el-option
|
||
v-for="opt in statusOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="关键字">
|
||
<el-input
|
||
v-model="searchForm.keyword"
|
||
placeholder="申请单号 / 检查项目"
|
||
clearable
|
||
style="width: 200px"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSearch" icon="Search">搜索</el-button>
|
||
<el-button @click="handleResetSearch" icon="Refresh">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="filteredApplicationList"
|
||
:max-height="200"
|
||
highlight-current-row
|
||
@row-click="handleRowClick"
|
||
border
|
||
size="small"
|
||
:header-cell-style="{ background: '#f5f5f5', color: '#303133', fontWeight: '600' }"
|
||
>
|
||
<el-table-column type="selection" width="40" align="center" />
|
||
<el-table-column label="申请ID" prop="id" width="80" align="center" />
|
||
<el-table-column label="申请单号" prop="applyNo" min-width="140" align="center" />
|
||
<el-table-column label="申检部位" prop="inspectionArea" min-width="100" align="center" />
|
||
<el-table-column label="申请医生" prop="applyDocCode" min-width="90" align="center" />
|
||
<el-table-column label="急" prop="isUrgent" width="50" align="center">
|
||
<template #default="{ row }">
|
||
<el-checkbox v-model="row.isUrgent" :true-label="1" :false-label="0" disabled />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收费" prop="isCharged" width="50" align="center">
|
||
<template #default="{ row }">
|
||
<el-checkbox v-model="row.isCharged" :true-label="1" :false-label="0" disabled />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="退费" prop="isRefunded" width="50" align="center">
|
||
<template #default="{ row }">
|
||
<el-checkbox v-model="row.isRefunded" :true-label="1" :false-label="0" disabled />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="执行" prop="isExecuted" width="50" align="center">
|
||
<template #default="{ row }">
|
||
<el-checkbox v-model="row.isExecuted" :true-label="1" :false-label="0" disabled />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额" prop="totalAmount" width="90" align="right">
|
||
<template #default="{ row }">
|
||
{{ (row.totalAmount || 0).toFixed(2) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link @click.stop="handlePrint(row)" title="打印">
|
||
<el-icon><Printer /></el-icon>
|
||
</el-button>
|
||
<el-button link type="danger" @click.stop="handleDelete(row)" title="删除">
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- ====== 底部主区:左表单 + 右分类 ====== -->
|
||
<div class="bottom-section">
|
||
<!-- 左:表单区 -->
|
||
<div class="form-panel">
|
||
<el-tabs v-model="activeDetailTab" class="form-tabs">
|
||
<!-- TAB1:检查申请单 -->
|
||
<el-tab-pane label="检查申请单" name="applyForm">
|
||
<el-form ref="formRef" :model="form" :rules="rules" size="small" class="apply-form">
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="申请单号" prop="applyNo">
|
||
<el-input v-model="form.applyNo" readonly placeholder="自动生成" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="姓名" prop="patientName">
|
||
<el-input v-model="form.patientName" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="就诊卡号" prop="medicalrecordNumber">
|
||
<el-input v-model="form.medicalrecordNumber" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="费用性质" prop="natureofCost">
|
||
<el-select v-model="form.natureofCost" style="width:100%">
|
||
<el-option label="自费医疗" value="自费医疗" />
|
||
<el-option label="医保报销" value="医保报销" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="申请日期" prop="applyTime">
|
||
<el-date-picker v-model="form.applyTime" type="date" style="width:100%"
|
||
format="YYYY-MM-DD" value-format="YYYY-MM-DD HH:mm:ss" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="申请科室" prop="applyDeptCode">
|
||
<el-input v-model="form.applyDeptCode" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="申请医生" prop="applyDocCode">
|
||
<el-input v-model="form.applyDocCode" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="执行科室" prop="performDeptCode">
|
||
<el-select
|
||
v-model="form.performDeptCode"
|
||
style="width: 100%"
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
clearable
|
||
placeholder="请选择执行科室(支持模糊查询)"
|
||
:remote-method="handleOrgRemoteSearch"
|
||
:loading="orgLoading"
|
||
>
|
||
<el-option
|
||
v-for="opt in orgFilteredOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
<template #empty>
|
||
<div style="padding: 10px 0; color: #909399; text-align: center;">
|
||
{{ orgLoading ? '加载中...' : '暂无匹配科室' }}
|
||
</div>
|
||
</template>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="24">
|
||
<el-form-item label="诊断描述" prop="clinicDesc">
|
||
<el-input v-model="form.clinicDesc" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="禁忌症" prop="contraindication">
|
||
<el-input v-model="form.contraindication" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="临床诊断" prop="clinicalDiag">
|
||
<el-input v-model="form.clinicalDiag" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="病史摘要" prop="medicalHistorySummary">
|
||
<el-input v-model="form.medicalHistorySummary" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="24">
|
||
<el-form-item label="检查目的" prop="purposeDesc">
|
||
<el-input v-model="form.purposeDesc" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="体格检查">
|
||
<el-input v-model="form.purposeofInspection" placeholder="T(摄氏度) P次/分 R次/分 BF" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="申检部位">
|
||
<el-input v-model="form.inspectionArea" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
<!-- Bug #384修复: 添加检查方法只读输入框,联动显示选中的检查方法 -->
|
||
<el-col :span="8">
|
||
<el-form-item label="检查方法">
|
||
<el-input v-model="form.selectedMethodDisplay" readonly placeholder="请在右侧选择" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="备注" prop="applyRemark">
|
||
<el-input v-model="form.applyRemark" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="16">
|
||
<el-form-item label="状态">
|
||
<el-checkbox v-model="form.isUrgent" :true-label="1" :false-label="0">急</el-checkbox>
|
||
<el-checkbox v-model="form.isCharged" :true-label="1" :false-label="0" disabled>收费</el-checkbox>
|
||
<el-checkbox v-model="form.isRefunded" :true-label="1" :false-label="0" disabled>退费</el-checkbox>
|
||
<el-checkbox v-model="form.isExecuted" :true-label="1" :false-label="0" disabled>执行</el-checkbox>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
|
||
<!-- TAB2:检查明细 -->
|
||
<el-tab-pane label="检查明细" name="applyDetail">
|
||
<!-- 🔧 BugFix#426: 支持树形展开显示套餐明细 -->
|
||
<el-table
|
||
ref="detailTableRef"
|
||
:data="selectedItems"
|
||
row-key="id"
|
||
border
|
||
size="small"
|
||
style="width:100%"
|
||
:max-height="350"
|
||
:header-cell-style="{ background: '#f5f5f5', color: '#303133' }"
|
||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||
:load="loadPackageDetails"
|
||
lazy
|
||
>
|
||
<el-table-column label="行" type="index" width="45" align="center" />
|
||
<el-table-column label="检查项目" prop="name" min-width="120">
|
||
<template #default="scope">
|
||
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
||
<span>{{ scope.row.name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="部位" prop="applyPart" min-width="90">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.applyPart" size="small" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="检查方法" min-width="160">
|
||
<template #default="scope">
|
||
<el-select
|
||
v-if="scope.row.methods && scope.row.methods.length > 1"
|
||
:model-value="scope.row.selectedMethod"
|
||
value-key="id"
|
||
size="small"
|
||
style="width: 100%"
|
||
placeholder="选择方法"
|
||
@update:model-value="(val) => onDetailMethodChange(scope.row, val)"
|
||
>
|
||
<el-option
|
||
v-for="meth in scope.row.methods"
|
||
:key="meth.id"
|
||
:label="`${meth.name}${meth.packagePrice != null ? ' ¥' + formatDetailAmount(meth.packagePrice) : ''}`"
|
||
:value="meth"
|
||
/>
|
||
</el-select>
|
||
<template v-else>
|
||
<span v-if="scope.row.selectedMethod">
|
||
{{ scope.row.selectedMethod.name }}
|
||
</span>
|
||
<span v-else-if="scope.row.methods && scope.row.methods.length > 0" style="color: #909399;">
|
||
未选择
|
||
</span>
|
||
<span v-else style="color: #c0c4cc;">-</span>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="单位" prop="unit" width="55" align="center" />
|
||
<el-table-column label="总量" prop="quantity" width="70" align="center">
|
||
<template #default="scope">
|
||
<el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<!-- Bug #384修复: 单价显示套餐价格(如果选中)或部位价格 -->
|
||
<el-table-column label="单价" width="75" align="right">
|
||
<template #default="scope">
|
||
{{ scope.row.selectedMethod?.packagePrice || scope.row.price }}
|
||
</template>
|
||
</el-table-column>
|
||
<!-- Bug #384修复: 金额使用有效价格计算 -->
|
||
<el-table-column label="金额" width="80" align="right">
|
||
<template #default="scope">
|
||
{{ ((scope.row.selectedMethod?.packagePrice || scope.row.price || 0) * (scope.row.quantity || 1)).toFixed(2) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" prop="checkType" width="70" align="center" />
|
||
<el-table-column label="国码" prop="nationalCode" width="70" align="center" />
|
||
<el-table-column label="自费" width="50" align="center">
|
||
<template #default>
|
||
<el-checkbox disabled />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="total-row">
|
||
合计:<span class="total-amount">{{ totalAmountCalc }}</span>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
|
||
<!-- 右:检查项目分类面板 -->
|
||
<div class="category-panel">
|
||
<div class="panel-top">
|
||
<!-- 左侧:分类搜索 + 折叠树 -->
|
||
<div class="category-left">
|
||
<div class="panel-label">检查项目分类</div>
|
||
<el-input
|
||
v-model="dictSearchKey"
|
||
placeholder="搜索检查项目(支持拼音首字母)"
|
||
prefix-icon="Search"
|
||
clearable
|
||
size="small"
|
||
class="search-input"
|
||
/>
|
||
<!-- 分类折叠列表 -->
|
||
<div class="collapse-scroll" v-loading="dictLoading">
|
||
<div v-if="filteredCategoryList.length === 0" class="empty-hint">
|
||
{{ dictLoading ? '' : '暂无检查项目,请在"检查项目设置"中配置' }}
|
||
</div>
|
||
<el-collapse
|
||
v-else
|
||
v-model="activeNames"
|
||
accordion
|
||
@change="handleCollapseChange"
|
||
>
|
||
<el-collapse-item
|
||
v-for="cat in filteredCategoryList"
|
||
:key="cat.typeId"
|
||
:name="cat.typeId"
|
||
>
|
||
<template #title>
|
||
<span class="cat-title">{{ cat.categoryName }}</span>
|
||
<span v-if="categoryLoadingSet.has(cat.typeId)" class="loading-dot"></span>
|
||
</template>
|
||
<!-- 检查项目(部位/项目列表) -->
|
||
<div
|
||
v-for="item in cat.items"
|
||
:key="item.id"
|
||
class="item-row"
|
||
>
|
||
<el-checkbox
|
||
v-model="item.checked"
|
||
@change="(val) => handleItemSelect(val, item, cat)"
|
||
class="item-checkbox"
|
||
>
|
||
{{ item.name }}
|
||
</el-checkbox>
|
||
<span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span>
|
||
</div>
|
||
<div v-if="categoryLoadingSet.has(cat.typeId)" class="category-loading-hint">
|
||
加载中...
|
||
</div>
|
||
<!-- Bug #428修复: 渲染分类联动加载的检查方法列表 -->
|
||
<!-- Bug #500修复: v-if 改为 v-show,避免方法列表加载时 DOM 突然插入导致高度跳变 -->
|
||
<div
|
||
v-show="cat.methods && cat.methods.length > 0"
|
||
class="method-section"
|
||
>
|
||
<div class="method-section-title">检查方法</div>
|
||
<div
|
||
v-for="method in cat.methods"
|
||
:key="method.id"
|
||
class="method-row"
|
||
>
|
||
<el-checkbox
|
||
:model-value="isMethodSelected(method, cat)"
|
||
@change="(val) => handleMethodSelect(val, method, cat)"
|
||
class="method-checkbox"
|
||
>
|
||
{{ method.name }}
|
||
</el-checkbox>
|
||
<span class="method-price-tag">¥{{ method.packagePrice || method.price || 0 }}</span>
|
||
</div>
|
||
</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:已选择 项目卡片(可展开显示检查方法) -->
|
||
<div class="selected-panel">
|
||
<div class="panel-label">已选择:</div>
|
||
<div class="selected-tags">
|
||
<div v-if="selectedItems.length === 0" class="empty-selected">–</div>
|
||
<div
|
||
v-else
|
||
v-for="(item, idx) in selectedItems"
|
||
:key="idx"
|
||
class="selected-item-card"
|
||
:class="{ 'is-expanded': item.expanded }"
|
||
>
|
||
<!-- Bug #384修复 + #426修复: 项目卡片头部,可展开/收起 -->
|
||
<div class="card-header" @click="toggleItemExpand(item)">
|
||
<el-tag v-if="item.isPackage || item.packageName" size="small" type="warning" style="margin-right: 4px; flex-shrink: 0;">套餐</el-tag>
|
||
<el-tooltip :content="item.name" placement="top" :show-after="400">
|
||
<span class="card-name">{{ item.name }}</span>
|
||
</el-tooltip>
|
||
<span class="card-price">¥{{ formatDetailAmount(item.price) }}</span>
|
||
<el-icon :class="['expand-icon', { expanded: item.expanded }]">
|
||
<ArrowDown v-if="!item.expanded" />
|
||
<ArrowUp v-if="item.expanded" />
|
||
</el-icon>
|
||
<!-- 删除按钮 -->
|
||
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
|
||
<el-icon><Close /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
<!-- Bug #428: 有套餐 ID 时默认展开;加载中/空/明细均在本区域展示 -->
|
||
<div v-if="item.expanded && shouldShowPackageBody(item)" class="selected-card-body">
|
||
<div v-if="item.packageDetailsLoading" class="package-details-loading">加载中...</div>
|
||
<template v-else>
|
||
<div v-if="getPackageDetailsList(item).length === 0" class="package-details-empty">
|
||
暂无套餐明细
|
||
</div>
|
||
<div v-else class="package-details-list">
|
||
<div class="package-details-head">套餐明细</div>
|
||
<div
|
||
v-for="(detail, dIdx) in getPackageDetailsList(item)"
|
||
:key="detail.id ?? detail.itemCode ?? `d-${dIdx}`"
|
||
class="detail-row"
|
||
>
|
||
<el-tooltip :content="detail.name" placement="top" :show-after="500">
|
||
<span class="detail-name">{{ detail.name }}</span>
|
||
</el-tooltip>
|
||
<div class="detail-meta">
|
||
<span class="detail-qty">×{{ detail.quantity || 1 }}</span>
|
||
<span class="detail-price">¥{{ formatDetailAmount(detail.price) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
|
||
import useUserStore from '@/store/modules/user';
|
||
import request from '@/utils/request';
|
||
import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType';
|
||
import { getEncounterDiagnosis } from '../api.js';
|
||
|
||
const props = defineProps({
|
||
patientInfo: { type: Object, default: () => ({}) },
|
||
activeTab: { type: String, default: '' }
|
||
});
|
||
|
||
// 保存成功后通知父组件刷新医嘱列表
|
||
const emit = defineEmits(['saved']);
|
||
|
||
const userStore = useUserStore();
|
||
const loading = ref(false);
|
||
const dictLoading = ref(false);
|
||
const activeDetailTab = ref('applyForm');
|
||
const applicationList = ref([]);
|
||
const selectedItems = ref([]);
|
||
|
||
// Bug #499: 查询过滤状态
|
||
const searchForm = reactive({
|
||
dateRange: [],
|
||
applyStatus: '',
|
||
keyword: ''
|
||
});
|
||
|
||
// 申请单状态选项
|
||
const statusOptions = [
|
||
{ label: '已开单', value: 0 },
|
||
{ label: '已收费', value: 1 },
|
||
{ label: '已预约', value: 2 },
|
||
{ label: '已签到', value: 3 },
|
||
{ label: '部分报告', value: 4 },
|
||
{ label: '已完告', value: 5 },
|
||
{ label: '已作废', value: 6 }
|
||
];
|
||
|
||
// Bug #499: 过滤后的申请单列表
|
||
const filteredApplicationList = computed(() => {
|
||
let result = applicationList.value;
|
||
|
||
// 日期范围过滤
|
||
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
|
||
const start = searchForm.dateRange[0];
|
||
const end = searchForm.dateRange[1];
|
||
result = result.filter(item => {
|
||
const d = item.applyTime;
|
||
if (!d) return false;
|
||
const dateStr = d.length > 10 ? d.substring(0, 10) : d;
|
||
return dateStr >= start && dateStr <= end;
|
||
});
|
||
}
|
||
|
||
// 状态过滤
|
||
if (searchForm.applyStatus !== '' && searchForm.applyStatus !== null && searchForm.applyStatus !== undefined) {
|
||
result = result.filter(item => item.applyStatus === searchForm.applyStatus);
|
||
}
|
||
|
||
// 关键字过滤(申请单号、申检部位、检查项目名)
|
||
if (searchForm.keyword) {
|
||
const kw = searchForm.keyword.toLowerCase();
|
||
result = result.filter(item => {
|
||
return (item.applyNo || '').toLowerCase().includes(kw)
|
||
|| (item.inspectionArea || '').toLowerCase().includes(kw);
|
||
});
|
||
}
|
||
|
||
return result;
|
||
});
|
||
|
||
// Bug #499: 搜索与重置
|
||
function handleSearch() {
|
||
// 过滤逻辑由 computed 自动处理
|
||
}
|
||
|
||
function handleResetSearch() {
|
||
const now = new Date();
|
||
const end = now.toISOString().substring(0, 10);
|
||
const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().substring(0, 10);
|
||
searchForm.dateRange = [start, end];
|
||
searchForm.applyStatus = '';
|
||
searchForm.keyword = '';
|
||
}
|
||
|
||
// 初始化默认日期范围为近一周
|
||
handleResetSearch();
|
||
|
||
// 🔧 BugFix#426: 懒加载套餐明细
|
||
async function loadPackageDetails(row, treeNode, resolve) {
|
||
if (!row.packageId) {
|
||
resolve([]);
|
||
return;
|
||
}
|
||
try {
|
||
const res = await request({
|
||
url: `/system/check-type/package/${row.packageId}/details`,
|
||
method: 'get'
|
||
});
|
||
if (res.code === 200) {
|
||
const list = parsePackageDetailsPayload(res);
|
||
const children = list.map((child) => ({
|
||
...child,
|
||
name: child.name || child.itemName,
|
||
unit: child.unit || '次',
|
||
price: child.price ?? child.unitPrice ?? child.itemPrice ?? 0,
|
||
quantity: row.quantity || 1,
|
||
isPackageDetail: true
|
||
}));
|
||
resolve(children);
|
||
} else {
|
||
resolve([]);
|
||
}
|
||
} catch (err) {
|
||
console.error('加载套餐明细失败:', err);
|
||
resolve([]);
|
||
}
|
||
}
|
||
|
||
// #428修复 + #426修复: 为已选择项目加载套餐明细(通过packageId或packageName查询)
|
||
/** 套餐明细挂在「部位」或已选的「检查方法」上(方法可带 packageId) */
|
||
function getPackageCarrier(item) {
|
||
return item?.selectedMethod?.packageId ? item.selectedMethod : item;
|
||
}
|
||
|
||
function getPackageDetailsList(item) {
|
||
// 明细挂在行对象上,避免仅写入 methods 内嵌对象时首帧不触发视图更新(体感需点两次才展开)
|
||
if (Array.isArray(item?.packageDetailsDisplay)) {
|
||
return item.packageDetailsDisplay;
|
||
}
|
||
const carrier = getPackageCarrier(item);
|
||
return Array.isArray(carrier?.packageDetails) ? carrier.packageDetails : [];
|
||
}
|
||
|
||
/** 有套餐 ID 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
|
||
function shouldShowPackageBody(item) {
|
||
return !!getPackageCarrier(item)?.packageId;
|
||
}
|
||
|
||
/** 金额展示:统一两位小数 */
|
||
function formatDetailAmount(value) {
|
||
const n = Number(value ?? 0);
|
||
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
|
||
}
|
||
|
||
/** 默认检查方法:优先与部位 packageId 一致的方法,否则取首个带套餐的方法,否则取第一个 */
|
||
function pickDefaultMethod(methods, partItem) {
|
||
if (!methods?.length) return null;
|
||
if (methods.length === 1) return methods[0];
|
||
const pid = partItem?.packageId ?? null;
|
||
if (pid != null && pid !== '') {
|
||
const matched = methods.find(
|
||
(x) => x.packageId != null && String(x.packageId) === String(pid)
|
||
);
|
||
if (matched) return matched;
|
||
}
|
||
const withPkg = methods.find((x) => x.packageId != null);
|
||
if (withPkg) return withPkg;
|
||
return methods[0];
|
||
}
|
||
|
||
function parsePackageDetailsPayload(res) {
|
||
const raw =
|
||
res?.data?.data ??
|
||
res?.data?.records ??
|
||
res?.data ??
|
||
res?.rows ??
|
||
res;
|
||
if (!Array.isArray(raw)) return [];
|
||
return raw;
|
||
}
|
||
|
||
// #428: 为已选择项目加载套餐明细(后端:CheckTypeController /system/check-type/package/{id}/details)
|
||
async function loadPackageDetailsForItem(item) {
|
||
const carrier = getPackageCarrier(item);
|
||
let packageId = item.packageId || carrier?.packageId;
|
||
if (!packageId && !item.packageName) {
|
||
return;
|
||
}
|
||
item.packageDetailsLoading = true;
|
||
try {
|
||
if (!packageId && item.packageName) {
|
||
const pkgRes = await listCheckPackage({ packageName: item.packageName });
|
||
let packages = pkgRes?.data || [];
|
||
if (!Array.isArray(packages)) {
|
||
packages = packages.records || packages.data || [];
|
||
}
|
||
if (packages.length === 0) {
|
||
item.packageDetails = [];
|
||
item.packageDetailsDisplay = [];
|
||
return;
|
||
}
|
||
packageId = packages[0].id;
|
||
item.packageId = packageId;
|
||
}
|
||
if (!packageId) {
|
||
item.packageDetails = [];
|
||
item.packageDetailsDisplay = [];
|
||
return;
|
||
}
|
||
const res = await request({
|
||
url: `/system/check-type/package/${packageId}/details`,
|
||
method: 'get'
|
||
});
|
||
const list = parsePackageDetailsPayload(res);
|
||
const mapped = list.map((detail) => ({
|
||
...detail,
|
||
name: detail.name || detail.itemName,
|
||
unit: detail.unit || '次',
|
||
price: detail.price ?? detail.unitPrice ?? detail.itemPrice ?? 0,
|
||
quantity: detail.quantity || 1
|
||
}));
|
||
item.packageDetailsDisplay = mapped;
|
||
carrier.packageDetails = mapped;
|
||
if (res.code === 200 && res.data) {
|
||
item.packageDetails = Array.isArray(res.data)
|
||
? res.data.map((detail) => ({
|
||
...detail,
|
||
name: detail.name || detail.itemName,
|
||
unit: detail.unit || '次',
|
||
price: detail.price || detail.unitPrice || 0,
|
||
quantity: detail.quantity || 1
|
||
}))
|
||
: mapped;
|
||
} else {
|
||
item.packageDetails = mapped;
|
||
}
|
||
} catch (err) {
|
||
console.error('加载套餐明细失败:', err);
|
||
item.packageDetailsDisplay = [];
|
||
carrier.packageDetails = [];
|
||
item.packageDetails = [];
|
||
} finally {
|
||
item.packageDetailsLoading = false;
|
||
}
|
||
}
|
||
const detailTableRef = ref(null);
|
||
const formRef = ref(null);
|
||
|
||
// ====== 表单数据 ======
|
||
const form = reactive({
|
||
applyNo: '',
|
||
patientName: '',
|
||
patientId: '',
|
||
visitNo: '',
|
||
applyDeptCode: '',
|
||
performDeptCode: '',
|
||
applyDocCode: '',
|
||
applyTime: '',
|
||
medicalrecordNumber: '',
|
||
natureofCost: '自费医疗',
|
||
clinicDesc: '',
|
||
contraindication: '',
|
||
medicalHistorySummary: '',
|
||
purposeofInspection: '',
|
||
inspectionArea: '',
|
||
inspectionMethod: '',
|
||
applyRemark: '',
|
||
clinicalDiag: '',
|
||
purposeDesc: '',
|
||
isUrgent: 0,
|
||
pregnancyState: 0,
|
||
allergyDesc: '',
|
||
applyStatus: 0,
|
||
isCharged: 0,
|
||
isRefunded: 0,
|
||
isExecuted: 0,
|
||
examTypeCode: '', // 检查类型编码,必填字段,保存时从已选项目自动推导
|
||
selectedMethodDisplay: '' // Bug #384修复: 检查方法显示字段(联动)
|
||
});
|
||
|
||
const rules = {
|
||
natureofCost: [{ required: true, message: '请选择费用性质', trigger: 'change' }],
|
||
applyDeptCode: [{ required: true, message: '请输入申请科室', trigger: 'blur' }],
|
||
applyDocCode: [{ required: true, message: '请输入申请医生', trigger: 'blur' }],
|
||
performDeptCode: [{ required: true, message: '请输入执行科室', trigger: 'blur' }],
|
||
clinicDesc: [{ required: true, message: '请输入诊断描述', trigger: 'blur' }],
|
||
clinicalDiag: [{ required: true, message: '请输入临床诊断', trigger: 'blur' }],
|
||
medicalHistorySummary: [{ required: true, message: '请输入病史摘要', trigger: 'blur' }],
|
||
purposeDesc: [{ required: true, message: '请输入检查目的', trigger: 'blur' }]
|
||
};
|
||
|
||
// ====== 检查项目分类 ======
|
||
const categoryList = ref([]); // 原始分类+项目数据
|
||
const dictSearchKey = ref('');
|
||
const activeNames = ref(''); // 当前展开的折叠项
|
||
const categoryLoadingSet = ref(new Set()); // Bug #500: 正在加载方法的分类集合
|
||
const currentActiveCategory = ref(null); // Bug #500: 记录当前激活的分类,忽略过期请求响应
|
||
|
||
const allMethods = ref([]);
|
||
|
||
// ====== 科室下拉(来源:科室管理)======
|
||
const orgLoading = ref(false);
|
||
const orgOptions = ref([]); // { label, value }
|
||
const orgFilteredOptions = ref([]); // 展示用(截断前200条)
|
||
|
||
// 加载所有检查方法
|
||
async function loadAllMethods() {
|
||
try {
|
||
const res = await listCheckMethod(); // 使用已导入的或者直接利用 request 请求
|
||
let methods = [];
|
||
if (res && res.data) {
|
||
if (Array.isArray(res.data)) {
|
||
methods = res.data;
|
||
} else if (res.data.records) {
|
||
methods = res.data.records;
|
||
} else if (res.data.data && Array.isArray(res.data.data)) {
|
||
methods = res.data.data;
|
||
}
|
||
} else if (Array.isArray(res)) {
|
||
methods = res;
|
||
} else if (res && res.rows) {
|
||
methods = res.rows;
|
||
}
|
||
allMethods.value = methods;
|
||
} catch (err) {
|
||
console.error('加载检查方法失败', err);
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadOrgOptions();
|
||
await loadAllMethods();
|
||
await loadCategoryList();
|
||
});
|
||
|
||
async function loadOrgOptions() {
|
||
orgLoading.value = true;
|
||
try {
|
||
const res = await request({
|
||
url: '/base-data-manage/organization/organization',
|
||
method: 'get',
|
||
});
|
||
const records = res?.data?.records || res?.data || [];
|
||
|
||
const flat = [];
|
||
const walk = (nodes) => {
|
||
if (!Array.isArray(nodes)) return;
|
||
for (const n of nodes) {
|
||
if (!n) continue;
|
||
// 约定:typeEnum=2 为科室;若没有 typeEnum 也兜底收集
|
||
if (n.name && (n.typeEnum === 2 || n.typeEnum === '2' || n.typeEnum == null)) {
|
||
flat.push({ label: n.name, value: n.name });
|
||
}
|
||
if (Array.isArray(n.children) && n.children.length > 0) walk(n.children);
|
||
}
|
||
};
|
||
walk(records);
|
||
|
||
// 去重 + 排序
|
||
const uniq = Array.from(new Map(flat.map(o => [o.value, o])).values())
|
||
.filter(o => o?.value)
|
||
.sort((a, b) => (a.label || '').localeCompare(b.label || '', 'zh-CN'));
|
||
|
||
orgOptions.value = uniq;
|
||
orgFilteredOptions.value = uniq.slice(0, 200);
|
||
} catch (e) {
|
||
console.error('加载科室列表失败', e);
|
||
orgOptions.value = [];
|
||
orgFilteredOptions.value = [];
|
||
} finally {
|
||
orgLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleOrgRemoteSearch(keyword) {
|
||
const key = (keyword || '').trim().toLowerCase();
|
||
if (!key) {
|
||
orgFilteredOptions.value = orgOptions.value.slice(0, 200);
|
||
return;
|
||
}
|
||
orgFilteredOptions.value = orgOptions.value
|
||
.filter((o) => (o.label || '').toLowerCase().includes(key))
|
||
.slice(0, 200);
|
||
}
|
||
|
||
// 动态可用的检查方法(根据已选部位所属的检查类型进行过滤)
|
||
const normalizeTypeValue = value => String(value ?? '').trim().toLowerCase();
|
||
|
||
const availableMethods = computed(() => {
|
||
// 获取当前已选部位的检查类型(可取第一个选中的部位的 checkType)
|
||
const currentType = form.examTypeCode || (selectedItems.value.length > 0 ? selectedItems.value[0].checkType : '');
|
||
const normalizedCurrentType = normalizeTypeValue(currentType);
|
||
if (normalizedCurrentType) {
|
||
// 兼容脏数据:method 的类型可能落在 checkType/type/typeCode/code/typeName/categoryName 中
|
||
const filtered = allMethods.value.filter(m => {
|
||
const typeCandidates = [
|
||
m.checkType,
|
||
m.type,
|
||
m.typeCode,
|
||
m.code,
|
||
m.typeName,
|
||
m.categoryName
|
||
].map(normalizeTypeValue).filter(Boolean);
|
||
return typeCandidates.includes(normalizedCurrentType);
|
||
});
|
||
return filtered.length > 0 ? filtered : allMethods.value;
|
||
}
|
||
return allMethods.value;
|
||
});
|
||
|
||
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
|
||
// #428: 分类展开时联动加载检查方法
|
||
// Bug #500: 使用 categoryLoadingSet 替代 dictLoading,避免切换分类时整个区域闪烁
|
||
// Bug #500: 添加 currentActiveCategory 守卫,忽略过期请求响应,防止快速切换时数据闪烁
|
||
async function handleCategoryExpand(cat) {
|
||
if (!cat || !cat.typeName) return;
|
||
|
||
if ((cat.methods && cat.methods.length > 0) || categoryLoadingSet.value.has(cat.typeId)) return;
|
||
|
||
categoryLoadingSet.value.add(cat.typeId);
|
||
currentActiveCategory.value = cat.typeId;
|
||
try {
|
||
const res = await searchCheckMethod({ checkType: cat.typeName });
|
||
// 忽略过期请求:用户已切换到其他分类,丢弃当前响应
|
||
if (currentActiveCategory.value !== cat.typeId) return;
|
||
let data = res?.data?.data || res?.data || res?.rows || res;
|
||
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
|
||
data = res.data.data;
|
||
}
|
||
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
cat.methods = data.map(m => ({
|
||
id: m.id,
|
||
name: m.name,
|
||
code: m.code,
|
||
price: m.price || 0,
|
||
packageName: m.packageName || '',
|
||
packageId: m.packageId || null,
|
||
packagePrice: m.packagePrice || null,
|
||
serviceFee: m.serviceFee || null
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
if (currentActiveCategory.value !== cat.typeId) return;
|
||
console.error('加载分类检查方法失败', err);
|
||
} finally {
|
||
categoryLoadingSet.value.delete(cat.typeId);
|
||
}
|
||
}
|
||
// Bug #500修复: 不阻塞 accordion 状态更新,仅防止重复加载同一分类的方法
|
||
function handleCollapseChange(activeName) {
|
||
// 始终记录当前激活的分类,确保 handleCategoryExpand 能正确忽略过期请求
|
||
currentActiveCategory.value = activeName || null;
|
||
|
||
if (activeName) {
|
||
// Bug #428修复: 直接从 categoryList(原始响应式数组)查找分类,
|
||
// 确保后续 handleCategoryExpand 对 cat.methods 的赋值能正确触发 Vue 响应式更新
|
||
const cat = categoryList.value.find(c => c.typeId == activeName);
|
||
if (cat && (!cat.methods || cat.methods.length === 0)) {
|
||
handleCategoryExpand(cat); // 异步加载,不 await
|
||
}
|
||
}
|
||
}
|
||
|
||
watch(availableMethods, (newMethods) => {
|
||
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
|
||
form.inspectionMethod = '';
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 加载检查类型(分类)和检查项目(部位/项目),按类型分组展示
|
||
*/
|
||
async function loadCategoryList() {
|
||
dictLoading.value = true;
|
||
try {
|
||
// 1. 加载检查类型(分类名称),只取父级
|
||
const typeRes = await request({
|
||
url: '/system/check-type/list',
|
||
method: 'get',
|
||
params: { pageNo: 1, pageSize: 500 } // 取全量分类数据
|
||
});
|
||
let types = [];
|
||
if (typeRes && typeRes.data) {
|
||
if (Array.isArray(typeRes.data)) {
|
||
types = typeRes.data;
|
||
} else if (typeRes.data.records) {
|
||
types = typeRes.data.records;
|
||
} else if (typeRes.data.data && Array.isArray(typeRes.data.data)) {
|
||
types = typeRes.data.data;
|
||
}
|
||
} else if (Array.isArray(typeRes)) {
|
||
types = typeRes;
|
||
} else if (typeRes && typeRes.rows) {
|
||
types = typeRes.rows;
|
||
}
|
||
|
||
// 2. 加载检查项目(检查部位项目)
|
||
const partRes = await request({ url: '/check/part/list', method: 'get' });
|
||
let parts = [];
|
||
if (partRes && partRes.data) {
|
||
if (Array.isArray(partRes.data)) {
|
||
parts = partRes.data;
|
||
} else if (partRes.data.records) {
|
||
parts = partRes.data.records;
|
||
} else if (partRes.data.data && Array.isArray(partRes.data.data)) {
|
||
parts = partRes.data.data;
|
||
}
|
||
} else if (Array.isArray(partRes)) {
|
||
parts = partRes;
|
||
} else if (partRes && partRes.rows) {
|
||
parts = partRes.rows;
|
||
}
|
||
|
||
// 3. 按 checkType 归类
|
||
const dict = [];
|
||
for (const t of types) {
|
||
dict.push({
|
||
typeId: t.id,
|
||
typeCode: t.code, // 保存 code 用于后备匹配
|
||
orgType: t.type, // 保存 type 用于后备匹配
|
||
typeName: t.name, // 保存 name
|
||
categoryName: t.name,
|
||
// “检查类型管理”里配置的执行科室(图三)
|
||
performDeptName: t.department || '',
|
||
items: [],
|
||
methods: [] // #428修复: 初始化 methods 数组,确保 Vue 响应式追踪
|
||
});
|
||
}
|
||
const unclassified = [];
|
||
for (const p of parts) {
|
||
const mapped = {
|
||
id: p.id,
|
||
name: p.name,
|
||
price: p.price || 0,
|
||
serviceFee: p.serviceFee || 0,
|
||
unit: '次',
|
||
checkType: p.checkType || '',
|
||
nationalCode: p.nationalCode || '',
|
||
packageName: p.packageName || '',
|
||
packageId: p.packageId || null,
|
||
checked: false
|
||
};
|
||
|
||
// 增强匹配逻辑:部位的 checkType (如 'ECG', 'CT') 优先去匹配大类的 orgType,
|
||
// 如果大类的 type 字段脏了(比如填了中文),则尝试匹配 code,甚至是分类名称
|
||
const target = dict.find(d =>
|
||
d.orgType === p.checkType ||
|
||
d.typeCode === p.checkType ||
|
||
d.typeName === p.checkType
|
||
);
|
||
|
||
if (target) target.items.push(mapped);
|
||
else unclassified.push(mapped);
|
||
}
|
||
if (unclassified.length > 0) {
|
||
dict.push({ typeId: 'uncls', typeCode: '', categoryName: '其他', items: unclassified });
|
||
}
|
||
categoryList.value = dict.filter(d => d.items.length > 0);
|
||
|
||
// 默认展开第一个
|
||
if (categoryList.value.length > 0) {
|
||
activeNames.value = categoryList.value[0].typeId;
|
||
}
|
||
} catch (err) {
|
||
console.error('加载检查项目分类失败', err);
|
||
} finally {
|
||
dictLoading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 关键词过滤后的分类列表 */
|
||
const filteredCategoryList = computed(() => {
|
||
if (!dictSearchKey.value) return categoryList.value;
|
||
const key = dictSearchKey.value.toLowerCase();
|
||
return categoryList.value.map(cat => ({
|
||
...cat,
|
||
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key))
|
||
})).filter(cat => cat.items.length > 0);
|
||
});
|
||
|
||
// ====== 合计 ======
|
||
// Bug #384修复: 如果选中了检查方法,使用套餐价格;否则使用部位价格
|
||
const totalAmountCalc = computed(() => {
|
||
const total = selectedItems.value.reduce((sum, item) => {
|
||
const effectivePrice = item.selectedMethod?.packagePrice || item.price;
|
||
return sum + (effectivePrice * (item.quantity || 1));
|
||
}, 0);
|
||
return total.toFixed(2);
|
||
});
|
||
|
||
// 监听已选项:自动更新申检部位
|
||
watch(selectedItems, () => {
|
||
form.inspectionArea = selectedItems.value.map(i => i.name).join('+');
|
||
form.isCharged = selectedItems.value.length > 0 ? 1 : 0;
|
||
}, { deep: true });
|
||
|
||
// 监听患者变化
|
||
watch(() => props.patientInfo, (newVal) => {
|
||
if (newVal?.encounterId) {
|
||
initPatientForm(newVal);
|
||
getList();
|
||
}
|
||
}, { immediate: true, deep: true });
|
||
|
||
watch(() => props.activeTab, async (val) => {
|
||
if (val === 'examination') {
|
||
getList();
|
||
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
|
||
if (props.patientInfo?.encounterId) {
|
||
await loadClinicalDiag();
|
||
}
|
||
}
|
||
});
|
||
|
||
function initPatientForm(patient) {
|
||
form.patientName = patient.patientName || '';
|
||
// 就诊卡号应取值于 identifierNo,而非 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 = [];
|
||
resetCategoryChecked();
|
||
activeDetailTab.value = 'applyForm';
|
||
// 自动加载临床诊断
|
||
loadClinicalDiag();
|
||
}
|
||
|
||
function handleSave() {
|
||
formRef.value.validate(valid => {
|
||
if (!valid) return;
|
||
if (selectedItems.value.length === 0) {
|
||
ElMessage.warning('请至少选择一个检查明细项目');
|
||
return;
|
||
}
|
||
// 检查每个项目是否已选择检查方法
|
||
const itemsWithoutMethod = selectedItems.value.filter(item => !item.selectedMethod);
|
||
if (itemsWithoutMethod.length > 0) {
|
||
const names = itemsWithoutMethod.map(item => item.name).join('、');
|
||
ElMessage.warning(`以下项目未选择检查方法:${names},请在右侧勾选后再保存`);
|
||
return;
|
||
}
|
||
// 从已选项目推导检查类型编码(取第一个项目的 checkType,如 CT / ECG / GI)
|
||
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
|
||
form.examTypeCode = firstCheckType;
|
||
|
||
|
||
const payload = {
|
||
...form,
|
||
encounterId: props.patientInfo?.encounterId || null,
|
||
patientIdNum: props.patientInfo?.patientId || null,
|
||
items: selectedItems.value.map((item, index) => ({
|
||
itemCode: String(item.id),
|
||
itemName: item.name,
|
||
bodyPartCode: item.checkType || 'unknown',
|
||
// Bug #384修复: 如果选中了检查方法且有套餐价格,使用套餐价格;否则使用部位价格
|
||
itemFee: item.selectedMethod?.packagePrice || item.price,
|
||
performDeptCode: form.performDeptCode || '',
|
||
itemStatus: 0,
|
||
itemSeq: index + 1,
|
||
// 检查方法信息
|
||
checkMethodId: item.selectedMethod?.id || null,
|
||
checkMethodName: item.selectedMethod?.name || null,
|
||
checkMethodCode: item.selectedMethod?.code || null,
|
||
checkMethodPackageName: item.selectedMethod?.packageName || null // Bug #384修复: 保存套餐名称
|
||
}))
|
||
};
|
||
request({
|
||
url: '/exam/apply',
|
||
method: payload.applyNo ? 'put' : 'post',
|
||
data: payload
|
||
}).then(res => {
|
||
ElMessage.success('保存成功');
|
||
getList();
|
||
if (res.data) form.applyNo = res.data;
|
||
// 通知父组件刷新医嘱列表
|
||
emit('saved');
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleRowClick(row) {
|
||
Object.assign(form, row);
|
||
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
|
||
selectedItems.value = [];
|
||
activeDetailTab.value = 'applyForm';
|
||
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
|
||
// 响应结构: Axios拦截器对code===200返回res.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,
|
||
packageDetailsLoading: false,
|
||
isPackage: false,
|
||
packageId: null
|
||
};
|
||
// 加载该项目的检查方法
|
||
if (m.bodyPartCode) {
|
||
try {
|
||
const methodRes = await searchCheckMethod({ checkType: m.bodyPartCode });
|
||
// 正确解析 API 返回结构
|
||
let methodData = methodRes?.data?.data || methodRes?.data || methodRes?.rows || methodRes;
|
||
if (!Array.isArray(methodData) && methodRes?.data && Array.isArray(methodRes.data.data)) {
|
||
methodData = methodRes.data.data;
|
||
}
|
||
if (Array.isArray(methodData)) {
|
||
item.methods = methodData.map(md => ({
|
||
id: md.id,
|
||
name: md.name,
|
||
code: md.code,
|
||
price: m.itemFee || 0,
|
||
packageName: md.packageName || '',
|
||
packageId: md.packageId || null,
|
||
packagePrice: md.packagePrice || null,
|
||
serviceFee: md.serviceFee || null
|
||
}));
|
||
// 回充已保存的检查方法
|
||
if (m.checkMethodId) {
|
||
item.selectedMethod = item.methods.find(md => String(md.id) === String(m.checkMethodId)) || null;
|
||
if (item.selectedMethod?.packageId) {
|
||
item.isPackage = true;
|
||
item.packageId = item.selectedMethod.packageId;
|
||
}
|
||
}
|
||
if (!item.selectedMethod && item.methods.length) {
|
||
item.selectedMethod = pickDefaultMethod(item.methods, { packageId: item.packageId });
|
||
}
|
||
if (item.selectedMethod?.packageId) {
|
||
item.packageId = item.selectedMethod.packageId;
|
||
item.isPackage = true;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('加载检查方法失败', err);
|
||
}
|
||
}
|
||
return item;
|
||
}));
|
||
// Bug #408修复: 确保明细数据正确加载到selectedItems
|
||
selectedItems.value = itemsWithMethods;
|
||
// 加载套餐明细(单个失败不影响其他项目和明细显示)
|
||
for (const it of selectedItems.value) {
|
||
if (getPackageCarrier(it)?.packageId) {
|
||
try {
|
||
await loadPackageDetailsForItem(it);
|
||
} catch (e) {
|
||
console.error('加载套餐明细失败:', it.name, e);
|
||
}
|
||
}
|
||
it.expanded = !!getPackageCarrier(it)?.packageId;
|
||
}
|
||
syncCategoryChecked();
|
||
// Bug #384修复: 回充后更新检查方法显示
|
||
updateMethodDisplay();
|
||
// Bug #408修复: 加载申请单详情后自动切换到检查明细页签,确保已加载的明细数据可见
|
||
activeDetailTab.value = 'applyDetail';
|
||
} catch (err) {
|
||
console.error('加载申请单详情失败', err);
|
||
ElMessage.error('加载申请单详情失败');
|
||
}
|
||
}
|
||
}).catch(err => {
|
||
console.error('获取申请单详情失败', err);
|
||
ElMessage.error('获取申请单详情失败');
|
||
});
|
||
}
|
||
|
||
function handlePrint(row) { ElMessage.info('打印申请单:' + row.applyNo); }
|
||
|
||
function handleDelete(row) {
|
||
ElMessageBox.confirm('确认删除该检查申请单吗?', '警告', { type: 'warning' }).then(() => {
|
||
request({ url: `/exam/apply/${row.applyNo}`, method: 'delete' }).then(() => {
|
||
ElMessage.success('删除成功');
|
||
getList();
|
||
if (form.applyNo === row.applyNo) handleAdd();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Bug #428修复: 判断某个检查方法是否已被选中(任意项目关联了该方法)
|
||
function isMethodSelected(method, cat) {
|
||
return selectedItems.value.some(item =>
|
||
item.selectedMethod?.id === method.id && item.checkType === cat.typeName
|
||
);
|
||
}
|
||
|
||
// Bug #428修复: 勾选检查方法
|
||
async function handleMethodSelect(checked, method, cat) {
|
||
if (checked) {
|
||
// 找到该方法所属的第一个检查项目
|
||
const targetItem = cat.items[0];
|
||
if (!targetItem) {
|
||
// 如果分类下没有项目,尝试从其他分类找同名项目或创建
|
||
console.warn('分类下没有检查项目,无法关联方法');
|
||
return;
|
||
}
|
||
|
||
// 如果该项目已存在,只更新 selectedMethod
|
||
const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
|
||
if (existingItem) {
|
||
existingItem.selectedMethod = method;
|
||
// 从方法中获取套餐信息
|
||
if (method.packageId) {
|
||
existingItem.isPackage = true;
|
||
existingItem.packageId = method.packageId;
|
||
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
|
||
// 预加载套餐明细
|
||
loadPackageDetailsForItem(existingItem);
|
||
}
|
||
updateMethodDisplay();
|
||
return;
|
||
}
|
||
|
||
// 如果该项目不存在,创建一个并关联方法
|
||
if (selectedItems.value.length > 0) {
|
||
const currentCategory = selectedItems.value[0].checkType;
|
||
// Bug #428修复: 使用 cat.typeName 进行比较(与 newItem.checkType 保持一致)
|
||
const newCategory = cat.typeName || '';
|
||
if (currentCategory !== newCategory) {
|
||
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const newItem = {
|
||
id: targetItem.id, name: targetItem.name,
|
||
price: targetItem.price, quantity: 1,
|
||
serviceFee: targetItem.serviceFee || 0,
|
||
unit: targetItem.unit || '次',
|
||
applyPart: targetItem.name,
|
||
checkType: cat.typeName,
|
||
nationalCode: targetItem.nationalCode || '',
|
||
checked: true,
|
||
methods: cat.methods || [method], // #428修复: 复制分类下全部方法,允许用户切换
|
||
selectedMethod: method,
|
||
expanded: false,
|
||
// 从方法或项目中获取套餐信息
|
||
isPackage: !!method.packageId || !!targetItem.packageName,
|
||
packageId: method.packageId || targetItem.packageId || null,
|
||
packageName: method.packageName || targetItem.packageName || null // #428修复: 复制 packageName,确保套餐明细可加载
|
||
};
|
||
selectedItems.value.push(newItem);
|
||
|
||
// 如果是套餐,预加载套餐明细
|
||
if (newItem.isPackage && newItem.packageId) {
|
||
loadPackageDetailsForItem(newItem);
|
||
}
|
||
|
||
// 自动回填执行科室
|
||
if (selectedItems.value.length === 1 && cat?.performDeptName) {
|
||
form.performDeptCode = cat.performDeptName;
|
||
}
|
||
|
||
// 同时勾选左侧项目的 checkbox
|
||
targetItem.checked = true;
|
||
|
||
} else {
|
||
// 取消选择方法:将 selectedItems 中关联该方法的项的 selectedMethod 清空
|
||
const itemsWithMethod = selectedItems.value.filter(
|
||
item => item.selectedMethod?.id === method.id
|
||
);
|
||
for (const item of itemsWithMethod) {
|
||
item.selectedMethod = null;
|
||
}
|
||
}
|
||
updateMethodDisplay();
|
||
}
|
||
|
||
// ====== 勾选逻辑 ======
|
||
async function handleItemSelect(checked, item, cat) {
|
||
if (checked) {
|
||
// Bug #384修复: 检查方法表的 checkType 字段关联的是检查类型的 name(中文名称,如"心电图")
|
||
const effectiveCheckType = cat?.typeName || cat?.categoryName || '';
|
||
|
||
// 查询该检查类型对应的检查方法
|
||
let methods = [];
|
||
try {
|
||
if (effectiveCheckType) {
|
||
const res = await searchCheckMethod({ checkType: effectiveCheckType });
|
||
// Bug #384修复: API 返回结构可能是 {data: {data: Array}} 或 {data: Array}
|
||
let data = res?.data?.data || res?.data || res?.rows || res;
|
||
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
|
||
data = res.data.data;
|
||
}
|
||
if (Array.isArray(data)) {
|
||
methods = data.map(m => ({
|
||
id: m.id,
|
||
name: m.name,
|
||
code: m.code,
|
||
price: m.price || item.price, // fallback 到项目价格
|
||
packageName: m.packageName || '',
|
||
packageId: m.packageId || null,
|
||
packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格
|
||
serviceFee: m.serviceFee || null // Bug #384修复: 服务费
|
||
}));
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('加载检查方法失败', err);
|
||
}
|
||
|
||
if (selectedItems.value.length > 0) {
|
||
const currentCategory = selectedItems.value[0].checkType;
|
||
// Bug #428修复: 使用 cat.typeName 进行比较(与 effectiveCheckType 保持一致)
|
||
const newCategory = cat.typeName || '';
|
||
if (currentCategory !== newCategory) {
|
||
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
|
||
item.checked = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
const newRow = {
|
||
id: item.id, name: item.name,
|
||
price: item.price, quantity: 1,
|
||
serviceFee: item.serviceFee || 0,
|
||
unit: item.unit || '次',
|
||
applyPart: item.name,
|
||
checkType: effectiveCheckType, // Bug #384修复: 使用有效的 checkType
|
||
nationalCode: item.nationalCode || '',
|
||
checked: true,
|
||
methods: methods,
|
||
selectedMethod: null,
|
||
expanded: false,
|
||
isPackage: !!(item.packageId || item.packageName),
|
||
packageName: item.packageName || null,
|
||
packageDetailsLoading: false,
|
||
packageId: item.packageId || null
|
||
};
|
||
selectedItems.value.push(newRow);
|
||
// 必须用数组里的响应式行,不能继续改局部 newRow:push 后列表内是 proxy,改 raw 对象不会触发右侧卡片更新(会一直卡在「加载中」)
|
||
const row = selectedItems.value[selectedItems.value.length - 1];
|
||
|
||
// 右侧不再展示「检查方法」列表:自动选默认方法(保存、计价仍依赖 selectedMethod)
|
||
if (methods.length >= 1) {
|
||
row.selectedMethod = pickDefaultMethod(methods, item);
|
||
}
|
||
updateMethodDisplay();
|
||
|
||
// 有套餐 ID 时默认展开(先显示加载区,明细写入行对象 packageDetailsDisplay)
|
||
row.expanded = !!getPackageCarrier(row)?.packageId;
|
||
if (getPackageCarrier(row)?.packageId) {
|
||
await loadPackageDetailsForItem(row);
|
||
}
|
||
|
||
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
|
||
if (selectedItems.value.length === 1 && cat?.performDeptName) {
|
||
form.performDeptCode = cat.performDeptName;
|
||
} else if (!form.performDeptCode && cat?.performDeptName) {
|
||
form.performDeptCode = cat.performDeptName;
|
||
}
|
||
|
||
} else {
|
||
const idx = selectedItems.value.findIndex(s => s.id === item.id);
|
||
if (idx > -1) selectedItems.value.splice(idx, 1);
|
||
|
||
if (selectedItems.value.length === 0) {
|
||
form.performDeptCode = '';
|
||
form.examTypeCode = '';
|
||
}
|
||
}
|
||
// Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
|
||
}
|
||
|
||
// Bug #384修复 + #426修复: 展开/收起项目卡片
|
||
async function toggleItemExpand(item) {
|
||
item.expanded = !item.expanded;
|
||
if (item.expanded && (item.isPackage || item.packageName) && (!item.packageDetails || item.packageDetails.length === 0) && !item.packageDetailsLoading) {
|
||
await loadPackageDetailsForItem(item);
|
||
}
|
||
if (item.expanded && shouldShowPackageBody(item)) {
|
||
if (getPackageDetailsList(item).length === 0 && !item.packageDetailsLoading) {
|
||
await loadPackageDetailsForItem(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Bug #384修复: 勾选框选择检查方法(单选逻辑)
|
||
async function selectMethodCheckbox(checked, item, method) {
|
||
if (checked) {
|
||
item.selectedMethod = method;
|
||
if (item.expanded && method.packageId) {
|
||
loadPackageDetailsForItem(item);
|
||
}
|
||
// 动态加载该方法对应的套餐明细
|
||
await loadMethodPackageDetails(item, method);
|
||
} else {
|
||
item.selectedMethod = null;
|
||
item.methodPackageDetails = [];
|
||
}
|
||
// 联动更新表单检查方法显示字段
|
||
updateMethodDisplay();
|
||
|
||
// #430: 套餐金额实时同步到申请单
|
||
nextTick(() => {
|
||
form.totalAmount = totalAmountCalc.value;
|
||
});
|
||
}
|
||
|
||
// 根据检查方法的packageName加载对应的套餐明细
|
||
async function loadMethodPackageDetails(item, method) {
|
||
item.methodPackageLoading = true;
|
||
item.methodPackageDetails = [];
|
||
try {
|
||
if (!method.packageName) {
|
||
item.methodPackageLoading = false;
|
||
return;
|
||
}
|
||
// 通过packageName查询套餐获取packageId
|
||
const pkgRes = await listCheckPackage({ packageName: method.packageName });
|
||
let packages = pkgRes?.data || [];
|
||
if (!Array.isArray(packages)) {
|
||
packages = packages.records || packages.data || [];
|
||
}
|
||
if (packages.length === 0) {
|
||
item.methodPackageLoading = false;
|
||
return;
|
||
}
|
||
const packageId = packages[0].id;
|
||
// 查询套餐明细
|
||
const detailRes = await request({
|
||
url: `/system/package/${packageId}/details`,
|
||
method: 'get'
|
||
});
|
||
if (detailRes.code === 200 && detailRes.data) {
|
||
item.methodPackageDetails = detailRes.data.map(d => ({
|
||
id: d.id,
|
||
name: d.itemName || d.name,
|
||
quantity: d.quantity || 1,
|
||
unit: d.unit || '次',
|
||
price: d.unitPrice || d.price || 0,
|
||
amount: d.amount || d.total || 0,
|
||
checked: true // 默认勾选
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
console.error('加载方法套餐明细失败:', err);
|
||
item.methodPackageDetails = [];
|
||
} finally {
|
||
item.methodPackageLoading = false;
|
||
}
|
||
}
|
||
|
||
/** 检查明细表格中切换检查方法 */
|
||
async function onDetailMethodChange(row, val) {
|
||
row.selectedMethod = val || null;
|
||
if (val?.packageId) {
|
||
row.packageId = val.packageId;
|
||
row.isPackage = true;
|
||
}
|
||
row.packageDetailsDisplay = undefined;
|
||
const carrier = getPackageCarrier(row);
|
||
if (carrier) {
|
||
carrier.packageDetails = undefined;
|
||
}
|
||
updateMethodDisplay();
|
||
row.expanded = !!getPackageCarrier(row)?.packageId;
|
||
if (getPackageCarrier(row)?.packageId) {
|
||
await loadPackageDetailsForItem(row);
|
||
}
|
||
nextTick(() => {
|
||
form.totalAmount = totalAmountCalc.value;
|
||
});
|
||
}
|
||
|
||
// Bug #384修复: 更新检查方法显示字段(联动)
|
||
function updateMethodDisplay() {
|
||
// 找到第一个有选中检查方法的项目
|
||
const itemWithMethod = selectedItems.value.find(item => item.selectedMethod);
|
||
if (itemWithMethod?.selectedMethod) {
|
||
form.selectedMethodDisplay = itemWithMethod.selectedMethod.name; // 显示检查方法名称,不显示套餐名称
|
||
} else {
|
||
form.selectedMethodDisplay = '';
|
||
}
|
||
}
|
||
|
||
// 选择检查方法
|
||
function selectMethod(item, method) {
|
||
if (item.selectedMethod?.id === method.id) {
|
||
item.selectedMethod = null;
|
||
} else {
|
||
item.selectedMethod = method;
|
||
}
|
||
// Bug #384修复: 联动更新表单检查方法显示字段
|
||
updateMethodDisplay();
|
||
}
|
||
|
||
function handleRemoveItem(idx, item) {
|
||
selectedItems.value.splice(idx, 1);
|
||
// 取消对应 category 中的 checkbox
|
||
for (const cat of categoryList.value) {
|
||
const found = cat.items.find(x => x.id === item.id);
|
||
if (found) { found.checked = false; break; }
|
||
}
|
||
|
||
if (selectedItems.value.length === 0) {
|
||
form.performDeptCode = '';
|
||
form.examTypeCode = '';
|
||
form.selectedMethodDisplay = ''; // Bug #384修复: 清空检查方法显示
|
||
} else {
|
||
// Bug #384修复: 移除后重新计算检查方法显示
|
||
updateMethodDisplay();
|
||
}
|
||
}
|
||
|
||
function resetCategoryChecked() {
|
||
for (const cat of categoryList.value)
|
||
for (const item of cat.items) item.checked = false;
|
||
}
|
||
|
||
function syncCategoryChecked() {
|
||
resetCategoryChecked();
|
||
const ids = new Set(selectedItems.value.map(s => s.id));
|
||
for (const cat of categoryList.value)
|
||
for (const item of cat.items)
|
||
if (ids.has(item.id)) item.checked = true;
|
||
}
|
||
|
||
defineExpose({ getList });
|
||
</script>
|
||
|
||
<style scoped>
|
||
.exam-app-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding: 8px;
|
||
height: 100%;
|
||
background: #f0f2f5;
|
||
}
|
||
|
||
/* 顶部申请单列表 */
|
||
.top-section {
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
padding: 10px 12px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* Bug #499: 查询过滤工具栏 */
|
||
.search-toolbar {
|
||
margin-bottom: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
.search-toolbar :deep(.el-form-item) {
|
||
margin-bottom: 8px;
|
||
}
|
||
.search-toolbar :deep(.el-form-item__label) {
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 底部区域:左表单 + 右分类 */
|
||
.bottom-section {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 左:表单面板 */
|
||
.form-panel {
|
||
flex: 1;
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
padding: 10px 12px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
overflow-y: auto;
|
||
min-width: 0;
|
||
}
|
||
.form-tabs :deep(.el-tabs__header) {
|
||
margin-bottom: 10px;
|
||
}
|
||
.apply-form :deep(.el-form-item) {
|
||
margin-bottom: 10px;
|
||
}
|
||
.apply-form :deep(.el-form-item__label) {
|
||
font-size: 12px;
|
||
}
|
||
.total-row {
|
||
margin-top: 8px;
|
||
text-align: right;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
.total-amount {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
color: #f56c6c;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
/* 右:分类面板 */
|
||
.category-panel {
|
||
width: 420px;
|
||
flex-shrink: 0;
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
padding: 10px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.panel-top {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
.category-left {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.panel-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 6px;
|
||
}
|
||
.search-input {
|
||
margin-bottom: 8px;
|
||
}
|
||
.collapse-scroll {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden; /* Bug #500: 防止切换时水平方向溢出导致抖动 */
|
||
min-height: 120px; /* Bug #500: 固定最小高度,避免分类切换时 flex 容器高度突变 */
|
||
}
|
||
.empty-hint {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
/* 检查项目分类折叠 */
|
||
.cat-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
/* Bug #500: 分类加载中的小圆点动画 */
|
||
.loading-dot {
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #409eff;
|
||
margin-left: 6px;
|
||
animation: dotPulse 1s ease-in-out infinite;
|
||
}
|
||
@keyframes dotPulse {
|
||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||
50% { opacity: 1; transform: scale(1.2); }
|
||
}
|
||
.item-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 3px 4px;
|
||
}
|
||
.item-row:hover {
|
||
background: #f5f7fa;
|
||
}
|
||
.item-checkbox {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
.item-checkbox :deep(.el-checkbox__label) {
|
||
font-size: 12px;
|
||
color: #303133;
|
||
}
|
||
.item-price {
|
||
font-size: 12px;
|
||
color: #1890FF;
|
||
flex-shrink: 0;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
/* Bug #428修复: 分类下检查方法区域样式 */
|
||
.method-section {
|
||
padding: 6px 8px;
|
||
background: #f0f7ff;
|
||
border-radius: 4px;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.method-section-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #409eff;
|
||
margin-bottom: 4px;
|
||
padding-bottom: 3px;
|
||
border-bottom: 1px dashed #d9ecff;
|
||
}
|
||
|
||
.method-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 3px 4px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.method-row:hover {
|
||
background: #e8f4ff;
|
||
}
|
||
|
||
.method-checkbox {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.method-checkbox :deep(.el-checkbox__label) {
|
||
font-size: 12px;
|
||
color: #303133;
|
||
}
|
||
|
||
.method-price-tag {
|
||
font-size: 11px;
|
||
color: #e6a23c;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
/* 已选择 tags */
|
||
/* 已选择:加宽,避免套餐明细挤成一团 */
|
||
.selected-panel {
|
||
width: 220px;
|
||
min-width: 200px;
|
||
max-width: 280px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.selected-tags {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding-right: 2px;
|
||
}
|
||
.selected-tag {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.empty-selected {
|
||
color: #c0c4cc;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 已选择项目卡片 */
|
||
.selected-item-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
border: 1px solid #e4e7ed;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.selected-item-card .card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 10px;
|
||
cursor: pointer;
|
||
gap: 8px;
|
||
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
|
||
border-bottom: 1px solid transparent;
|
||
}
|
||
|
||
.selected-item-card .card-header:hover {
|
||
background: linear-gradient(180deg, #ecf5ff 0%, #e3eef8 100%);
|
||
}
|
||
|
||
.selected-item-card.is-expanded .card-header {
|
||
border-bottom-color: #ebeef5;
|
||
}
|
||
|
||
.card-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-price {
|
||
font-size: 13px;
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.expand-icon {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
transition: transform 0.2s ease;
|
||
flex-shrink: 0;
|
||
transition: transform 0.2s;
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
.expand-icon.expanded {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
/* Bug #428修复: 套餐明细列表样式 */
|
||
.package-details-list {
|
||
padding: 6px 10px;
|
||
background: #fffbe6;
|
||
border-top: 1px solid #ffe58f;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
background: #fff;
|
||
}
|
||
|
||
.detail-row:hover {
|
||
background: #fff9e6;
|
||
}
|
||
|
||
.detail-name {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.detail-info {
|
||
color: #909399;
|
||
font-size: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 展开区域 */
|
||
.selected-card-body {
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.package-details-loading,
|
||
.package-details-empty {
|
||
padding: 12px 10px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
text-align: center;
|
||
}
|
||
|
||
.package-details-empty {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.package-details-list {
|
||
padding: 10px 10px 12px;
|
||
}
|
||
|
||
.package-details-head {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #909399;
|
||
letter-spacing: 0.02em;
|
||
margin-bottom: 8px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px dashed #dcdfe6;
|
||
}
|
||
|
||
.detail-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 8px 12px;
|
||
align-items: start;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.detail-row:last-of-type {
|
||
border-bottom: none;
|
||
padding-bottom: 2px;
|
||
}
|
||
|
||
.detail-name {
|
||
font-size: 12px;
|
||
color: #303133;
|
||
line-height: 1.5;
|
||
word-break: break-word;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.detail-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
text-align: right;
|
||
}
|
||
|
||
.detail-qty {
|
||
font-size: 11px;
|
||
color: #909399;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.detail-price {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #e6a23c;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* 折叠组件细节 */
|
||
:deep(.el-collapse) {
|
||
border: none;
|
||
}
|
||
:deep(.el-collapse-item__header) {
|
||
font-size: 13px;
|
||
padding: 6px 0;
|
||
height: auto;
|
||
line-height: 1.5;
|
||
}
|
||
/* Bug #500修复: 折叠内容使用明确属性过渡,避免 transition: all 导致子元素意外动画 */
|
||
:deep(.el-collapse-item__content) {
|
||
padding-bottom: 4px;
|
||
transition: height 0.3s ease, max-height 0.3s ease;
|
||
}
|
||
/* Bug #500: 折叠面板动画容器,添加 overflow:hidden 防止展开时内容溢出导致闪烁 */
|
||
:deep(.el-collapse-item__wrap) {
|
||
border: none;
|
||
overflow: hidden;
|
||
}
|
||
:deep(.el-collapse-item) {
|
||
transition: margin 0.2s ease;
|
||
}
|
||
/* Bug #500: 分类加载中提示样式 */
|
||
.category-loading-hint {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
padding: 8px 0;
|
||
}
|
||
</style>
|