1. 修复检验申请单生成的医嘱签发失败问题(BugFix#328) 2. 修复处方工具类空指针异常问题 3. 修复检验项目套餐价格查询问题 4. 修复医嘱签发时费用项状态更新问题
3149 lines
93 KiB
Vue
3149 lines
93 KiB
Vue
<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>
|