Files
his/openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue
wangjian963 d99daa3048 修复问题:
1. 修复检验申请单生成的医嘱签发失败问题(BugFix#328)
2. 修复处方工具类空指针异常问题
3. 修复检验项目套餐价格查询问题
4. 修复医嘱签发时费用项状态更新问题
2026-04-13 18:23:36 +08:00

3149 lines
93 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">
<!-- 顶部操作按钮区 - Bug#334: 优化垂直空间利用率 -->
<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>
<el-row class="card-header" type="flex" align="middle">
<el-icon><DocumentChecked /></el-icon>
<span>检验信息</span>
</el-row>
</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"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
@selection-change="handleSelectionChange"
@current-change="handleRowClick"
@cell-click="handleCellClick"
>
<!-- Bug #326: 添加展开列 -->
<el-table-column type="expand" width="50" align="center" header-align="center">
<template #default="scope">
<div v-if="scope.row.children && scope.row.children.length > 0" class="expand-content">
<el-table :data="scope.row.children" border size="small" style="width: 100%">
<el-table-column label="明细项目" prop="itemName" min-width="150" />
<el-table-column label="样本类型" prop="sampleType" width="100" />
<el-table-column label="单位" prop="unit" width="80" />
<el-table-column label="单价" prop="itemPrice" width="80" align="right">
<template #default="itemScope">
¥{{ formatAmount(itemScope.row.itemPrice) }}
</template>
</el-table-column>
<el-table-column label="数量" prop="itemQty" width="80" align="center" />
<el-table-column label="金额" prop="itemAmount" width="80" align="right">
<template #default="itemScope">
¥{{ formatAmount(itemScope.row.itemAmount) }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="expand-empty">无明细项目</div>
</template>
</el-table-column>
<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 v-if="scope.row.hasChildren" style="color: #409EFF; cursor: pointer" @click.stop="toggleExpand(scope.row)">
<el-icon style="vertical-align: middle; margin-right: 4px">
<Right v-if="!isExpanded(scope.row.applicationId)" />
<Bottom v-else />
</el-icon>
{{ scope.row.itemName }}
</span>
<span v-else>{{ 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="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>
<!-- Bug #326: 添加树形展开功能支持套餐明细展示 -->
<el-table
:data="selectedInspectionItems"
border
size="small"
style="width: 100%; min-width: 100%"
max-height="250"
row-key="itemId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
default-expand-all
>
<el-table-column label="项目名称" prop="itemName" min-width="180">
<template #default="scope">
<!-- BugFix#326: 套餐项目添加标识和加粗显示 -->
<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
/>
<!-- BugFix#326: 套餐项目添加标识 -->
<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 }}</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 }}</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, Right, Bottom } from '@element-plus/icons-vue'
import {
deleteInspectionApplication, getApplyList,
saveInspectionApplication,
getInspectionTypeList,
getInspectionTypeDetail,
getInspectionItemList,
getEncounterDiagnosis,
getInspectionApplyDetail,
getOrgTree,
getInspectionPackageDetails
} from '../api'
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()
const { inspection_lab_dept } = proxy.useDict('inspection_lab_dept')
// 动态获取的检验类型缓存(用于缓存不在 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')
// Bug #326: 展开的行
const expandedRowKeys = ref([])
// 申请日期实时更新定时器
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 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添加筛选条件
if (category.typeId) {
params.inspectionTypeId = category.typeId
}
const res = await getInspectionItemList(params)
// 解析数据
let records = []
let total = 0
if (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: 套餐项目使用套餐金额,普通项目使用零售价
// BugFix#404: 增加对空字符串的判断避免空字符串被误认为有效套餐ID
const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null'
const itemPrice = isPackage
? (item.packageAmount || item.retailPrice || item.price || 0)
: (item.retailPrice || item.price || 0)
return {
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
itemName: item.name || item.itemName || '',
itemPrice: itemPrice,
itemAmount: itemPrice,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
itemQty: 1,
serviceFee: item.serviceFee || 0,
type: category.label,
isSelfPay: false,
activityId: item.activityId,
code: item.busNo || item.code || item.activityCode,
inspectionTypeId: item.inspectionTypeId || category.typeId, // 如果项目没有inspectionTypeId使用分类的typeId
// 套餐相关字段
isPackage: isPackage,
feePackageId: item.feePackageId,
packageName: item.packageName,
children: isPackage ? [] : undefined, // 套餐项目预留children字段
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) {
// 加载失败时设置空数据
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 getInspectionItemList(params)
let suggestions = []
if (res.data && res.data.records) {
// 映射数据格式,与 loadInspectionItemsByType 保持一致
suggestions = res.data.records.map(item => ({
itemId: item.id || item.activityId,
itemName: item.name || item.itemName || '',
itemPrice: item.retailPrice || item.price || 0,
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
unit: item.unit || '',
code: item.busNo || item.code || item.activityCode,
activityId: item.activityId,
inspectionTypeId: item.inspectionTypeId || null
}))
}
cb(suggestions)
} catch (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
// 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 => ({
...item,
performDeptCode: formData.executeDepartment // 从字典获取的执行科室代码
}))
return {
...formData,
labApplyItemList,
physicalExamination: formData.physicalExam, // 字段名映射:前端 physicalExam -> 后端 physicalExamination
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)
}
// 切换检验项目选择
// 切换检验项目选择状态(支持异步获取检验类型)
// BugFix#CodeReview: 移除竞态条件风险,使用加载标志替代 setTimeout sleep
const toggleInspectionItem = async (item) => {
const index = selectedInspectionItems.value.findIndex(selected => selected.itemId === item.itemId)
if (index > -1) {
// 取消选择项目
selectedInspectionItems.value.splice(index, 1)
// 检查剩余项目:如果为空则清空执行科室,否则保持不变
if (selectedInspectionItems.value.length === 0) {
formData.executeDepartment = ''
}
} else {
// 选择新项目
const newItem = {
...item,
itemName: item.itemName,
// BugFix: 套餐项目需要初始化展开属性,否则点击无法展开
expanded: false,
childrenLoaded: false,
loading: false,
children: item.isPackage ? [] : undefined
}
selectedInspectionItems.value.push(newItem)
// Bug #329: 根据检验项目所属类型自动设置执行科室默认值
const inspectionTypeId = item.inspectionTypeId
if (inspectionTypeId) {
// BugFix#CodeReview: 使用加载标志等待,替代 setTimeout sleep
// 确保执行科室列表已加载完成最多等待5秒
if (!isExecuteDepartmentLoaded.value) {
const maxWaitTime = 5000
const startTime = Date.now()
while (!isExecuteDepartmentLoaded.value && (Date.now() - startTime) < maxWaitTime) {
await new Promise(resolve => setTimeout(resolve, 100))
}
if (!isExecuteDepartmentLoaded.value) {
console.warn('执行科室列表加载超时,跳过自动设置')
return
}
}
// 异步获取执行科室(支持动态获取缺失的检验类型)
const defaultDeptCode = await getDefaultPerformDeptCode(inspectionTypeId)
if (defaultDeptCode) {
formData.executeDepartment = defaultDeptCode
} else {
// BugFix#CodeReview: 匹配失败时提示用户手动选择
console.warn('未找到检验类型对应的执行科室typeId:', inspectionTypeId)
ElMessage.warning('未能自动匹配执行科室,请手动选择')
}
} else {
console.warn('检验项目没有 inspectionTypeId')
}
}
}
// 移除检验项目
const removeInspectionItem = (item) => {
const index = selectedInspectionItems.value.findIndex(selected => selected.itemId === item.itemId)
if (index > -1) {
selectedInspectionItems.value.splice(index, 1)
// 检查剩余项目:如果为空则清空执行科室,否则保持不变
if (selectedInspectionItems.value.length === 0) {
formData.executeDepartment = ''
}
}
}
// 清空所有选择
const clearAllSelected = () => {
selectedInspectionItems.value = []
formData.executeDepartment = ''
}
// BugFix#326: 展开/收起套餐明细
const togglePackageExpand = async (item) => {
if (!item.isPackage) return
item.expanded = !item.expanded
// 如果展开且未加载明细,则加载
// BugFix#404: 增加对有效套餐ID的校验避免请求404
const feePackageIdStr = String(item.feePackageId || '').trim()
const validPackageId = feePackageIdStr && feePackageIdStr !== '' && feePackageIdStr !== 'null' && feePackageIdStr !== 'undefined'
console.log('togglePackageExpand debug:', {
feePackageId: item.feePackageId,
feePackageIdStr,
validPackageId,
isPackage: item.isPackage,
expanded: item.expanded,
childrenLoaded: item.childrenLoaded
})
if (item.expanded && !item.childrenLoaded && validPackageId) {
item.loading = true
try {
console.log('正在请求套餐明细, packageId:', feePackageIdStr)
const res = await getInspectionPackageDetails(feePackageIdStr)
console.log('套餐明细响应:', res)
if (res.code === 200 && res.data) {
item.children = res.data.map(detail => ({
detailId: detail.detailId,
itemName: detail.itemName,
unit: detail.unit,
unitPrice: detail.unitPrice,
quantity: detail.quantity
}))
item.childrenLoaded = true
}
} catch (e) {
console.error('加载套餐明细失败:', e)
item.children = []
} finally {
item.loading = false
}
}
}
// 分页大小改变
const handleSizeChange = (size) => {
queryParams.pageSize = size
getInspectionList()
}
const handleCurrentChange = (page) => {
queryParams.pageNo = page
getInspectionList()
}
// 选择框变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// Bug #326: 处理展开/收起
const handleExpandChange = (row, expandedRows) => {
// 更新展开状态
if (expandedRows.includes(row)) {
if (!expandedRowKeys.value.includes(row.applicationId)) {
expandedRowKeys.value.push(row.applicationId)
}
} else {
const index = expandedRowKeys.value.indexOf(row.applicationId)
if (index > -1) {
expandedRowKeys.value.splice(index, 1)
}
}
}
// 判断是否展开
const isExpanded = (applicationId) => {
return expandedRowKeys.value.includes(applicationId)
}
// 切换展开状态
const toggleExpand = (row) => {
if (isExpanded(row.applicationId)) {
// 收起
expandedRowKeys.value = expandedRowKeys.value.filter(id => id !== row.applicationId)
} else {
// 展开
expandedRowKeys.value.push(row.applicationId)
// 如果没有加载过明细,加载明细数据
if (!row.childrenLoaded) {
loadInspectionItemDetails(row)
}
}
}
// 加载检验项目明细(套餐子项)
const loadInspectionItemDetails = async (row) => {
try {
const res = await getInspectionApplyDetail(row.applyNo)
if (res.code === 200 && res.data) {
const detail = res.data
if (detail.labApplyItemList && detail.labApplyItemList.length > 0) {
// 将明细数据转换为 children
row.children = detail.labApplyItemList.map(item => ({
itemId: item.itemId || item.id,
itemName: item.itemName || item.name,
sampleType: item.sampleType || '未知',
unit: item.unit || '',
itemPrice: item.itemPrice || item.price || 0,
itemQty: item.itemQty || 1,
itemAmount: item.itemAmount || item.price || 0
}))
row.childrenLoaded = true
// 标记是否有子项
row.hasChildren = row.children.length > 0
} else {
row.children = []
row.childrenLoaded = true
row.hasChildren = false
}
}
} catch (error) {
console.error('加载检验项目明细失败:', error)
row.children = []
row.childrenLoaded = true
row.hasChildren = false
}
}
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) => {
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('删除成功')
// 刷新列表
getInspectionList()
} else {
ElMessage.error(res.message || '删除失败')
}
}).catch((error) => {
console.error('删除检验<E6A380><E9AA8C>请单异常:', error)
ElMessage.error('删除异常')
})
}).catch(() => {
// 用户取消删除
})
}
// 单元格点击 - 点击表格行时加载申请单详情
const handleCellClick = (row, column) => {
// 点击表格行时,将该申请单的数据加载到表单中
// 使用 applyNo 判断是否有效
if (row && row.applyNo) {
loadApplicationToForm(row);
}
}
// 行点击事件处理
const handleRowClick = (currentRow, oldRow) => {
// 点击表格行时,将该申请单的数据加载到表单中
// 使用 applyNo 判断是否有效
if (currentRow && currentRow.applyNo) {
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) {
selectedInspectionItems.value = detail.labApplyItemList.map(item => ({
...item,
itemId: item.itemId || item.id || Math.random().toString(36).substring(2, 11),
itemName: item.itemName || item.name || '',
itemPrice: item.itemPrice || item.price || 0,
itemAmount: item.itemAmount || item.price || 0,
// BugFix: 套餐项目需要初始化展开属性,否则点击无法展开
// BugFix#404: 增加对空字符串的判断避免空字符串被误认为有效套餐ID
isPackage: item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null',
feePackageId: item.feePackageId,
expanded: false,
childrenLoaded: false,
loading: false,
children: (item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null') ? [] : undefined
}))
} 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,
// BugFix: 套餐项目需要初始化展开属性
expanded: false,
childrenLoaded: false,
loading: false,
children: item.isPackage ? [] : undefined
})
}
})
})
}
}
} 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 })
// 监听已选择的检验项目,自动更新检验项目文本(用+号拼接)
watch(() => selectedInspectionItems.value, (newVal) => {
if (newVal && newVal.length > 0) {
formData.inspectionItemsText = newVal.map(item => item.itemName).join('+')
} else {
formData.inspectionItemsText = ''
}
}, { deep: true })
// 监听执行科室选项加载完成,不自动设置默认值
// Bug #329: 等待选择检验项目时根据检验类型自动设置
watch(() => executeDepartmentOptions.value, (newVal) => {
// 不再自动设置第一个值为默认值
}, { immediate: true })
// 组件挂载时预加载检验项目数据(不依赖 patientInfo
onMounted(async () => {
// Bug #329: 先加载执行科室列表,确保选择检验项目时能正确映射
await loadExecuteDepartmentList()
// 再加载检验类型分类
await loadInspectionData()
})
// 组件卸载时清除定时器
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 #326: 展开内容样式 */
.expand-content {
padding: 10px;
}
.expand-empty {
padding: 10px;
text-align: center;
color: #999;
font-size: 12px;
}
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
.top-action-bar {
display: flex;
align-items: center;
justify-content: flex-end;
border-bottom: 1px solid var(--el-border-color-light);
background: var(--el-bg-color);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
padding: 0 12px;
}
.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 {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--el-text-color-primary);
}
/* 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;
height: calc(100vh - 200px);
min-height: 600px;
}
.inspection-selector,
.selected-items-area {
flex: 1;
min-height: 300px;
display: flex;
flex-direction: column;
}
/* 检验项目选择区 - 固定高度 */
.inspection-selector {
max-height: 350px;
}
/* 已选项目区 - 固定高度 */
.selected-items-area {
min-height: 300px;
}
.card-title {
font-weight: 600;
color: var(--el-text-color-primary);
}
/* 搜索输入框样式 */
.search-input {
margin-bottom: 8px;
}
/* 分类树样式 */
.category-tree {
flex: 1;
overflow-y: auto;
}
.category-tree-item {
margin-bottom: 4px;
}
.category-tree-header {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.category-tree-header:hover {
background-color: #f5f7fa;
}
.category-tree-header.active {
background-color: #e6f1ff;
color: #409EFF;
}
.category-tree-icon {
font-size: 12px;
color: #909399;
}
.category-count {
font-size: 12px;
color: #909399;
margin-left: auto;
}
.category-tree-children {
padding-left: 24px;
margin-top: 4px;
}
.inspection-tree-item {
padding: 6px 12px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.inspection-tree-item:hover {
background-color: #f5f7fa;
}
.inspection-tree-item.selected {
background-color: #e6f1ff;
color: #409EFF;
}
.item-itemName {
flex: 1;
font-size: 13px;
}
.item-price {
font-size: 12px;
color: #67c23a;
font-weight: 500;
}
/* 已选项目列表样式 */
.selected-tree {
flex: 1;
overflow-y: auto;
}
.selected-items-list {
margin-top: 8px;
}
.selected-list-item {
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s;
}
.selected-list-item:hover {
background-color: #f5f7fa;
}
.selected-item-content {
gap: 8px;
}
/* 加载状态样式 */
.loading-placeholder,
.load-more,
.no-more {
padding: 8px 12px;
text-align: center;
font-size: 12px;
color: #909399;
}
.load-info {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
/* 展开内容样式 */
.expand-content {
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 4px;
margin: 8px;
}
.expand-empty {
padding: 20px;
text-align: center;
color: #909399;
font-size: 13px;
background-color: #f5f7fa;
border-radius: 4px;
margin: 8px;
}
.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;
}
/* BugFix#326: 套餐明细展开样式 */
.selected-item-wrapper {
margin-bottom: 4px;
}
.expand-icon {
cursor: pointer;
margin-right: 4px;
transition: transform 0.3s;
}
.expand-icon:hover {
color: #409EFF;
}
.package-details {
margin-left: 24px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
margin-top: 4px;
}
.package-details.loading {
display: flex;
align-items: center;
gap: 8px;
color: #909399;
}
.package-detail-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
color: #606266;
border-bottom: 1px dashed #e4e7ed;
}
.package-detail-item:last-child {
border-bottom: none;
}
.detail-name {
flex: 1;
}
.detail-unit {
width: 50px;
text-align: center;
color: #909399;
}
.detail-price {
width: 70px;
text-align: right;
color: #e6a23c;
}
.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;
}
/* BugFix#326: 已选择区域树形结构样式(仿照项目选择区) */
.selected-tree-item {
border-bottom: 1px solid #ebeef5;
}
.selected-tree-item:last-child {
border-bottom: none;
}
.selected-tree-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: default;
transition: all 0.3s ease;
background: #fff;
border-radius: 4px;
margin: 2px;
}
.selected-tree-header:hover {
background-color: #f5f5f5;
}
.selected-tree-item.is-package .selected-tree-header {
cursor: pointer;
}
.selected-tree-item.is-package .selected-tree-header:hover {
background-color: #e6f7ff;
}
.selected-tree-item.is-package .selected-tree-header.expanded {
background-color: #e6f7ff;
font-weight: bold;
}
.selected-tree-icon {
margin-right: 8px;
font-size: 12px;
width: 12px;
text-align: center;
color: #51A3F3;
}
.selected-tree-children {
background: #fafafa;
border-top: 1px solid #ebeef5;
padding: 8px 0;
}
.selected-tree-detail {
display: flex;
align-items: center;
padding: 6px 12px 6px 32px;
font-size: 12px;
color: #606266;
}
.selected-tree-detail:hover {
background-color: #f0f0f0;
}
.selected-tree-detail .detail-name {
flex: 1;
min-width: 100px;
}
.selected-tree-detail .detail-unit {
width: 50px;
text-align: center;
color: #909399;
}
.selected-tree-detail .detail-qty {
width: 40px;
text-align: center;
color: #909399;
}
.selected-tree-detail .detail-price {
width: 60px;
text-align: right;
color: #e6a23c;
font-weight: 500;
}
.no-detail {
padding: 12px 32px;
color: #909399;
font-size: 12px;
text-align: center;
}
/* 加载中占位样式 */
.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;
}
.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>