Files
his/openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue

2904 lines
89 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<el-container class="inspection-application-container">
<el-header class="top-action-bar" height="48px">
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
<el-icon><Document /></el-icon>
保存
</el-button>
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
</el-row>
</el-header>
<!-- 检验信息表格区 -->
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
<el-card class="table-card" style="width: 100%">
<template #header>
<div class="card-header-flex">
<div class="header-title">
<el-icon><DocumentChecked /></el-icon>
<span>检验信息</span>
</div>
<div class="header-actions">
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
<el-icon><Document /></el-icon>
保存
</el-button>
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
</div>
</template>
<el-table
ref="inspectionTableRef"
:data="inspectionList"
border
stripe
size="small"
max-height="280px"
style="width: 100%; min-width: 100%"
class="inspection-table"
highlight-current-row
row-key="applicationId"
@selection-change="handleSelectionChange"
@current-change="handleRowClick"
@cell-click="handleCellClick"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column label="申请 ID" prop="applicationId" width="80" align="center" header-align="center">
<template #default="scope">
<span>{{ scope.row.applicationId || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
<template #default="scope">
<span>{{ scope.row.itemName }}</span>
</template>
</el-table-column>
<el-table-column label="申请医生" prop="applyDocName" width="120" align="center" header-align="center" />
<el-table-column label="急" width="60" align="center" header-align="center">
<template #default="scope">
<el-icon v-if="scope.row.priorityCode == 1" color="#409EFF" :size="18"><Check /></el-icon>
</template>
</el-table-column>
<el-table-column label="收费" width="60" align="center" header-align="center">
<template #default="scope">
<el-icon v-if="scope.row.applyStatus == 1" color="#409EFF" :size="18"><Check /></el-icon>
</template>
</el-table-column>
<el-table-column label="退费" width="60" align="center" header-align="center">
<template #default="scope">
<el-icon v-if="scope.row.needRefund" color="#409EFF" :size="18"><Check /></el-icon>
</template>
</el-table-column>
<el-table-column label="执行" width="60" align="center" header-align="center">
<template #default="scope">
<el-icon v-if="scope.row.needExecute" color="#409EFF" :size="18"><Check /></el-icon>
</template>
</el-table-column>
<el-table-column label="金额" prop="amount" width="90" align="center" header-align="center">
<template #default="scope">
¥{{ formatAmount(scope.row.itemAmount) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" header-align="center">
<template #default="scope">
<el-row type="flex" align="middle" justify="center" :gutter="8">
<el-button link size="default" @click="handlePrint(scope.row)" :icon="Printer" title="打印" style="font-size: 16px"></el-button>
<el-button link size="default" @click.stop="handleDelete(scope.row)" :icon="Delete" style="color: #f56c6c; font-size: 16px" title="删除"></el-button>
</el-row>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="width: 100%; text-align: center; margin-top: 8px;">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="prev, pager, next"
:pager-count="5"
:hide-on-single-page="true"
small
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
style="display: inline-flex;"
/>
</div>
</el-card>
</el-main>
<!-- 底部表单与项目选择区 -->
<el-main class="bottom-content-area">
<el-row :gutter="12">
<!-- 左侧申请单表单区60% -->
<el-col :span="14" class="form-area">
<el-card class="form-card" style="width: 100%">
<el-tabs v-model="leftActiveTab" class="form-tabs">
<el-tab-pane label="申请单" name="application">
<el-form class="application-form" :model="formData" label-width="auto">
<el-form-item label="申请单号" style="margin-bottom: 2px">
<el-input v-model="formData.applyNo" disabled size="small" />
</el-form-item>
<!-- 患者信息行 -->
<el-row :gutter="12" style="margin-bottom: 0">
<el-col :span="8">
<el-form-item label="姓名" required style="margin-bottom: 4px">
<el-input v-model="formData.patientName" readonly size="small" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="就诊卡号" required style="margin-bottom: 4px">
<el-input v-model="formData.medicalrecordNumber" readonly size="small" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="费用性质" required style="margin-bottom: 4px">
<el-select v-model="formData.natureofCost" placeholder="请选择费用性质" size="small" style="width: 100%">
<el-option label="自费医疗" value="self" />
<el-option label="医保" value="medical" />
<el-option label="公费医疗" value="public" />
<el-option label="商业保险" value="commercial" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 申请信息行 -->
<el-row :gutter="12" style="margin-bottom: 0">
<!--申请日期-->
<el-col :span="8">
<el-form-item label="申请日期" required style="margin-bottom: 4px">
<el-input
v-model="formData.applyTime"
readonly
size="small"
/>
</el-form-item>
</el-col>
<!--申请科室-->
<el-col :span="8">
<el-form-item label="申请科室" required style="margin-bottom: 4px">
<el-input v-model="formData.applyDepartment" readonly size="small" />
</el-form-item>
</el-col>
<!--申请医生-->
<el-col :span="8">
<el-form-item label="申请医生" required style="margin-bottom: 4px">
<el-input v-model="formData.applyDocName" readonly size="small" />
</el-form-item>
</el-col>
</el-row>
<!-- 执行科室 -->
<el-form-item
label="执行科室"
required
style="margin-bottom: 4px"
:class="{ 'form-item-error': validationErrors.executeDepartment }"
:error="validationErrors.executeDepartment ? '请选择执行科室' : ''"
>
<el-select
v-model="formData.executeDepartment"
placeholder="请选择执行科室"
size="small"
style="width: 100%"
:class="{ 'is-error': validationErrors.executeDepartment }"
filterable
>
<el-option
v-for="item in executeDepartmentOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 诊断描述与临床诊断 -->
<el-row :gutter="12" style="margin-bottom: 0">
<el-col :span="12">
<el-form-item
label="诊断描述"
required
style="margin-bottom: 4px"
:class="{ 'form-item-error': validationErrors.clinicDesc }"
:error="validationErrors.clinicDesc ? '请输入诊断描述' : ''"
>
<el-input
v-model="formData.clinicDesc"
type="textarea"
:rows="1"
size="small"
:class="{ 'is-error': validationErrors.clinicDesc }"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="临床诊断"
required
style="margin-bottom: 4px"
:class="{ 'form-item-error': validationErrors.clinicDiag }"
:error="validationErrors.clinicDiag ? '请输入临床诊断' : ''"
>
<el-input
v-model="formData.clinicDiag"
type="textarea"
:rows="1"
size="small"
:class="{ 'is-error': validationErrors.clinicDiag }"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 病史摘要与检验目的 -->
<el-row :gutter="12" style="margin-bottom: 0">
<el-col :span="12">
<el-form-item
label="病史摘要"
required
style="margin-bottom: 4px"
:class="{ 'form-item-error': validationErrors.medicalHistorySummary }"
:error="validationErrors.medicalHistorySummary ? '请输入病史摘要' : ''"
>
<el-input
v-model="formData.medicalHistorySummary"
type="textarea"
:rows="1"
size="small"
:class="{ 'is-error': validationErrors.medicalHistorySummary }"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="检验目的"
required
style="margin-bottom: 4px"
:class="{ 'form-item-error': validationErrors.purposeofInspection }"
:error="validationErrors.purposeofInspection ? '请输入检验目的' : ''"
>
<el-input
v-model="formData.purposeofInspection"
type="textarea"
:rows="1"
size="small"
:class="{ 'is-error': validationErrors.purposeofInspection }"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 禁忌症与体格检查 -->
<el-row :gutter="12" style="margin-bottom: 0">
<el-col :span="12">
<el-form-item label="禁忌症" style="margin-bottom: 4px">
<el-input v-model="formData.contraindication" size="small" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="体格检查" style="margin-bottom: 4px">
<el-input v-model="formData.physicalExam" size="small" />
</el-form-item>
</el-col>
</el-row>
<!-- 检验项目和备注 -->
<el-row :gutter="12" style="margin-bottom: 0">
<el-col :span="12">
<el-form-item label="检验项目" style="margin-bottom: 4px">
<el-input v-model="formData.inspectionItemsText" size="small" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" style="margin-bottom: 4px">
<el-input v-model="formData.applyRemark" size="small" />
</el-form-item>
</el-col>
</el-row>
<!-- 状态复选框组 -->
<el-card style="margin-bottom: 4px; padding: 8px 12px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef" shadow="never">
<template #header>
<span style="font-weight: bold; color: #1a2b6d; font-size: 13px">
状态设置
</span>
</template>
<el-row type="flex" :gutter="12" wrap>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<!-- 只有急标记能编辑 -->
<el-checkbox v-model="formData.priorityCode" :true-value="1" :false-value="0"></el-checkbox>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<!-- 收费标记默认不勾选并不可编辑 -->
<el-checkbox v-model="formData.applyStatus" :true-value="1" :false-value="0" disabled>收费</el-checkbox>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<!-- 退费标记默认不勾选并不可编辑 -->
<el-checkbox v-model="formData.needRefund" :true-value="true" :false-value="false" disabled>退费</el-checkbox>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<!-- 执行标记默认不勾选并不可编辑 -->
<el-checkbox v-model="formData.needExecute" :true-value="true" :false-value="false" disabled>执行</el-checkbox>
</el-col>
</el-row>
</el-card>
</el-form>
</el-tab-pane>
<el-tab-pane label="检验信息" name="inspectionInfo">
<el-card style="padding: 10px; overflow-y: auto; border: 1px solid #e4e7ed; border-radius: 4px; margin: 5px; width: 100%">
<el-form :model="formData" label-width="100px" style="margin-bottom: 10px">
<el-row :gutter="15">
<el-col :span="12">
<el-form-item label="检验医生">
<el-input v-model="formData.inspectionDoctor" size="small" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="检验时间">
<el-date-picker
v-model="formData.inspectionTime"
type="datetime"
placeholder="选择时间"
size="small"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核医生">
<el-input v-model="formData.auditDoctor" size="small" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核时间">
<el-date-picker
v-model="formData.auditTime"
type="datetime"
placeholder="选择时间"
size="small"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 检验信息详情表格 -->
<el-card style="margin-top: 10px; width: 100%" shadow="never">
<template #header>
<h4 style="margin: 0; font-weight: bold">检验信息详情</h4>
</template>
<el-table
:data="selectedInspectionItems"
border
size="small"
style="width: 100%; min-width: 100%"
max-height="250"
row-key="itemId"
lazy
:load="loadPackageDetailsForTable"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column label="项目名称" prop="itemName" min-width="180">
<template #default="scope">
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span :style="{ fontWeight: scope.row.isPackage ? 'bold' : 'normal' }">
{{ scope.row.itemName }}
</span>
</template>
</el-table-column>
<el-table-column label="样本类型" prop="sampleType" width="80" align="center" />
<el-table-column label="单位" prop="unit" width="60" align="center" />
<el-table-column label="总量" prop="itemQty" width="60" align="center">
<template #default="scope">
<span>{{ scope.row.itemQty || 1 }}</span>
</template>
</el-table-column>
<el-table-column label="单价" prop="itemPrice" width="70" align="right">
<template #default="scope">
<span v-if="scope.row.itemPrice">¥{{ scope.row.itemPrice }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="金额" prop="itemAmount" width="70" align="right">
<template #default="scope">
<span v-if="scope.row.itemAmount">¥{{ scope.row.itemAmount }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="服务费" width="70" align="right">
<template #default="scope">
¥{{ scope.row.serviceFee || 0 }}
</template>
</el-table-column>
<el-table-column label="类型" prop="type" width="60" align="center" />
<el-table-column label="自费" width="50" align="center">
<template #default="scope">
<el-checkbox :model-value="scope.row.isSelfPay" disabled />
</template>
</el-table-column>
</el-table>
</el-card>
</el-card>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
<!-- 右侧项目选择区40% -->
<el-col :span="10" class="selection-area">
<!-- 检验项目选择区上部50% -->
<el-card class="inspection-selector" v-loading="inspectionLoading" element-loading-text="正在加载检验项目...">
<template #header>
<span class="card-title">检验项目选择</span>
</template>
<!-- 搜索框自动完成 -->
<el-autocomplete
v-model="searchKeyword"
:fetch-suggestions="querySearchInspectionItems"
placeholder="搜索检验项目..."
size="small"
clearable
prefix-icon="Search"
@select="handleSearchSelect"
@clear="handleSearchClear"
value-key="itemName"
class="search-input"
:debounce="300"
>
<template #default="{ item }">
<div class="suggestion-item">
<span class="suggestion-name">{{ item.itemName }}</span>
<el-tag size="small" type="info" class="suggestion-category">{{ item.typeName || '检验' }}</el-tag>
<span class="suggestion-price">¥{{ item.itemPrice }}</span>
</div>
</template>
</el-autocomplete>
<!-- 分类树 -->
<el-scrollbar
class="category-tree"
style="max-height: 220px"
@scroll="handleScroll"
>
<!-- 无数据提示 -->
<el-empty v-if="!inspectionLoading && inspectionCategories.length === 0" description="暂无检验项目数据" :image-size="80" />
<!-- 数据列表 -->
<div
v-for="category in inspectionCategories"
:key="category.key"
class="category-tree-item"
>
<div
:class="['category-tree-header', { active: activeCategory === category.key }]"
@click="switchCategory(category.key)"
>
<span class="category-tree-icon">{{ category.expanded ? '▼' : '▶' }}</span>
<span>{{ category.label }}</span>
<span class="category-count">({{ category.total || category.items.length }})</span>
<!-- 加载状态图标 -->
<el-icon v-if="category.loading" class="is-loading" style="margin-left: 8px; color: #409eff;">
<Loading />
</el-icon>
</div>
<div v-if="category.expanded" class="category-tree-children">
<!-- 加载中占位 -->
<div v-if="category.loading && category.items.length === 0" class="loading-placeholder">
<el-icon class="is-loading" style="margin-right: 8px;"><Loading /></el-icon>
<span>加载中...</span>
</div>
<!-- 项目列表 -->
<div
v-for="item in getFilteredItems(category.key)"
:key="item.itemId"
:class="['inspection-tree-item', { selected: isItemSelected(item) }]"
@click="handleItemClick(item)"
>
<el-checkbox
:model-value="isItemSelected(item)"
@change="toggleInspectionItem(item)"
@click.stop
/>
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
</div>
<!-- 加载更多 -->
<div v-if="category.hasMore && category.items.length > 0" class="load-more">
<el-button
link
size="small"
:loading="category.loading"
@click.stop="loadMoreItems(category.key)"
>
{{ category.loading ? '加载中...' : '加载更多' }}
</el-button>
<span class="load-info">(已加载 {{ category.items.length }}/{{ category.total }} )</span>
</div>
<!-- 加载完成提示 -->
<div v-if="!category.hasMore && category.items.length > 0" class="no-more">
已全部加载 ( {{ category.total }} )
</div>
</div>
</div>
</el-scrollbar>
</el-card>
<!-- 下部已选项目区 -->
<el-card class="selected-items-area">
<template #header>
<el-row class="selected-header" type="flex" justify="space-between" align="middle">
<span class="card-title">已选择</span>
<el-button link @click="clearAllSelected" type="danger" size="small">清空</el-button>
</el-row>
</template>
<!-- 已选项目列表 -->
<el-scrollbar class="selected-tree" style="max-height: 220px">
<div v-if="selectedInspectionItems.length > 0" class="selected-items-list">
<div
v-for="item in selectedInspectionItems"
:key="item.itemId"
:class="['selected-tree-item', { 'is-package': item.isPackage }]"
>
<!-- 项目行仿照项目选择区样式 -->
<div
:class="['selected-tree-header', { expanded: item.expanded }]"
@click="item.isPackage ? togglePackageExpand(item) : null"
>
<span class="selected-tree-icon">
<template v-if="item.isPackage">{{ item.expanded ? '' : '' }}</template>
<template v-else></template>
</span>
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
<el-button
link
size="small"
style="color: #f56c6c; margin-left: auto"
@click.stop="removeInspectionItem(item)"
>
删除
</el-button>
</div>
<!-- 套餐明细展开区仿照项目选择区children样式 -->
<div v-if="item.isPackage && item.expanded" class="selected-tree-children">
<!-- 加载中 -->
<div v-if="item.loading" class="loading-placeholder">
<el-icon class="is-loading" style="margin-right: 8px;"><Loading /></el-icon>
<span>加载中...</span>
</div>
<!-- 明细列表 -->
<template v-else-if="item.children && item.children.length > 0">
<div
v-for="child in item.children"
:key="child.detailId || child.itemName"
class="selected-tree-detail"
>
<span class="detail-name">{{ child.itemName }}</span>
<span class="detail-unit">{{ child.unit || '-' }}</span>
<span class="detail-qty">×{{ child.quantity || 1 }}</span>
<span class="detail-price">¥{{ child.unitPrice || child.itemPrice || 0 }}</span>
</div>
</template>
<div v-else class="no-detail">暂无明细</div>
</div>
</div>
</div>
<el-empty v-if="selectedInspectionItems.length === 0" class="no-selection" description="暂无选择项目" />
</el-scrollbar>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup>
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
import {
deleteInspectionApplication, getApplyList,
saveInspectionApplication,
getInspectionTypeList,
getInspectionTypeDetail,
getInspectionItemList,
getEncounterDiagnosis,
getInspectionApplyDetail,
getOrgTree,
getInspectionPackageDetails
} from '../api'
import { getLabActivityDefinitionPage } from '@/api/lab/labActivityDefinition'
import useUserStore from '@/store/modules/user.js'
// 迁移到 hiprint
import { previewPrint } from '@/utils/printUtils.js'
import {storeToRefs} from 'pinia'
import { debounce } from 'lodash-es'
// 获取当前组件实例和字典
const { proxy } = getCurrentInstance()
// Bug #329: 移除 inspection_lab_dept 字典,执行科室数据从 Organization 表获取
// 动态获取的检验类型缓存(用于缓存不在 inspectionCategories 中的检验类型)
const dynamicInspectionTypesCache = ref([])
// 根据检验类型获取执行科室默认值(支持动态获取缺失的检验类型)
// BugFix#CodeReview: 清理调试日志,仅保留必要的 warn/error 级别日志
const getDefaultPerformDeptCode = async (inspectionTypeId) => {
if (!inspectionTypeId) return ''
// 第一步:尝试从 inspectionCategories 中查找
const category = inspectionCategories.value.find(c =>
c.typeId === parseInt(inspectionTypeId) || c.typeId === inspectionTypeId.toString()
)
// 第二步:如果找不到,尝试从动态缓存中查找
let targetTypeInfo = null
if (category) {
targetTypeInfo = {
id: category.typeId,
name: category.label,
department: category.performDeptCode
}
} else {
// 从动态缓存中查找
const cachedType = dynamicInspectionTypesCache.value.find(t =>
t.id === parseInt(inspectionTypeId) || t.id === inspectionTypeId.toString()
)
if (cachedType) {
targetTypeInfo = cachedType
} else {
// 第三步如果缓存中也没有动态调用API获取检验类型详情
console.warn('未找到检验类型 ID:', inspectionTypeId, ',尝试动态获取')
try {
const res = await getInspectionTypeDetail(inspectionTypeId)
if (res.code === 200 && res.data) {
const typeInfo = res.data
// 添加到动态缓存BugFix#CodeReview: 限制缓存大小)
if (dynamicInspectionTypesCache.value.length >= 50) {
dynamicInspectionTypesCache.value.shift() // 移除最旧的
}
dynamicInspectionTypesCache.value.push({
id: typeInfo.id,
name: typeInfo.name,
department: typeInfo.department
})
targetTypeInfo = typeInfo
} else {
console.error('获取检验类型详情失败:', res)
return ''
}
} catch (error) {
console.error('动态获取检验类型详情异常:', error)
return ''
}
}
}
if (!targetTypeInfo) {
console.warn('无法获取检验类型信息ID:', inspectionTypeId)
return ''
}
const performDeptCode = targetTypeInfo.department || ''
if (!performDeptCode) {
console.warn('检验类型没有设置所属科室ID:', inspectionTypeId, '名称:', targetTypeInfo.name)
return ''
}
// 使用检验类型的所属科室名称匹配执行科室选项
// 精确匹配
const deptOption = executeDepartmentOptions.value.find(opt =>
opt.label === performDeptCode
)
if (deptOption) {
return deptOption.value
}
// 包含匹配
const deptOptionInclude = executeDepartmentOptions.value.find(opt =>
opt.label.includes(performDeptCode) || performDeptCode.includes(opt.label)
)
if (deptOptionInclude) {
return deptOptionInclude.value
}
// 去除或添加"科"字匹配
const deptWithoutKe = performDeptCode.replace(/科$/, '')
const deptWithKe = performDeptCode + '科'
const deptOptionWithModified = executeDepartmentOptions.value.find(opt =>
opt.label === deptWithoutKe || opt.label === deptWithKe ||
opt.label.includes(deptWithoutKe) || opt.label.includes(deptWithKe)
)
if (deptOptionWithModified) {
return deptOptionWithModified.value
}
console.warn('未找到匹配的执行科室performDeptCode:', performDeptCode)
return ''
}
// Props
const props = defineProps({
patientInfo: {
type: Object,
required: true
},
activeTab: {
type: String
}
})
// Emits
const emit = defineEmits(['save'])
// 响应式数据
const loading = ref(false)
const saving = ref(false) // 保存状态
const total = ref(0)
const leftActiveTab = ref('application')
/**
* 加载套餐明细(公共函数)
* @param {string|number} packageId 套餐ID
* @returns {Promise<Array>} 明细数组
*/
const fetchPackageDetails = async (packageId) => {
if (!packageId) return []
try {
const res = await getInspectionPackageDetails(packageId)
if (res.code === 200 && res.data) {
return res.data.map(detail => {
const detailId = detail.detailId || detail.id || detail.itemId
const qty = detail.quantity || detail.itemQty || detail.qty || 1
const price = detail.unitPrice || detail.itemPrice || detail.price || 0
return {
detailId: detailId,
itemId: detailId, // 兼容表格 row-key
itemName: detail.itemName || detail.name,
sampleType: detail.sampleType || '',
unit: detail.unit || '',
quantity: qty,
itemQty: qty, // 兼容表格"总量"列
unitPrice: price,
itemPrice: price, // 兼容表格"单价"列
itemAmount: price * qty
}
})
}
return []
} catch (error) {
console.error('加载套餐明细失败:', error)
return []
}
}
const loadPackageDetailsForTable = async (row, treeNode, resolve) => {
if (!row.isPackage) {
resolve([])
return
}
const packageId = row.feePackageId || row.packageId
const children = await fetchPackageDetails(packageId)
resolve(children)
}
const togglePackageExpand = async (item) => {
if (!item.isPackage) return
item.expanded = !item.expanded
if (item.expanded && (!item.children || item.children.length === 0)) {
item.loading = true
const packageId = item.feePackageId || item.packageId
item.children = await fetchPackageDetails(packageId)
item.loading = false
}
}
// 申请日期实时更新定时器
let applyTimeTimer = null
// 用户信息store
const userStore = useUserStore()
const { id: userId, name: userName, nickName: userNickName } = storeToRefs(userStore)
// 修改 initData 函数
const initData = async () => {
// 先初始化患者信息(如果有)
if (props.patientInfo && props.patientInfo.encounterId) {
queryParams.encounterId = props.patientInfo.encounterId
formData.visitNo = props.patientInfo.busNo || ''
formData.patientId = props.patientInfo.patientId || ''
formData.patientName = props.patientInfo.patientName || ''
formData.medicalrecordNumber = props.patientInfo.identifierNo || ''
formData.applyDepartment = props.patientInfo.organizationName || ''
formData.applyDocName = userNickName.value || userName.value || ''
formData.applyDocCode = userId.value || ''
//此处样本数据暂时固定为血液,后续根据实际情况调整
formData.specimenName = '血液'
formData.applyDeptCode = props.patientInfo.organizationName || ''
formData.applyOrganizationId = props.patientInfo.orgId || ''
formData.encounterId = props.patientInfo.encounterId
// 申请单号在保存时由后端生成,此处显示"自动生成"
formData.applyNo = '自动生成'
// 申请日期实时更新(启动定时器)
startApplyTimeTimer()
// 获取主诊断信息
try {
const res = await getEncounterDiagnosis(props.patientInfo.encounterId)
if (res.code === 200 && res.data && res.data.length > 0) {
// 查找主诊断maindiseFlag === 1
const mainDiagnosis = res.data.find(item => item.maindiseFlag === 1)
if (mainDiagnosis) {
formData.clinicDiag = mainDiagnosis.name || ''
} else {
// 没有主诊断时清空临床诊断
formData.clinicDiag = ''
}
} else {
// 没有诊断数据时清空临床诊断
formData.clinicDiag = ''
}
} catch (error) {
formData.clinicDiag = ''
}
}
}
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 3,
encounterId: props.patientInfo.encounterId
})
// 检验项目列表
const inspectionList = ref([])
// 执行科室选项Bug #329: 从科室管理加载完整数据)
const executeDepartmentOptions = ref([])
// 执行科室加载完成标志BugFix#CodeReview: 防止竞态条件)
const isExecuteDepartmentLoaded = ref(false)
// 删除操作标志,用于避免删除时触发数据填充
const isDeleting = ref(false)
// 表单数据
const formData = reactive({
applyOrganizationId: '',
applicationId: null,
applyNo: '',
patientId: '',
patientName: '',
medicalrecordNumber: '',
natureofCost: 'self',
applyTime: '', // 初始值设为空字符串,在新增时设置为当前时间
applyDepartment: '',
applyDocName: '',
executeDepartment: '',
clinicDesc: '',
contraindication: '',
clinicDiag: '',
medicalHistorySummary: '',
purposeofInspection: '',
physicalExam: '',
labApplyItemList: [],
inspectionItemsText: '',
applyRemark: '',
priorityCode: 0,
// 收费标记默认不勾选
applyStatus: 0,
needRefund: false,
needExecute: false,
inspectionDoctor: '',
inspectionTime: null,
auditDoctor: '',
auditTime: null,
visitNo: '',
applyDocCode: '',
applyDeptCode: '',
specimenName: '血液',
encounterId: ''
})
// 表单引用
const formRef = ref()
// 表格引用
const inspectionTableRef = ref()
// 验证错误状态
const validationErrors = reactive({
executeDepartment: false,
clinicDesc: false,
clinicDiag: false,
medicalHistorySummary: false,
purposeofInspection: false,
labApplyItemList: false,
applyTime: false
})
// 已选择的表格行
const selectedRows = ref([])
// 已选择的检验项目
const selectedInspectionItems = ref([])
// 搜索关键词
const searchKeyword = ref('')
// 活动分类
const activeCategory = ref('')
// 检验项目分类动态从API获取支持懒加载和分页
const inspectionCategories = ref([])
// 检验项目加载状态(整体)
const inspectionLoading = ref(false)
// 每页加载条数
const PAGE_SIZE = 50
// 搜索防抖时间(毫秒)
const SEARCH_DEBOUNCE_TIME = 300
// 加载执行科室列表Bug #329: 从科室管理加载完整数据)
// BugFix#CodeReview: 添加加载完成标志防止竞态条件
const loadExecuteDepartmentList = async () => {
if (isExecuteDepartmentLoaded.value) return // 已加载则跳过
try {
const res = await getOrgTree()
if (res.code === 200 && res.data) {
// 注意getOrgTree 返回的数据格式是 res.data.records
const deptList = Array.isArray(res.data.records) ? res.data.records : (Array.isArray(res.data) ? res.data : [])
if (deptList.length === 0) {
console.warn('未找到科室数据')
}
// 过滤出执行性质的科室
// OrganizationType 枚举1=医院2=科室
// OrganizationClass 枚举1=门诊2=住院3=药房4=库房5=财务6=护士站7=管理部门8=后勤部门9=其他
// 执行科室包括:门诊科室、住院科室、药房、护士站、检验科、放射科等医技科室
// 非执行科室:财务、管理部门、后勤部门、库房
// BugFix#329: 放宽过滤条件,确保医技科室(检验科、放射科等)不被排除
executeDepartmentOptions.value = deptList
.filter(dept => {
// typeEnum: 1=医院2=科室
// classEnum: 科室分类(可能是数字或字符串)
const typeEnum = dept.typeEnum
const classEnum = dept.classEnum
// 过滤条件:
// 1. 必须是科室类型typeEnum = 2
const isDepartment = typeEnum === 2
// 只有明确为以下分类的才排除(财务、管理部门、后勤部门、库房)
// 注意:医技科室(检验科、放射科等)可能分类为"其他(9)"或未分类,不应排除
const excludeClassEnums = ['4', '5', '7', '8', 4, 5, 7, 8, 'storage', 'fin', 'manager', 'support']
const isNonExecuteDept = excludeClassEnums.includes(classEnum)
// 保留所有执行科室(包括检验科、放射科等医技科室)
// 如果 classEnum 为空或不在排除列表中,默认认为是执行科室
return isDepartment && !isNonExecuteDept
})
.map(dept => ({
// 🔧 BugFix: 使用 busNo科室编码作为 value而不是 id
// 原因lab_apply_item.perform_dept_code 字段长度为 varchar(12)
// 而 dept.id 是长整型,转换为字符串后可能超过 12 位
value: dept.busNo,
label: dept.name,
id: dept.id, // 保留 id 用于其他用途
code: dept.busNo // 使用 busNo 作为科室编码
}))
// 🔧 BugFix: 过滤掉没有 busNo 的科室,确保数据完整性
.filter(dept => dept.value)
}
} catch (error) {
console.error('加载执行科室列表失败:', error)
} finally {
// BugFix#CodeReview: 设置加载完成标志
isExecuteDepartmentLoaded.value = true
}
}
// 加载检验类型分类列表(只加载分类,项目懒加载)
const loadInspectionData = async () => {
// 如果已经加载过分类,直接返回
if (inspectionCategories.value.length > 0) {
return
}
inspectionLoading.value = true
try {
// 只获取检验类型列表
const typeRes = await getInspectionTypeList().catch(() => {
return { data: [] }
})
const typeList = typeRes.data || []
// 创建分类结构,但不加载项目(懒加载)
const categories = typeList
.filter(type => type.validFlag === 1 || type.validFlag === undefined)
.map((type, index) => {
let performDeptCode = type.department || ''
// 如果部门字段是数字ID尝试转换为科室名称
if (performDeptCode && !isNaN(performDeptCode)) {
// 🔧 BugFix: 通过 opt.id 匹配科室ID而不是 opt.value
// 因为 opt.value 现在是 busNo不再是 id
const deptOption = executeDepartmentOptions.value.find(opt => opt.id.toString() === performDeptCode.toString())
if (deptOption) {
performDeptCode = deptOption.label // 使用科室名称作为执行科室代码
}
}
return {
key: type.code || `type_${index}`,
label: type.name || `分类${index + 1}`,
typeId: type.id, // 保存类型 ID 用于分页查询
performDeptCode: performDeptCode, // 使用所属科室作为执行科室代码
expanded: index === 0, // 默认展开第一个
loaded: false, // 是否已加载项目
loading: false, // 是否正在加载
items: [], // 项目列表
pageNo: 1, // 当前页码
pageSize: PAGE_SIZE, // 每页条数
total: 0, // 总条数
hasMore: true // 是否还有更多数据
}
})
if (categories.length > 0) {
inspectionCategories.value = categories
activeCategory.value = categories[0].key
// 预加载第一个分类的项目
await loadCategoryItems(categories[0].key)
}
} catch (error) {
console.error('加载检验类型数据失败:', error)
} finally {
inspectionLoading.value = false
}
}
// 懒加载分类项目(分页)
const loadCategoryItems = async (categoryKey, loadMore = false) => {
const category = inspectionCategories.value.find(c => c.key === categoryKey)
if (!category) return
// 已加载完成且不是加载更多,或正在加载中,跳过
if ((category.loaded && !loadMore) || category.loading) return
// 没有更多数据了,跳过
if (loadMore && !category.hasMore) return
category.loading = true
try {
const params = {
pageNo: category.pageNo,
pageSize: category.pageSize,
searchKey: searchKeyword.value || ''
}
// 如果有类型ID添加筛选条件从检验项目维护页面获取数据
// 如果有类型 ID添加筛选条件
if (category.typeId) {
params.inspectionTypeId = category.typeId
}
const res = await getLabActivityDefinitionPage(params)
// 解析数据
let records = []
let total = 0
if (res.code === 200 && res.data && res.data.records) {
records = res.data.records
total = res.data.total || 0
} else if (res.data && Array.isArray(res.data)) {
records = res.data
total = records.length
} else if (Array.isArray(res)) {
records = res
total = records.length
}
// 映射数据格式(从检验项目维护页面的数据结构映射)
const mappedItems = records.map(item => {
// 套餐项目处理:套餐项目使用套餐金额,普通项目使用零售价
// BugFix#404: 增加对空字符串的判断避免空字符串被误认为有效套餐ID
const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null'
const itemPrice = isPackage
? (parseFloat(item.packageAmount || 0) || parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
: (parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
return {
itemId: item.id || Math.random().toString(36).substring(2, 11),
itemName: item.name || '',
itemPrice: itemPrice,
itemAmount: itemPrice,
sampleType: item.specimenCode || '血液',
unit: item.permittedUnitCode_dictText || item.unit || '',
itemQty: 1,
serviceFee: 0,
type: category.label,
isSelfPay: false,
code: item.busNo || '',
inspectionTypeId: item.inspectionTypeId ? Number(item.inspectionTypeId) : null,
activityId: item.id, // 保存 activityId 用于保存申请单时使用
// 套餐相关字段
isPackage: isPackage,
feePackageId: item.feePackageId,
packageName: item.packageName,
children: isPackage ? [] : undefined,
hasChildren: isPackage
}
})
// 更新分类数据
if (loadMore) {
// 追加数据
category.items.push(...mappedItems)
} else {
// 首次加载
category.items = mappedItems
}
category.total = total
category.hasMore = category.items.length < total
category.loaded = true
} catch (error) {
console.error('加载检验项目失败:', error)
// 加载失败时设置空数据
if (!loadMore) {
category.items = []
category.total = 0
category.hasMore = false
category.loaded = true
}
} finally {
category.loading = false
}
}
// 加载更多项目
const loadMoreItems = (categoryKey) => {
const category = inspectionCategories.value.find(c => c.key === categoryKey)
if (!category || !category.hasMore || category.loading) return
category.pageNo++
loadCategoryItems(categoryKey, true)
}
// 处理滚动事件(无限滚动)
const handleScroll = ({ scrollTop, scrollHeight, clientHeight }) => {
// 距离底部 50px 时触发加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
const expandedCategory = inspectionCategories.value.find(c => c.expanded)
if (expandedCategory && expandedCategory.hasMore && !expandedCategory.loading) {
loadMoreItems(expandedCategory.key)
}
}
}
// 防抖搜索处理
const handleSearchDebounced = debounce(() => {
// 重新加载当前展开分类的数据
const expandedCategory = inspectionCategories.value.find(c => c.expanded)
if (expandedCategory) {
// 重置分页状态
expandedCategory.pageNo = 1
expandedCategory.loaded = false
expandedCategory.hasMore = true
expandedCategory.items = []
// 重新加载
loadCategoryItems(expandedCategory.key)
}
}, SEARCH_DEBOUNCE_TIME)
// 获取过滤后的项目(本地搜索)
const getFilteredItems = (categoryKey) => {
const category = inspectionCategories.value.find(cat => cat.key === categoryKey)
if (!category) return []
// 如果正在加载,返回现有数据
if (category.loading) {
return category.items
}
// 本地过滤(安全检查 itemName
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
return category.items.filter(item =>
item.itemName && item.itemName.toLowerCase().includes(keyword)
)
}
return category.items
}
// 搜索建议查询(自动完成)
const querySearchInspectionItems = async (queryString, cb) => {
if (!queryString) {
cb([])
return
}
try {
const params = {
pageNo: 1,
pageSize: 20, // 限制返回数量
categoryCode: inspectionCategoryCode.value,
searchKey: queryString
}
const res = await getLabActivityDefinitionPage(params)
let suggestions = []
if (res.code === 200 && res.data && res.data.records) {
// 映射数据格式,与 loadCategoryItems 保持一致
suggestions = res.data.records.map(item => ({
itemId: item.id,
itemName: item.name || '',
itemPrice: parseFloat(item.packageAmount || 0),
sampleType: item.specimenCode || '血液',
unit: item.permittedUnitCode_dictText || item.unit || '',
code: item.busNo || '',
activityId: item.id,
inspectionTypeId: item.inspectionTypeId ? Number(item.inspectionTypeId) : null
}))
}
cb(suggestions)
} catch (error) {
console.error('搜索检验项目失败:', error)
cb([])
}
}
// 搜索选择处理
const handleSearchSelect = (item) => {
// 直接添加到已选列表
if (!isItemSelected(item)) {
selectedInspectionItems.value.push({
...item,
itemName: item.itemName,
// BugFix: 套餐项目需要初始化展开属性,否则点击无法展开
expanded: false,
childrenLoaded: false,
loading: false,
children: item.isPackage ? [] : undefined
})
}
// 清空搜索关键词
searchKeyword.value = ''
}
// 搜索框清空处理
const handleSearchClear = () => {
searchKeyword.value = ''
}
// 获取检验申请单列表
const getInspectionList = () => {
// 如果没有encounterId,不调用接口
if (!queryParams.encounterId) {
return
}
loading.value = true
getApplyList({
encounterId: queryParams.encounterId,
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize
}).then((res) => {
if (res.code === 200) {
// 处理分页响应数据
if (res.data && typeof res.data === 'object') {
// 如果返回的是分页对象 {records: [...], total: 100}
if (Array.isArray(res.data.records)) {
// 直接使用后端返回的数据(后端已按申请单返回,无需合并)
inspectionList.value = res.data.records
total.value = res.data.total || res.data.records.length
}
// 如果返回的是普通数组
else if (Array.isArray(res.data)) {
// 直接使用后端返回的数据
inspectionList.value = res.data
total.value = res.data.length
}
// 如果返回的是其他对象结构
else {
inspectionList.value = []
total.value = 0
}
} else {
inspectionList.value = []
total.value = 0
}
} else {
inspectionList.value = []
total.value = 0
ElMessage.error(res.message || '获取检验申请单列表失败')
}
}).catch((error) => {
inspectionList.value = []
total.value = 0
ElMessage.error('获取检验申请单列表异常: ' + (error.message || ''))
}).finally(() => {
loading.value = false
})
}
// 合并检验申请单记录:将同一个申请单的多个明细合并成一条记录
const mergeInspectionApplyRecords = (records) => {
if (!records || records.length === 0) {
return []
}
// 使用Map按申请单号分组
const applyMap = new Map()
records.forEach(record => {
const applyNo = record.applyNo
if (applyMap.has(applyNo)) {
// 如果申请单已存在,合并检验项目
const existing = applyMap.get(applyNo)
existing.itemName = existing.itemName + '+' + record.itemName
// 累加金额,保留两位小数
const totalAmount = (parseFloat(existing.itemAmount) || 0) + (parseFloat(record.itemAmount) || 0)
existing.itemAmount = parseFloat(totalAmount.toFixed(2))
} else {
// 如果申请单不存在,直接添加
applyMap.set(applyNo, { ...record })
}
})
// 将Map转换为数组
return Array.from(applyMap.values())
}
// 格式化金额:确保显示两位小数
const formatAmount = (amount) => {
if (amount === null || amount === undefined || amount === '') {
return '0.00'
}
const num = parseFloat(amount)
if (isNaN(num)) {
return '0.00'
}
return num.toFixed(2)
}
// 格式化日期时间为字符串 YYYY-MM-DD HH:mm:ss
const formatDateTime = (date) => {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 启动申请日期实时更新定时器
const startApplyTimeTimer = () => {
// 先清除已存在的定时器
stopApplyTimeTimer()
// 立即更新一次
formData.applyTime = formatDateTime(new Date())
// 每秒更新
applyTimeTimer = setInterval(() => {
formData.applyTime = formatDateTime(new Date())
}, 1000)
}
// 停止申请日期实时更新定时器
const stopApplyTimeTimer = () => {
if (applyTimeTimer) {
clearInterval(applyTimeTimer)
applyTimeTimer = null
}
}
// 新增申请单
const handleNewApplication = async () => {
resetForm()
// 申请单号在保存时由后端生成,此处显示"待生成"
formData.applyNo = '自动生成'
// 申请日期实时更新(启动定时器)
startApplyTimeTimer()
// 确保申请医生是当前登录医生
formData.applyDocName = userNickName.value || userName.value || ''
leftActiveTab.value = 'application'
}
// 重置表单
const resetForm = async () => {
Object.assign(formData, {
applicationId: null,
applyOrganizationId: props.patientInfo.orgId || '',
patientName: props.patientInfo.patientName || '',
medicalrecordNumber: props.patientInfo.identifierNo || '',
natureofCost: 'self',
applyTime: '', // 申请日期由定时器实时更新
applyDepartment: props.patientInfo.organizationName || '',
applyDeptCode: props.patientInfo.organizationName,
applyDocCode: userId.value || '',
applyDocName: userNickName.value || userName.value || '',
executeDepartment: '',
clinicDesc: '',
contraindication: '',
clinicDiag: '',
medicalHistorySummary: '',
purposeofInspection: '',
physicalExam: '',
labApplyItemList: [],
applyRemark: '',
priorityCode: 0,
applyStatus: 0,
needRefund: false,
needExecute: false,
patientId: props.patientInfo.patientId || '',
visitNo: '',
specimenName: '血液',
encounterId: props.patientInfo.encounterId || '',
})
selectedInspectionItems.value = []
// 重置验证错误状态
Object.keys(validationErrors).forEach(key => {
validationErrors[key] = false
})
formRef.value?.clearValidate()
// 获取主诊断信息
if (props.patientInfo && props.patientInfo.encounterId) {
try {
const res = await getEncounterDiagnosis(props.patientInfo.encounterId)
if (res.code === 200 && res.data && res.data.length > 0) {
const mainDiagnosis = res.data.find(item => item.maindiseFlag === 1)
if (mainDiagnosis) {
formData.clinicDiag = mainDiagnosis.name || ''
} else {
// 没有主诊断时清空临床诊断
formData.clinicDiag = ''
}
} else {
// 没有诊断数据时清空临床诊断
formData.clinicDiag = ''
}
} catch (error) {
formData.clinicDiag = ''
}
}
}
// 保存
const handleSave = () => {
// P1防重复提交 - 立即设置标志
if (saving.value) return
saving.value = true
// 重置验证错误状态
Object.keys(validationErrors).forEach(key => {
validationErrors[key] = false
})
let hasErrors = false
// 修复【#406】保存前尝试从 props 同步患者信息,避免因加载时序导致信息缺失
if ((!formData.patientName?.trim() || !formData.medicalrecordNumber?.trim()) && props.patientInfo && props.patientInfo.encounterId) {
formData.patientName = props.patientInfo.patientName || ''
formData.medicalrecordNumber = props.patientInfo.identifierNo || ''
formData.encounterId = props.patientInfo.encounterId || ''
formData.visitNo = props.patientInfo.busNo || ''
formData.patientId = props.patientInfo.patientId || ''
formData.applyDepartment = props.patientInfo.organizationName || ''
formData.applyDeptCode = props.patientInfo.organizationName || ''
formData.applyOrganizationId = props.patientInfo.orgId || ''
}
// P0检查患者信息是否已加载
if (!formData.patientName?.trim() || !formData.medicalrecordNumber?.trim()) {
ElMessage.error('患者信息未加载,请稍后重试')
saving.value = false
return
}
// P0检查就诊信息是否有效
if (!formData.encounterId) {
ElMessage.error('就诊信息无效,请重新选择患者')
saving.value = false
return
}
// 检查必填字段,执行科室
if (!formData.executeDepartment) {
validationErrors.executeDepartment = true
hasErrors = true
}
// 检查必填字段,诊断描述
if (!formData.clinicDesc?.trim()) {
validationErrors.clinicDesc = true
hasErrors = true
}
// 检查必填字段,临床诊断
if (!formData.clinicDiag?.trim()) {
validationErrors.clinicDiag = true
hasErrors = true
}
// 检查必填字段,病史摘要
if (!formData.medicalHistorySummary?.trim()) {
validationErrors.medicalHistorySummary = true
hasErrors = true
}
// 检查必填字段,检验目的
if (!formData.purposeofInspection?.trim()) {
validationErrors.purposeofInspection = true
hasErrors = true
}
// 检查必填字段,检验项目
if (selectedInspectionItems.value.length === 0) {
validationErrors.labApplyItemList = true
hasErrors = true
ElMessage.error('请至少选择一项检验项目')
saving.value = false
return
}
// 申请日期由后端在保存时自动生成,无需前端校验
if (hasErrors) {
ElMessage.error('请填写所有必填字段')
saving.value = false
return
}
// 准备保存数据
const prepareSaveData = () => {
const labApplyItemList = selectedInspectionItems.value.map(item => ({
itemSeq: item.itemSeq || 1,
itemCode: item.code || '',
itemName: item.itemName || '',
itemPrice: item.itemPrice || 0,
itemQty: item.itemQty || 1,
itemAmount: item.itemAmount || 0,
itemStatus: formData.applyStatus || 0,
performDeptCode: formData.executeDepartment,
// Bug #326修复: 传入 activityId后端直接使用 ID 关联,避免用名称反查
activityId: item.activityId || item.itemId || null,
feePackageId: item.feePackageId || null,
isPackage: item.isPackage || false,
sampleType: item.sampleType || '',
unit: item.unit || ''
}))
return {
...formData,
labApplyItemList,
physicalExamination: formData.physicalExam,
inspectionItemsText: selectedInspectionItems.value.map(item => item.itemName).join('+')
}
}
// 申请单号由后端在保存时生成,直接保存
const saveData = prepareSaveData();
executeSave(saveData);
}
const executeSave = (saveData) => {
saveInspectionApplication(saveData).then((res) => {
if (res.code === 200) {
ElMessage.success('保存成功')
// 停止申请日期实时更新
stopApplyTimeTimer()
// 从后端返回获取生成的申请单号
if (res.data && res.data.applyNo) {
ElMessage.info(`申请单号:${res.data.applyNo}`)
// 显示后端返回的实际申请时间
if (res.data.applyTime) {
formData.applyTime = res.data.applyTime
}
}
emit('save', res.data) // 通知父组件保存成功
resetForm()
// 设置下一个新单为"待生成"
formData.applyNo = '自动生成'
// 启动新的申请日期实时更新
startApplyTimeTimer()
leftActiveTab.value = 'application'
// 刷新列表
getInspectionList()
} else {
// 对于其他错误,也使用弹窗提示
ElMessageBox.alert(res.message || '保存失败', '错误', {
confirmButtonText: '确定',
type: 'error',
}).catch(() => {
});
}
}).catch(() => {
// 处理请求失败的其他错误
ElMessage.error('保存失败,请稍后重试');
}).finally(() => {
saving.value = false
})
}
// 查看详情
const handleView = (row) => {
// 停止申请日期实时更新(查看已保存的申请单)
stopApplyTimeTimer()
// 加载表单数据
Object.assign(formData, row)
// 根据检验项目名称找到对应的项目数据
selectedInspectionItems.value = []
const itemNames = row.itemName?.split('、') || row.inspectionItem?.split('、') || []
inspectionCategories.value.forEach(category => {
category.items.forEach(item => {
if (itemNames.includes(item.itemName)) {
selectedInspectionItems.value.push(item)
}
})
})
leftActiveTab.value = 'application'
}
// 切换分类(修改为懒加载)
const switchCategory = (category) => {
if (activeCategory.value === category) {
// 如果点击的是当前激活的分类,则收起
activeCategory.value = ''
inspectionCategories.value.forEach(cat => {
if (cat.key === category) {
cat.expanded = false
}
})
} else {
// 否则切换到新的分类并展开
activeCategory.value = category
inspectionCategories.value.forEach(cat => {
cat.expanded = cat.key === category
})
// 懒加载该分类的项目
const targetCategory = inspectionCategories.value.find(c => c.key === category)
if (targetCategory && !targetCategory.loaded) {
loadCategoryItems(category)
}
}
}
// 处理项目项点击(排除勾选框点击)
const handleItemClick = (item) => {
toggleInspectionItem(item)
}
// 判断项目是否已选择
const isItemSelected = (item) => {
return selectedInspectionItems.value.some(selected => selected.itemId === item.itemId)
}
// 切换检验项目选择
const toggleInspectionItem = (item) => {
const index = selectedInspectionItems.value.findIndex(selected => selected.itemId === item.itemId)
if (index > -1) {
selectedInspectionItems.value.splice(index, 1)
} else {
// 创建新对象,包含 itemName 属性(等于 name 属性)
const newItem = {
...item,
itemName: item.itemName
};
selectedInspectionItems.value.push(newItem);
}
}
// 移除检验项目
const removeInspectionItem = (item) => {
const index = selectedInspectionItems.value.findIndex(selected => selected.itemId === item.itemId)
if (index > -1) {
selectedInspectionItems.value.splice(index, 1)
}
}
// 清空所有选择
const clearAllSelected = () => {
selectedInspectionItems.value = []
}
// Bug #387修复: 同步分类勾选状态
const syncCategoryChecked = () => {
// 重置所有分类项目的勾选状态
inspectionCategories.value.forEach(category => {
category.items.forEach(item => {
item.checked = false
})
})
// 获取已选项目的ID集合
const ids = new Set(selectedInspectionItems.value.map(s => s.itemId))
// 同步勾选状态
for (const cat of inspectionCategories.value) {
for (const item of cat.items) {
if (ids.has(item.itemId)) {
item.checked = true
}
}
}
}
// 分页大小改变
const handleSizeChange = (size) => {
queryParams.pageSize = size
getInspectionList()
}
const handleCurrentChange = (page) => {
queryParams.pageNo = page
getInspectionList()
}
// 选择框变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
const handlePrint = (row) => {
// 切换到申请单TAB
leftActiveTab.value = 'application'
// 加载要打印的数据
handleView(row)
// 等待DOM更新后执行打印
setTimeout(() => {
// 使用 hiprint 的 previewPrint 方法
const printDom = document.querySelector('.application-form')
if (printDom) {
previewPrint(printDom)
ElMessage.success('正在准备打印...')
} else {
ElMessage.warning('未找到打印内容')
}
}, 100)
}
// 删除申请单
const handleDelete = (row) => {
isDeleting.value = true; // 设置删除标志
ElMessageBox.confirm(
`确定要删除申请单 "${row.applyNo}" 吗?此操作将同时删除对应的医嘱。`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
}
).then(() => {
// 调用真实的 API 删除(传递 applyNo
// 调用真实的API删除
deleteInspectionApplication(row.applyNo).then((res) => {
if (res.code === 200) {
ElMessage.success('删除成功')
resetForm(); // 删除成功后清空表单
// 刷新列表
getInspectionList()
} else {
ElMessage.error(res.message || '删除失败')
}
}).catch((error) => {
console.error('删除检验请单异常:', error)
ElMessage.error('删除异常')
})
}).catch(() => {
// 用户取消删除
}).finally(() => {
isDeleting.value = false; // 重置删除标志
})
}
// 单元格点击 - 点击表格行时加载申请单详情
const handleCellClick = (row, column) => {
// 如果点击的是操作列或展开列,不触发数据填充
if (column.property === '操作' || column.label === '操作' ||
column.type === 'expand' || column.type === 'selection') {
return;
}
// 点击表格行时,将该申请单的数据加载到表单中
// 使用 applyNo 判断是否有效
if (row && row.applyNo) {
loadApplicationToForm(row);
}
}
// 行点击事件处理
const handleRowClick = (currentRow, oldRow) => {
// 点击表格行时,将该申请单的数据加载到表单中
// 使用 applyNo 判断是否有效,同时检查是否处于删除状态
if (currentRow && currentRow.applyNo && !isDeleting.value) {
loadApplicationToForm(currentRow);
}
}
// 提取公共方法加载申请单到表单
const loadApplicationToForm = async (row) => {
// 停止申请日期实时更新(加载已保存的申请单)
stopApplyTimeTimer()
// 切换到申请单 TAB
leftActiveTab.value = 'application'
// 先用列表数据设置基本信息
Object.assign(formData, {
applyNo: row.applyNo,
applyDocName: row.applyDocName,
priorityCode: row.priorityCode || 0,
applyStatus: row.applyStatus || 0
})
// 根据申请单号获取完整详情
try {
const res = await getInspectionApplyDetail(row.applyNo)
if (res.code === 200 && res.data) {
const detail = res.data
// 加载完整的表单数据
Object.assign(formData, {
applicationId: detail.applicationId || null,
applyNo: detail.applyNo,
patientId: detail.patientId,
patientName: detail.patientName,
medicalrecordNumber: detail.medicalrecordNumber,
natureofCost: detail.natureofCost || 'self',
applyTime: detail.applyTime,
applyDepartment: detail.applyDepartment,
applyDocName: detail.applyDocName,
applyDocCode: detail.applyDocCode,
applyDeptCode: detail.applyDeptCode,
applyOrganizationId: detail.applyOrganizationId,
executeDepartment: detail.executeDepartment || '',
clinicDesc: detail.clinicDesc,
contraindication: detail.contraindication,
clinicDiag: detail.clinicDiag,
medicalHistorySummary: detail.medicalHistorySummary,
purposeofInspection: detail.purposeofInspection,
physicalExam: detail.physicalExamination,
applyRemark: detail.applyRemark,
priorityCode: detail.priorityCode || 0,
applyStatus: detail.applyStatus || 0,
needRefund: detail.needRefund || false,
needExecute: detail.needExecute || false,
inspectionDoctor: detail.inspectionDoctor,
inspectionTime: detail.inspectionTime,
auditDoctor: detail.auditDoctor,
auditTime: detail.auditTime,
visitNo: detail.visitNo,
specimenName: detail.specimenName,
encounterId: detail.encounterId
})
// 加载检验项目数据
selectedInspectionItems.value = []
if (detail.labApplyItemList && detail.labApplyItemList.length > 0) {
// Bug #326修复: 直接使用后端返回的数据,不再从本地缓存查找匹配项
// 后端已返回完整关联信息activityId、feePackageId、inspectionTypeId、sampleType、unit
// Bug #387修复: 套餐项目默认展开,并自动加载明细
selectedInspectionItems.value = detail.labApplyItemList.map(item => {
const itemId = item.activityId || item.itemId || item.id || Math.random().toString(36).substring(2, 11)
const isPackage = item.feePackageId != null || item.itemName?.includes('套餐')
return {
itemId: itemId,
itemName: item.itemName || '',
itemPrice: item.itemPrice || 0,
itemAmount: item.itemAmount || 0,
itemQty: item.itemQty || 1,
sampleType: item.sampleType || '',
unit: item.unit || '',
code: item.itemCode || '',
isPackage: isPackage,
hasChildren: isPackage,
feePackageId: item.feePackageId || null,
activityId: item.activityId || itemId,
inspectionTypeId: item.inspectionTypeId || null,
expanded: isPackage, // Bug #387: 套餐默认展开
children: [],
childrenLoaded: !isPackage, // Bug #387: 套餐需加载明细
loading: false
}
})
// Bug #387修复: 自动加载套餐明细
for (const pkgItem of selectedInspectionItems.value) {
if (pkgItem.isPackage && pkgItem.feePackageId) {
pkgItem.loading = true
pkgItem.children = await fetchPackageDetails(pkgItem.feePackageId)
pkgItem.childrenLoaded = true
pkgItem.loading = false
}
}
// Bug #387修复: 同步分类勾选状态
syncCategoryChecked()
} else if (detail.inspectionItem || detail.itemName) {
// 如果只有项目名称,尝试从本地分类中查找匹配项
const itemNames = (detail.inspectionItem || detail.itemName).split(/[+,]/)
inspectionCategories.value.forEach(category => {
category.items.forEach(item => {
if (itemNames.includes(item.itemName)) {
selectedInspectionItems.value.push({ ...item })
}
})
})
}
}
} catch (error) {
// 如果获取详情失败,至少显示列表中的基本信息
Object.assign(formData, row)
}
// 重置验证错误状态
Object.keys(validationErrors).forEach(key => {
validationErrors[key] = false
})
}
// 监听activeTab变化
watch(() => props.activeTab, async (newVal) => {
if (newVal === 'inspection') {
await initData()
// 根据动态加载的分类设置默认展开
if (inspectionCategories.value.length > 0) {
// 展开第一个分类
activeCategory.value = inspectionCategories.value[0].key
inspectionCategories.value.forEach((cat, index) => {
cat.expanded = index === 0
})
}
}
})
// 监听patientInfo变化,确保encounterId及时更新并重新加载数据
watch(() => props.patientInfo, async (newVal) => {
if (newVal && newVal.encounterId) {
const oldEncounterId = queryParams.encounterId
queryParams.encounterId = newVal.encounterId
// 初始化数据
await initData();
// 如果encounterId发生变化重新加载检验申请单列表
if (oldEncounterId !== newVal.encounterId) {
getInspectionList()
}
// 更新科室编码
// const currentDeptCode = await getCurrentDeptCode();
// formData.applyDeptCode = currentDeptCode || '';
}
}, { deep: true, immediate: true })
// Bug #329: 监听已选择的检验项目,自动更新检验项目文本并设置默认执行科室
watch(() => selectedInspectionItems.value, async (newVal) => {
if (newVal && newVal.length > 0) {
formData.inspectionItemsText = newVal.map(item => item.itemName).join('+')
// Bug #329: 如果执行科室为空,根据第一个检验项目的检验类型自动设置默认执行科室
if (!formData.executeDepartment) {
const firstItem = newVal[0]
// 根据检验项目的 inspectionTypeId 获取默认执行科室
if (firstItem.inspectionTypeId) {
const defaultDeptCode = await getDefaultPerformDeptCode(firstItem.inspectionTypeId)
if (defaultDeptCode) {
formData.executeDepartment = defaultDeptCode
}
}
}
} else {
// Bug #329: 当项目被清空时,同时清空执行科室(下次选择项目时会重新自动设置)
formData.inspectionItemsText = ''
formData.executeDepartment = ''
}
}, { deep: true })
// Bug #329: 移除字典数据默认值设置,执行科室应从 Organization 表获取,不应使用字典数据
// 原问题inspection_lab_dept 字典的 value (如 'ORG045') 与 executeDepartmentOptions 的 value (科室编码 busNo) 格式不一致
// 导致 el-select 显示原始 value 值而非科室名称
// 组件挂载时预加载检验项目数据不依赖patientInfo
// BugFix #329: 先加载执行科室列表再加载检验类型确保检验类型的department字段能正确匹配
onMounted(async () => {
await loadExecuteDepartmentList()
await loadInspectionData()
// 修复【#406】挂载时如果已有patientInfo但watch未触发initData则手动调用
if (props.patientInfo && props.patientInfo.encounterId && !formData.encounterId) {
await initData()
}
})
// 组件卸载时清除定时器
onUnmounted(() => {
stopApplyTimeTimer()
})
// 暴露方法
defineExpose({
getList: getInspectionList
})
</script>
<style lang="scss" scoped>
/* 页面容器 - 紧凑布局 */
.inspection-application-container {
height: auto;
max-height: none;
overflow: visible;
padding: 0;
}
/* 覆盖 el-main 默认 padding */
.inspection-application-container .el-main {
padding: 0;
}
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
.top-action-bar {
display: none; /* 隐藏原有的顶部操作栏 */
}
.action-buttons {
display: flex;
gap: 8px;
}
/* 新增按钮样式 - PRD要求蓝色背景 #4a89dc */
.new-btn {
background-color: #4a89dc !important;
border-color: #4a89dc !important;
color: #fff !important;
}
.new-btn:hover {
background-color: #5a9aec !important;
border-color: #5a9aec !important;
}
/* 保存按钮样式 - PRD要求绿色背景 #48cfad */
.save-btn {
background-color: #48cfad !important;
border-color: #48cfad !important;
color: #fff !important;
}
.save-btn:hover {
background-color: #58dfbd !important;
border-color: #58dfbd !important;
}
/* Bug#334: 检验信息表格区 - 优化垂直空间利用率 */
.inspection-section {
padding: 2px 10px 0 10px;
}
.table-card {
height: auto;
}
.table-card :deep(.el-card__body) {
padding-bottom: 6px;
}
.card-header-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-actions {
display: flex;
gap: 8px;
}
/* Bug#334: 底部内容区域 - 优化垂直空间利用率 */
.bottom-content-area {
padding: 2px 10px;
}
/* 表单区域 */
.form-card {
height: auto;
}
/* 表单区域使用主色调 */
.form-tabs :deep(.el-tabs__item.is-active) {
color: #51A3F3 !important;
}
.form-tabs :deep(.el-tabs__active-bar) {
background-color: #51A3F3 !important;
}
.form-tabs {
height: auto;
}
.application-form {
overflow: visible;
padding: 4px 8px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin: 2px;
}
/* 选择区域 */
.selection-area {
display: flex;
flex-direction: column;
gap: 8px;
}
.inspection-selector,
.selected-items-area {
height: auto;
}
.card-title {
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-input {
margin-bottom: 8px;
}
.selected-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
:deep(.el-pagination) {
.el-pager li {
border-radius: 4px;
margin: 0 2px;
min-width: 32px;
height: 32px;
line-height: 30px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
transition: all 0.3s ease;
}
.el-pager li:hover {
border-color: #409eff;
color: #409eff;
}
.el-pager li.is-active {
border-color: #409eff;
background-color: #409eff;
color: #fff;
}
.el-pagination__jump {
margin-left: 10px;
}
}
.inspection-form {
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding: 15px 20px;
margin-bottom: 15px;
}
.form-header .title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
color: #1a2b6d;
}
.form-header .title i {
margin-right: 10px;
font-size: 24px;
}
.inspection-items-section {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
}
.selected-items {
margin-bottom: 20px;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-weight: bold;
}
.selected-list {
min-height: 40px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
}
.item-selector {
margin-top: 20px;
}
.category-tabs {
display: flex;
border-bottom: 1px solid #ebeef5;
margin-bottom: 15px;
}
.category-tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.category-tab:hover {
color: #409eff;
}
.category-tab.active {
color: #409eff;
border-bottom-color: #409eff;
font-weight: bold;
}
.items-list {
max-height: 300px;
overflow-y: auto;
}
.inspection-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
margin-bottom: 5px;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.inspection-item:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.inspection-item.selected {
border-color: #409eff;
background-color: #ecf5ff;
}
.item-itemName {
font-weight: 500;
}
.item-price {
color: #e6a23c;
font-weight: bold;
}
.inspection-details {
margin-top: 30px;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.details-header {
margin-bottom: 15px;
}
.details-header h3 {
margin: 0;
color: #1a2b6d;
font-size: 16px;
font-weight: bold;
}
.inspection-selector {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
background: white;
}
.category-list {
margin-bottom: 15px;
}
.category-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s ease;
margin-bottom: 2px;
}
.category-item:hover {
background-color: #f5f5f5;
}
.category-item.active {
background-color: #e6f7ff;
color: #409eff;
font-weight: bold;
}
.category-icon {
margin-right: 8px;
font-weight: bold;
width: 12px;
text-align: center;
}
.selected-summary {
margin: 10px 0;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.selected-items {
margin-top: 5px;
}
.inspection-selector .items-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
}
.inspection-selector .inspection-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 5px;
border: 1px solid #f0f0f0;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
background: #fafafa;
}
.inspection-selector .inspection-item:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.inspection-selector .inspection-item.selected {
border-color: #409eff;
background-color: #ecf5ff;
font-weight: bold;
}
/* 检验信息表格样式 */
:deep(.inspection-table) {
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}
/* 主色调 #51A3F3 - PRD样式规范 */
:deep(.inspection-table .el-table__header) {
th {
background: linear-gradient(to bottom, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 2px solid #e2e8f0;
color: #1e293b;
font-weight: 600;
font-size: 13px;
padding: 12px 8px;
}
}
/* 选中状态使用主色调 #51A3F3 */
:deep(.inspection-table .el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #51A3F3 !important;
border-color: #51A3F3 !important;
}
:deep(.inspection-table .el-checkbox__input.is-indeterminate .el-checkbox__inner) {
background-color: #51A3F3 !important;
border-color: #51A3F3 !important;
}
:deep(.inspection-table .el-table__body) {
td {
border-bottom: 1px solid #f1f5f9;
padding: 10px 8px;
font-size: 13px;
color: #475569;
}
}
:deep(.inspection-table .el-table__row:hover > td) {
background-color: #f8fafc !important;
}
:deep(.inspection-table .el-table__row--striped) {
background-color: #fafbfc;
}
:deep(.inspection-table .el-table__row--striped:hover > td) {
background-color: #f1f5f9 !important;
}
:deep(.inspection-table .el-checkbox) {
margin: 0;
}
:deep(.inspection-table .el-button--small) {
font-size: 12px;
padding: 4px 8px;
}
:deep(.inspection-table .el-button--link) {
color: #409eff;
transition: color 0.3s ease;
}
:deep(.inspection-table .el-button--link:hover) {
color: #66b1ff;
}
:deep(.inspection-table .el-button--link:last-child) {
color: #f56c6c;
}
:deep(.inspection-table .el-button--link:last-child:hover) {
color: #f78989;
}
:deep(.inspection-table .cell) {
line-height: 1.4;
}
/* 新的树形结构样式 - PRD要求高度约350px */
.category-tree {
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafafa;
max-height: 280px;
overflow-y: auto;
}
.category-tree-item {
border-bottom: 1px solid #ebeef5;
}
.category-tree-item:last-child {
border-bottom: none;
}
.category-tree-header {
display: flex;
align-items: center;
padding: 12px 15px;
cursor: pointer;
transition: all 0.3s ease;
background: #fff;
border-radius: 4px;
margin: 2px;
}
.category-tree-header:hover {
background-color: #f5f5f5;
}
.category-tree-header.active {
background-color: #e6f7ff;
color: #51A3F3;
font-weight: bold;
}
.category-tree-icon {
margin-right: 8px;
font-size: 12px;
width: 12px;
text-align: center;
color: #51A3F3;
}
.category-count {
margin-left: auto;
color: #999;
font-size: 12px;
}
.category-tree-children {
background: #fff;
border-top: 1px solid #ebeef5;
}
/* 加载中占位样式 */
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
font-size: 14px;
}
/* 加载更多样式 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-top: 1px dashed #ebeef5;
}
.load-info {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
/* 没有更多数据提示 */
.no-more {
text-align: center;
padding: 10px;
font-size: 12px;
color: #c0c4cc;
}
.inspection-tree-item {
display: flex;
align-items: center;
padding: 8px 15px 8px 35px;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 1px solid #f0f0f0;
}
.inspection-tree-item:last-child {
border-bottom: none;
}
.inspection-tree-item:hover {
background-color: #f5f5f5;
}
.inspection-tree-item.selected {
background-color: #ecf5ff;
border-left: 3px solid #51A3F3;
}
.inspection-tree-item .item-itemName {
flex: 1;
margin-left: 10px;
font-weight: 500;
}
.inspection-tree-item .item-price {
color: #e6a23c;
font-weight: bold;
margin-left: 10px;
}
/* 已选项目区样式 - PRD要求高度约350px */
.selected-items-area {
display: flex;
flex-direction: column;
}
.selected-tree {
flex: 1;
max-height: 280px;
}
.selected-tree-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.selected-tree-item:last-child {
border-bottom: none;
}
.selected-item-content {
display: flex;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 4px;
margin: 2px 0;
}
.selected-item-content .item-itemName {
flex: 1;
font-weight: 500;
}
.selected-item-content .item-price {
color: #e6a23c;
font-weight: bold;
margin-right: 10px;
}
/* 套餐明细展开样式 */
.selected-tree-header {
display: flex;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.selected-tree-header .item-itemName {
flex: 1;
margin-left: 8px;
font-weight: 500;
color: #333;
}
.selected-tree-header .item-price {
color: #e6a23c;
font-weight: bold;
margin-left: 16px;
min-width: 60px;
}
.selected-tree-header:hover {
background: #f0f0f0;
}
.selected-tree-header.expanded {
background: #e8f4fc;
border-left: 3px solid #51A3F3;
}
.selected-tree-icon {
width: 20px;
text-align: center;
color: #51A3F3;
font-size: 12px;
margin-right: 4px;
}
.is-package .selected-tree-header {
font-weight: 600;
}
.selected-tree-children {
padding: 4px 12px 4px 36px;
background: #fff;
border-top: 1px dashed #ebeef5;
}
.selected-tree-detail {
display: flex;
align-items: center;
padding: 6px 8px;
border-bottom: 1px solid #f5f5f5;
font-size: 12px;
}
.selected-tree-detail:last-child {
border-bottom: none;
}
.selected-tree-detail .detail-name {
flex: 1;
color: #333;
}
.selected-tree-detail .detail-unit {
width: 60px;
text-align: center;
color: #666;
}
.selected-tree-detail .detail-qty {
width: 40px;
text-align: center;
color: #666;
}
.selected-tree-detail .detail-price {
width: 60px;
text-align: right;
color: #e6a23c;
font-weight: 500;
}
.no-detail {
text-align: center;
padding: 10px;
color: #909399;
font-size: 12px;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
color: #909399;
font-size: 12px;
}
.no-selection {
text-align: center;
padding: 40px 20px;
color: #999;
}
/* 表单验证错误样式 */
.form-item-error .el-input__inner,
.form-item-error .el-textarea__inner {
border-color: #f56c6c !important;
}
.form-item-error .el-select .el-input__inner {
border-color: #f56c6c !important;
}
.is-error.el-input .el-input__inner,
.is-error.el-textarea .el-textarea__inner {
border-color: #f56c6c !important;
}
.error-message {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
line-height: 1;
}
.custom-message-box {
z-index: 9999 !important;
}
/* 确保模态框相关元素具有高z-index */
.el-popup-parent--hidden {
overflow: hidden;
padding-right: 0 !important;
}
.v-modal {
z-index: 2000 !important;
}
/* 响应式布局 */
@media (max-width: 992px) {
.bottom-content-area {
flex-direction: column;
}
.form-area,
.selection-area {
width: 100%;
}
.el-col {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
@media (max-width: 768px) {
.inspection-section {
padding: 10px;
}
.bottom-content-area {
padding: 0 10px 10px 10px;
gap: 15px;
}
/* 表单字段纵向排列 */
.application-form .el-form-item {
margin-bottom: 15px;
}
/* 隐藏表格非关键列 */
.inspection-table .el-table__cell:nth-child(n+4):nth-child(-n+7) {
display: none;
}
.top-action-bar {
padding: 0 10px;
}
.action-buttons {
flex-direction: column;
gap: 5px;
}
.el-button--large {
width: 100%;
}
}
/* 优化搜索框样式 */
.inspection-selector .el-input {
margin-bottom: 15px;
}
</style>