Files
his/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue

2570 lines
84 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-container">
<!-- 左侧导航 -->
<el-aside width="200px" class="side-aside">
<el-menu
:default-active="String(activeNav)"
class="side-nav"
@select="(index) => activeNav = Number(index)"
>
<el-menu-item v-for="(navItem, index) in navItems" :key="index" :index="String(index)">
{{ navItem }}
</el-menu-item>
</el-menu>
</el-aside>
<!-- 右侧主内容 -->
<el-main class="main-content">
<!-- 检验类型页面 -->
<template v-if="activeNav === 0">
<div class="header-actions">
<el-button type="primary" @click="addNewRow">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
<div class="table-container" @click="handleTableClick">
<el-table
:data="pagedTypeRows"
border
stripe
style="width: 100%"
:row-class-name="({ row }) => editingRowId === row.id ? 'editing-row' : ''"
>
<el-table-column label="" min-width="50" align="center">
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="*大类编码" min-width="140" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.code" size="small" />
</template>
<template v-else>
{{ row.code }}
</template>
</template>
</el-table-column>
<el-table-column label="*大类项目名称" min-width="180" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.name" size="small" />
</template>
<template v-else>
{{ row.name }}
</template>
</template>
</el-table-column>
<el-table-column label="*执行科室" min-width="160" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-tree-select
v-model="row.department"
:data="departments"
:props="{
value: 'name',
label: 'name',
children: 'children',
}"
value-key="name"
placeholder="请选择科室"
check-strictly
:expand-on-click-node="false"
clearable
size="small"
style="width: 100%;"
/>
</template>
<template v-else>
{{ row.department }}
</template>
</template>
</el-table-column>
<el-table-column label="序号" min-width="80" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.sortOrder" size="small" :disabled="isChildTypeRow(row)" />
</template>
<template v-else>
{{ row.sortOrder }}
</template>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.remark" size="small" />
</template>
<template v-else>
{{ row.remark }}
</template>
</template>
</el-table-column>
<el-table-column label="操作" min-width="160" align="center">
<template #default="{ row, $index }">
<div class="action-cell" style="display: flex; gap: 5px; justify-content: center;">
<el-button
type="primary"
size="small"
circle
@click="handleConfirm(row)"
>
<el-icon v-if="editingRowId === row.id"><Check /></el-icon>
<span v-else>✓</span>
</el-button>
<el-button
v-if="editingRowId !== row.id"
type="warning"
size="small"
circle
@click="handleEdit(row)"
>
<el-icon><Edit /></el-icon>
</el-button>
<el-button
v-if="editingRowId !== row.id"
type="success"
size="small"
circle
@click="handleAdd(row, $index)"
>
<el-icon><Plus /></el-icon>
</el-button>
<el-button
type="danger"
size="small"
circle
@click="handleDelete(row.id)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 页码区域 -->
<el-pagination
v-if="tableData.length > typePageSize"
:current-page="typeCurrentPage"
:page-size="typePageSize"
:total="tableData.length"
layout="total, prev, pager, next"
@current-change="typeGoPage"
style="margin-top: 20px; justify-content: center;"
/>
</template>
<!-- 检验项目页面 -->
<template v-else-if="activeNav === 1">
<div class="page-header">
<h2>检验项目</h2>
</div>
<div class="filter-section">
<div class="filter-item">
<label>检验类型</label>
<el-select v-model="testTypeFilter" placeholder="选择检验类型" clearable size="default" style="width: 140px;">
<el-option
v-for="type in testTypes"
:key="type.value"
:label="type.label"
:value="type.value"
/>
</el-select>
</div>
<div class="filter-item">
<label>名称</label>
<el-input v-model="nameFilter" placeholder="名称/编码" clearable size="default" style="width: 160px;" />
</div>
<div class="filter-item">
<label>费用套餐</label>
<!-- <el-select v-model="packageFilter" placeholder="选择费用套餐" clearable size="default" style="width: 140px;">-->
<el-select
v-model="packageFilter"
placeholder="点击选择套餐"
filterable
:loading="loading"
clearable
size="default"
style="width: 140px;"
no-data-text="未找到相关套餐"
@change="handleChange"
@visible-change="handlePackageVisibleChange">
<el-option
v-for="item in feePackages"
:key="item.id"
:label="item.packageName"
:value="item.id"
>
</el-option>
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" :icon="Plus" @click="addNewItem">新增</el-button>
<el-button :icon="RefreshRight" @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" @click="filterItems">查询</el-button>
<el-button type="primary" :icon="Download" @click="exportTable">导出表格</el-button>
</div>
</div>
<div class="table-container">
<el-table
:data="pagedInspectionItems"
border
stripe
style="width: 100%"
:row-class-name="({ row }) => editingRowId === row.id ? 'editing-row' : ''"
>
<el-table-column label="" min-width="50" align="center">
<template #default="{ $index }">
{{ (inspectionCurrentPage - 1) * inspectionPageSize + $index + 1 }}
</template>
</el-table-column>
<el-table-column label="小类编码" min-width="100" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.code" size="small" />
</template>
<template v-else>
{{ row.code }}
</template>
</template>
</el-table-column>
<el-table-column label="小类项目名称" min-width="140" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.name" size="small" />
</template>
<template v-else>
{{ row.name }}
</template>
</template>
</el-table-column>
<el-table-column label="检验类型" min-width="120" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-select v-model="row.inspectionTypeId" placeholder="选择检验类型" size="small" style="width: 100%;" @change="(val) => { const selected = testTypes.find(t => t.id === val); row.testType = selected ? selected.label : ''; }">
<el-option label="选择检验类型" value="" />
<el-option
v-for="type in testTypes"
:key="type.id"
:label="type.label"
:value="type.id"
/>
</el-select>
</template>
<template v-else>
{{ row.testType }}
</template>
</template>
</el-table-column>
<el-table-column label="费用套餐" min-width="120" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-select v-model="row.package" placeholder="选择费用套餐" size="small" style="width: 100%;" filterable clearable @change="updateAmountFromPackage(row)" @visible-change="(visible) => { if (visible && feePackages.length === 0) getFeePackages() }">
<el-option label="选择费用套餐" value="" />
<el-option
v-for="pkg in feePackages"
:key="pkg.id"
:label="pkg.packageName"
:value="pkg.id"
/>
</el-select>
</template>
<template v-else>
{{ row.package }}
</template>
</template>
</el-table-column>
<el-table-column label="样本类型" min-width="100" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-select v-model="row.sampleType" placeholder="选择样本类型" size="small" style="width: 100%;">
<el-option label="选择样本类型" value="" />
<el-option
v-for="type in sampleTypes"
:key="type.value"
:label="type.label"
:value="type.value"
/>
</el-select>
</template>
<template v-else>
{{ row.sampleType }}
</template>
</template>
</el-table-column>
<el-table-column label="金额" min-width="70" align="center">
<template #default="{ row }">
{{ row.amount }}
</template>
</el-table-column>
<el-table-column label="序号" min-width="70" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input-number v-model="row.sortOrder" size="small" :controls="false" style="width: 100%;" />
</template>
<template v-else>
{{ row.sortOrder || 999999 }}
</template>
</template>
</el-table-column>
<el-table-column label="服务范围" min-width="90" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-select v-model="row.serviceRange" size="small" style="width: 100%;">
<el-option
v-for="range in serviceRanges"
:key="range.value"
:label="range.label"
:value="range.value"
/>
</el-select>
</template>
<template v-else>
{{ row.serviceRange || '全部' }}
</template>
</template>
</el-table-column>
<el-table-column label="下级医技类型" min-width="100" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.subType" size="small" />
</template>
<template v-else>
{{ row.sub医技Type || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="备注" min-width="100" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-input v-model="row.remark" size="small" />
</template>
<template v-else>
{{ row.remark || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="操作" min-width="100" align="center">
<template #default="{ row }">
<template v-if="editingRowId === row.id">
<el-button type="success" size="small" circle @click="saveItem(row)" title="保存">
<el-icon><Check /></el-icon>
</el-button>
<el-button size="small" circle @click="cancelEdit(row)" title="取消">
<el-icon><Close /></el-icon>
</el-button>
</template>
<template v-else-if="editingRowId !== row.id">
<el-button type="warning" size="small" circle @click="editItem(row)" title="编辑">
<el-icon><Edit /></el-icon>
</el-button>
</template>
<el-button
v-if="!editingRowId || editingRowId === row.id"
type="danger"
size="small"
circle
@click="deleteItem(row.id)"
title="删除"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 页码区域 -->
<el-pagination
v-if="inspectionTotalPages > 1"
:current-page="inspectionCurrentPage"
:page-size="inspectionPageSize"
:total="inspectionTotalCount"
layout="total, prev, pager, next"
@current-change="inspectionGoPage"
style="margin-top: 20px; justify-content: center;"
/>
</template>
<!-- 套餐设置页面 -->
<template v-else-if="activeNav === 2">
<!-- 顶部操作栏 -->
<div class="top-bar">
<div class="action-group">
<el-button :icon="Refresh" @click="refreshPage">刷新</el-button>
<el-button :icon="RefreshRight" @click="resetForm">重置</el-button>
<el-button type="success" @click="handlePackageManagement">套餐管理</el-button>
</div>
<el-button type="success" size="large" @click="handleSave">保存</el-button>
</div>
<!-- 表单区域 -->
<div class="form-section">
<div class="section-title">基本信息</div>
<div class="form-grid">
<div class="form-item">
<span class="form-label">套餐类别</span>
<el-select v-model="packageCategory" style="width: 100%;">
<el-option label="检验套餐" value="检验套餐" />
</el-select>
</div>
<div class="form-item">
<span class="form-label">套餐级别</span>
<el-select
v-model="packageLevel"
filterable
allow-create
default-first-option
placeholder="请选择或输入套餐级别"
style="width: 100%;"
@change="handlePackageLevelChange"
>
<el-option
v-for="item in packageLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="form-item" id="departmentContainer" v-show="packageLevel === '科室套餐'">
<span class="form-label">科室</span>
<el-tree-select
v-model="department"
placeholder="请选择科室"
:data="departments"
:props="{
value: 'name',
label: 'name',
children: 'children',
}"
value-key="name"
check-strictly
:expand-on-click-node="false"
clearable
style="width: 100%;"
@change="handlePackageDepartmentChange"
/>
</div>
<div class="form-item" id="userContainer" v-show="packageLevel === '个人套餐'">
<span class="form-label">用户</span>
<el-input :model-value="userStore.nickName" readonly />
</div>
<div class="form-item">
<span class="form-label"><span style="color:red"></span>套餐名称</span>
<el-input v-model="packageName" placeholder="请输入套餐名称" />
<div class="error-message" id="packageNameError" style="color: #ff4d4f; font-size: 12px; margin-top: 4px; display: none;">套餐名称不能为空</div>
</div>
<div class="form-item">
<span class="form-label">卫生机构</span>
<el-input :model-value="userStore.orgName || '测试机构'" readonly />
</div>
<div class="form-item">
<span class="form-label">套餐金额</span>
<el-input :model-value="packageAmount.toFixed(2)" readonly style="width: 120px;" />
</div>
<div class="form-item">
<span class="form-label">折扣 %</span>
<el-input v-model="discount" @input="calculateAmounts" />
</div>
<div class="form-item">
<span class="form-label">制单人</span>
<el-input :model-value="userStore.nickName" readonly />
</div>
<div class="form-item">
<span class="form-label">备注</span>
<el-input v-model="remarks" placeholder="请输入备注" />
</div>
<div class="form-item">
<span class="form-label">是否停用</span>
<el-radio-group v-model="isDisabled">
<el-radio :label="false">启用</el-radio>
<el-radio :label="true">停用</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">显示套餐名</span>
<el-radio-group v-model="showPackageName">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">生成服务费</span>
<el-radio-group v-model="generateServiceFee">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">套餐价格</span>
<el-radio-group v-model="enablePackagePrice">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">不启用</el-radio>
</el-radio-group>
</div>
<div class="form-item">
<span class="form-label">服务费</span>
<el-input :model-value="serviceFee.toFixed(2)" readonly />
</div>
<div class="form-item">
<span class="form-label">lis分组</span>
<el-select v-model="selectedLisGroup" placeholder="请选择lis分组" style="width: 100%;">
<el-option
v-for="group in lisGroupList"
:key="group.id"
:label="group.groupName || group.lisGroupName"
:value="group.id"
/>
</el-select>
</div>
<div class="form-item">
<span class="form-label">血量</span>
<el-input v-model="bloodVolume" />
</div>
</div>
</div>
<!-- 检验套餐明细表格区域 -->
<div class="table-container" style="width: 100%; margin-top: 20px;">
<div class="table-header" style="display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="table-title" style="font-size: 16px; font-weight: bold;">检验套餐明细</div>
<el-button type="primary" size="small" circle @click="addPackageItem" title="添加项目">
<el-icon><Plus /></el-icon>
</el-button>
</div>
<el-table
:data="packageItems"
border
stripe
style="width: 100%"
:row-class-name="({ $index }) => editingRowId === $index ? 'editing-row' : ''"
>
<el-table-column label="行号" min-width="50" align="center">
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="项目名称/规格" min-width="180" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input
v-model="row.name"
placeholder="请输入或选择项目名称"
size="small"
/>
</template>
<template v-else>
{{ row.name }}
</template>
</template>
</el-table-column>
<el-table-column label="剂量" min-width="70" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input v-model="row.dosage" placeholder="剂量" size="small" />
</template>
<template v-else>
{{ row.dosage || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="途径" min-width="90" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-select v-model="row.route" placeholder="请选择" size="small" style="width: 100%;">
<el-option label="请选择" value="" />
<el-option label="/" value="/" />
<el-option label="/" value="/" />
</el-select>
</template>
<template v-else>
{{ row.route || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="频次" min-width="90" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-select v-model="row.frequency" placeholder="请选择" size="small" style="width: 100%;">
<el-option label="请选择" value="" />
<el-option label="一次" value="一次" />
<el-option label="每日" value="每日" />
<el-option label="每周" value="每周" />
</el-select>
</template>
<template v-else>
{{ row.frequency || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="天数" min-width="60" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input-number v-model.number="row.days" placeholder="天数" size="small" :controls="false" style="width: 100%;" />
</template>
<template v-else>
{{ row.days || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="数量" min-width="60" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input-number v-model.number="row.quantity" placeholder="数量" size="small" :controls="false" :min="0" :step="1" style="width: 100%;" @change="updateItemAmount(row)" />
</template>
<template v-else>
{{ row.quantity || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="单位" min-width="50" align="center">
<template #default="{ row }">
{{ row.unit || '-' }}
</template>
</el-table-column>
<el-table-column label="单价" min-width="80" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input-number v-model.number="row.unitPrice" placeholder="单价" size="small" :controls="false" :min="0" :precision="2" style="width: 100%;" @change="updateItemAmount(row)" />
</template>
<template v-else>
{{ row.unitPrice.toFixed(2) }}
</template>
</template>
</el-table-column>
<el-table-column label="金额" min-width="80" align="center">
<template #default="{ row }">
{{ row.amount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="服务费" min-width="80" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input-number v-model.number="row.serviceFee" placeholder="服务费" size="small" :controls="false" :min="0" :precision="2" style="width: 100%;" @change="updateItemTotalAmount(row)" />
</template>
<template v-else>
{{ row.serviceFee.toFixed(2) }}
</template>
</template>
</el-table-column>
<el-table-column label="总金额" min-width="80" align="center">
<template #default="{ row }">
{{ row.totalAmount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="产地" min-width="80" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-input v-model="row.origin" placeholder="产地" size="small" />
</template>
<template v-else>
{{ row.origin || '-' }}
</template>
</template>
</el-table-column>
<el-table-column label="操作" min-width="100" align="center">
<template #default="{ $index }">
<template v-if="editingRowId === $index">
<el-button type="success" size="small" circle @click="handleEditItem($index)" title="保存">
<el-icon><Check /></el-icon>
</el-button>
<el-button size="small" circle @click="cancelEditItem($index)" title="取消">
<el-icon><Close /></el-icon>
</el-button>
</template>
<template v-else>
<el-button type="warning" size="small" circle @click="handleEditItem($index)" title="编辑">
<el-icon><Edit /></el-icon>
</el-button>
<el-button type="danger" size="small" circle @click="deletePackageItem($index)" title="删除">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-main>
</el-container>
</template>
<script setup>
import request from '@/utils/request';
import useUserStore from '@/store/modules/user';
import {computed, getCurrentInstance, nextTick, onActivated, onMounted, ref, watch} from 'vue';
import {ElLoading, ElMessage, ElMessageBox} from 'element-plus';
import {Check, Edit, Plus, Delete, Refresh, RefreshRight, Search, Download, Close} from '@element-plus/icons-vue';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {
addInspectionType,
delInspectionType,
listInspectionType,
updateInspectionType
} from '@/api/system/inspectionType';
import {
getDiagnosisTreatmentList,
addDiagnosisTreatment,
editDiagnosisTreatment,
stopDiseaseTreatment
} from '@/views/catalog/diagnosistreatment/components/diagnosistreatment';
import {listLisGroup} from '@/api/system/checkType';
import {
addInspectionPackage,
getInspectionPackage,
listInspectionPackageDetails,
saveInspectionPackageDetails
} from '@/api/system/inspectionPackage';
import {deptTreeSelect} from '@/api/system/user';
// 获取当前登录用户信息
const userStore = useUserStore();
// 获取字典数据
const { proxy } = getCurrentInstance()
const { activity_category_code } = proxy.useDict('activity_category_code')
// 获取"检验"分类的字典值
const inspectionCategoryCode = computed(() => {
const dictList = activity_category_code.value
const inspectionItem = dictList?.find(item => item.label === '检验')
return inspectionItem?.value // 默认使用数据库中检验的 category_code 值
})
// 创建路由实例
const router = useRouter();
const route = useRoute();
// 存储LIS分组数据
const lisGroupList = ref([]);
// 选中的LIS分组
const selectedLisGroup = ref('');
// 获取就诊科室数据 - 与门诊挂号页面保持一致,在组件初始化时直接调用
getLocationInfo();
// 获取LIS分组数据
const getLisGroupList = async () => {
try {
const response = await listLisGroup();
if (response.code === 200) {
// 适配可能的不同响应格式
let items = [];
// 检查响应数据
if (response.data) {
// 格式1: {data: {rows: [], total: number}} - 标准分页格式
if (response.data.rows && Array.isArray(response.data.rows)) {
items = response.data.rows;
}
// 格式2: {data: []} - 简单数组格式
else if (Array.isArray(response.data)) {
items = response.data;
}
// 格式3: {data: {data: []}} - 双重嵌套格式
else if (response.data.data && Array.isArray(response.data.data)) {
items = response.data.data;
}
// 格式4: {data: {data: {rows: []}}} - 双重嵌套分页格式
else if (response.data.data && response.data.data.rows && Array.isArray(response.data.data.rows)) {
items = response.data.data.rows;
}
}
lisGroupList.value = items;
} else {
ElMessage.error('获取LIS分组数据失败');
}
} catch (error) {
console.error('获取LIS分组数据失败:', error);
ElMessage.error('获取LIS分组数据失败');
}
};
// 导航数据
const navItems = ref(['检验类型', '检验项目', '套餐设置']);
const activeNav = ref(0);
// 检验类型数据
const tableData = ref([]);
// ==============================
// 分页(检验类型 tab
// ==============================
const typeCurrentPage = ref(1);
// 每页条数按需求固定为10
const typePageSize = ref(10);
const typeTotalPages = computed(() => {
const total = tableData.value.length;
return Math.max(1, Math.ceil(total / typePageSize.value));
});
// 按”序号(sortOrder)”排序后再分页展示(需求:不要按大类编码排序)
function parseCodeParts(code) {
const s = (code ?? '').toString().trim();
// 支持 1、01、3-001、3_001 等:用 -/_ 分隔
const parts = s.split(/[-_]/g);
const mainRaw = parts[0] ?? '';
const subRaw = parts[1] ?? '';
const mainNum = Number.parseInt(mainRaw, 10);
const subNum = Number.parseInt(subRaw, 10);
return {
raw: s,
mainRaw,
subRaw,
mainIsNum: !Number.isNaN(mainNum),
subIsNum: !Number.isNaN(subNum),
mainNum,
subNum
};
}
function isTempId(id) {
return typeof id === 'string' && id.startsWith('temp_');
}
function isChildTypeRow(row) {
const p = parseCodeParts(row?.code);
return !!p.subRaw;
}
// 检验类型表:保持“当前顺序”展示,不在前端根据序号实时重新排序,
// 这样在点击“修改”或编辑序号时,行号不会在编辑过程中发生变化。
const sortedTypeRows = computed(() => {
return [...tableData.value];
});
const pagedTypeRows = computed(() => {
const start = (typeCurrentPage.value - 1) * typePageSize.value;
return sortedTypeRows.value.slice(start, start + typePageSize.value);
});
function typeGoPage(p) {
if (p < 1 || p > typeTotalPages.value) return;
typeCurrentPage.value = p;
}
watch(
() => tableData.value.length,
() => {
// 数据变化后,确保当前页有效
if (typeCurrentPage.value > typeTotalPages.value) {
typeCurrentPage.value = 1;
}
}
);
// 获取检验类型列表 - 从后端API获取
const getInspectionTypeList = () => {
listInspectionType().then(data => {
// 确保数据结构与前端使用的一致处理后端返回的AjaxResult格式
// 后端返回的数据格式: {code: 200, msg: "查询成功", data: [检验类型列表]}
const inspectionTypeList = data.data || [];
// 后端实体字段名本身就是 sortOrder这里不再从不存在的 item.order 做映射
let formattedData = inspectionTypeList.map(item => ({
...item,
sortOrder: item.sortOrder
}));
// 初始/刷新时:按“序号 + 编码”做一次排序,确保表单整体是按序号从小到大排列
formattedData = formattedData.sort((a, b) => {
const aOrder = a?.sortOrder == null || Number.isNaN(Number(a.sortOrder))
? Number.POSITIVE_INFINITY
: Number(a.sortOrder);
const bOrder = b?.sortOrder == null || Number.isNaN(Number(b.sortOrder))
? Number.POSITIVE_INFINITY
: Number(b.sortOrder);
if (aOrder !== bOrder) return aOrder - bOrder;
// 同序号时按编码作为稳定排序的备用键
const aCode = (a?.code ?? '').toString().trim();
const bCode = (b?.code ?? '').toString().trim();
return aCode.localeCompare(bCode, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' });
});
// 过滤掉已逻辑删除的记录validFlag为0
tableData.value = formattedData.filter(item => item.validFlag === 1);
}).catch(error => {
console.error('获取检验类型列表失败:', error);
});
};
const editingRowId = ref(null);
const departments = ref([]);
/** 查询执行科室 - 与”科室管理”页面保持完全一致的过滤与展示口径 */
function getLocationInfo() {
// 与科室管理页 queryParams 对齐pageNo/pageSize/typeEnum/classEnum/name
// deptTreeSelect 内部默认 typeEnum=2科室这里显式传入确保不会被外部覆盖
const queryParams = {
pageNo: 1,
pageSize: 1000, // 获取足够多的数据
name: undefined,
typeEnum: 2,
classEnum: undefined
};
deptTreeSelect(queryParams).then((response) => {
let orgList = [];
// 处理分页响应格式
if (response && response.data) {
if (response.data.records && Array.isArray(response.data.records)) {
orgList = response.data.records;
} else if (Array.isArray(response.data)) {
orgList = response.data;
}
} else if (Array.isArray(response)) {
orgList = response;
}
/**
* 关键:科室管理页面使用 el-table 展示的是 records顶层列表并不会展示 children 子节点。
* 为了”完全一致”,这里也只使用 records 顶层数据,并移除 children避免出现科室管理页看不到的下级节点。
*/
const topLevel = (orgList || []).map(n => {
const { children, ...rest } = (n || {});
return rest;
});
// 与科室管理页保持一致:即使 name 为空也保留该行(科室管理表格仍会显示一行)
departments.value = topLevel;
}).catch((error) => {
console.error('获取执行科室失败:', error);
departments.value = [];
});
}
// 处理套餐级别选择变化
function handlePackageLevelChange(value) {
if (value !== '科室套餐') {
department.value = '';
}
}
// 处理套餐科室选择变化
function handlePackageDepartmentChange(selectedNode) {
// 如果selectedNode是对象只取name属性
if (typeof selectedNode === 'object' && selectedNode !== null) {
department.value = selectedNode.name;
} else {
// 否则直接使用(可能是字符串)
department.value = selectedNode;
}
}
// 费用套餐数据
const feePackages = ref([]);
const loading = ref(false);
/**
* 核心修改:点击/聚焦时触发
* 只有当列表为空 或 用户明确点击时,才去后端拉取最新数据
*/
const handleFocus = () => {
// 如果正在加载中,防止重复点击
if (loading.value) return;
// 策略 A: 每次点击都刷新最新数据 (推荐用于数据变动频繁的场景)
getFeePackages();
// 策略 B: 仅第一次点击时加载,后续使用缓存数据 (如果数据不常变,可取消下面注释并注释掉上面那行)
/*
if (!hasLoadedOnce.value) {
getFeePackages();
hasLoadedOnce.value = true;
}
*/
};
/**
* 获取数据方法
* 这里不再接收搜索关键词,而是直接拉取所有启用的套餐
* 如果需要后端支持模糊搜索,可以配合 filterable 在前端做,或者在这里加逻辑
*/
const getFeePackages = async () => {
loading.value = true;
try {
const response = await request({
url: '/system/inspection-package/list',
method: 'get',
params: {
pageNum: 1,
pageSize: 100, // 点击加载时,建议稍微多拉一点,方便前端过滤
isDisabled: false // 只查未禁用的
// 注意:这里没有传 packageName因为我们是全量加载供用户选择
}
});
if (response.code === 200) {
feePackages.value = response.rows || [];
} else {
ElMessage.warning(response.msg || '查询失败');
}
} catch (error) {
console.error('加载套餐列表失败:', error);
// 错误拦截器通常会处理弹窗,这里可不处理或简单记录
} finally {
loading.value = false;
}
};
/**
* 选中值变化回调
*/
const handleChange = (val) => {
if (val) {
const selected = feePackages.value.find(item => item.id === val);
console.log('用户选中了套餐:', selected);
}
};
/**
* 顶部筛选费用套餐下拉展开/收起回调
* 展开时若列表为空则加载数据
*/
const handlePackageVisibleChange = (visible) => {
if (visible && feePackages.value.length === 0) {
getFeePackages();
}
};
// 可选:如果希望页面一打开就预加载一次,保留 onMounted
// 如果希望完全由用户点击触发,可以注释掉 onMounted
onMounted(() => {
// getFeePackages();
});
// 样本类型数据
const sampleTypes = ref([
{ value: '血液', label: '血液' },
{ value: '尿液', label: '尿液' },
{ value: '粪便', label: '粪便' },
{ value: '脑脊液', label: '脑脊液' },
{ value: '胸水', label: '胸水' },
{ value: '腹水', label: '腹水' },
{ value: '分泌物', label: '分泌物' },
{ value: '组织', label: '组织' },
{ value: '其他', label: '其他' }
]);
// 服务范围数据
const serviceRanges = ref([
{ value: '全部', label: '全部' },
{ value: '门诊', label: '门诊' },
{ value: '住院', label: '住院' },
{ value: '急诊', label: '急诊' },
{ value: '体检', label: '体检' }
]);
// 检验类型数据 - 从后端动态获取
const testTypes = ref([]);
// 加载检验类型列表(用于检验项目页面的下拉框)
const loadInspectionTypes = async () => {
try {
const response = await listInspectionType();
if (response.code === 200 && response.data) {
const data = Array.isArray(response.data) ? response.data : (response.data.records || []);
testTypes.value = data
.filter(item => item.validFlag === 1)
.map(item => ({
id: item.id,
value: item.name,
label: item.name
}));
}
} catch (error) {
console.error('获取检验类型列表失败:', error);
}
};
// 监听页面切换,当切换到检验项目页面时加载检验类型下拉框数据
watch(activeNav, (newVal) => {
if (newVal === 1 && testTypes.value.length === 0) {
// 切换到检验项目页面时,且检验类型数据未加载时才加载
loadInspectionTypes();
}
}, { immediate: true });
// 检验项目数据 - 从后端API获取
const inspectionItems = ref([]);
// 检验项目总数(用于后端分页)
const inspectionTotalCount = ref(0);
// 从后端API获取检验项目数据支持真正的查询功能
const loadObservationItems = async (resetPage = false) => {
try {
// 构建查询参数,将过滤条件传递给后端
const params = {
pageNo: resetPage ? 1 : inspectionCurrentPage.value,
pageSize: inspectionPageSize.value,
categoryCode: inspectionCategoryCode.value
};
// 添加搜索关键字(名称/编码)
if (nameFilter.value) {
params.searchKey = nameFilter.value;
}
// 添加检验类型过滤
if (testTypeFilter.value) {
const selectedType = testTypes.value.find(t => t.label === testTypeFilter.value);
if (selectedType) {
params.inspectionTypeId = selectedType.id;
}
}
const response = await getDiagnosisTreatmentList(params);
if (response.code === 200) {
let data = [];
let totalCount = 0;
if (response.data && response.data.records) {
data = response.data.records;
totalCount = response.data.total || data.length;
} else if (response.data && Array.isArray(response.data)) {
data = response.data;
totalCount = data.length;
}
// 更新总数用于分页
inspectionTotalCount.value = totalCount;
// 如果是重置页码,回到第一页
if (resetPage) {
inspectionCurrentPage.value = 1;
}
inspectionItems.value = data
// 过滤掉已停用的项目状态为3
.filter(item => item.statusEnum !== 3)
.map(item => ({
id: item.id,
code: item.busNo || '',
name: item.name || '',
testType: item.inspectionTypeId_dictText || item.testType || '', // 优先使用字典翻译字段
inspectionTypeId: item.inspectionTypeId || null, // 检验类型ID
package: '',
sampleType: item.specimenCode_dictText || '',
amount: parseFloat(item.retailPrice || 0),
sortOrder: item.sortOrder || null,
serviceRange: item.serviceRange || '全部',
sub医技Type: '',
remark: item.descriptionText || '',
status: true
}));
}
} catch (error) {
console.error('获取检验项目数据失败:', error);
}
};
// 过滤条件
const testTypeFilter = ref('');
const nameFilter = ref('');
const packageFilter = ref('');
// 过滤后的检验项目数据(仅保留费用套餐的前端过滤,其他过滤已由后端处理)
const filteredInspectionItems = computed(() => {
return inspectionItems.value.filter(item => {
// 按费用套餐过滤(费用套餐是前端自定义字段,需要前端过滤)
if (packageFilter.value && item.package !== packageFilter.value) {
return false;
}
return true;
});
});
// ==============================
// 分页(检验项目 tab- 后端分页
// ==============================
const inspectionCurrentPage = ref(1);
// 每页条数按需求固定为10
const inspectionPageSize = ref(10);
const inspectionTotalPages = computed(() => {
const total = inspectionTotalCount.value;
return Math.max(1, Math.ceil(total / inspectionPageSize.value));
});
// 由于改为后端分页,直接使用 filteredInspectionItems 即可
const pagedInspectionItems = computed(() => {
return filteredInspectionItems.value;
});
function inspectionGoPage(p) {
if (p < 1 || p > inspectionTotalPages.value) return;
inspectionCurrentPage.value = p;
// 后端分页:切换页码时重新加载数据
loadObservationItems();
}
// 执行过滤(真正的查询功能)
const filterItems = () => {
// 传递 true 表示重置到第一页并重新加载数据
loadObservationItems(true);
};
// 重置过滤条件
const resetFilters = () => {
testTypeFilter.value = '';
nameFilter.value = '';
packageFilter.value = '';
// 重置后重新加载数据
loadObservationItems(true);
};
// 套餐相关数据
const packageCategory = ref('检验套餐');
const packageLevel = ref('');
const packageLevelOptions = ref([
{ value: '全院套餐', label: '全院套餐' },
{ value: '科室套餐', label: '科室套餐' },
{ value: '个人套餐', label: '个人套餐' }
]);
const packageName = ref('');
const department = ref('');
const discount = ref('');
const isDisabled = ref(false);
const showPackageName = ref(true);
const generateServiceFee = ref(true);
const enablePackagePrice = ref(true);
const packageAmount = ref(0.00);
const serviceFee = ref(0.00);
const bloodVolume = ref('');
const remarks = ref('');
// 检验套餐明细项目 - 从后端API获取
const packageItems = ref([]);
// 从后端API获取检验项目数据并转换为套餐明细格式
const loadPackageItemsFromAPI = () => {
queryDiagnosisItems('', (results) => {
// 将API返回的检验项目转换为套餐明细格式
const formattedItems = results.map(item => ({
name: item.name,
dosage: '项/人',
route: '项/人',
frequency: '',
days: '',
quantity: 1,
unit: item.unit || '项',
unitPrice: parseFloat(item.retailPrice || 0.00),
amount: parseFloat(item.retailPrice || 0.00),
serviceFee: 0.00,
totalAmount: parseFloat(item.retailPrice || 0.00),
origin: ''
}));
// 只保留前几个项目作为示例
packageItems.value = formattedItems.slice(0, 10);
});
};
// 查询诊疗目录中的检验项目
const queryDiagnosisItems = (queryString, cb) => {
// 调用诊疗目录API查询检验类别的项目
const params = {
searchKey: queryString || '',
pageNo: 1,
pageSize: 100 // 增加分页大小,显示更多项目
};
getDiagnosisTreatmentList(params).then(response => {
// 处理不同的数据结构
let data;
if (response.data && response.data.records) {
data = response.data.records;
} else if (response.data && Array.isArray(response.data)) {
data = response.data;
} else if (response.data && response.data.rows) {
data = response.data.rows;
} else {
console.error('API返回数据格式不符合预期:', response.data);
return cb([]);
}
// 过滤出目录类别为检验的项目
// 支持多种可能的字段名
const inspectionItems = data.filter(item => {
return item.categoryCode_dictText === '检验' ||
item.categoryName === '检验' ||
item.category === '检验';
});
// 处理每个检验项目,确保有正确的字段映射
const results = inspectionItems.map(item => {
// 确保每个项目都有必要的字段
return {
value: item.name || item.itemName || item.drugName || '',
label: `${item.name || item.itemName || item.drugName || ''} - ${item.unit || item.usageUnit || '项'} - ¥${item.retailPrice || item.price || item.unitPrice || 0.00}`,
name: item.name || item.itemName || item.drugName || '',
unit: item.unit || item.usageUnit || '',
retailPrice: item.retailPrice || item.price || item.unitPrice || 0.00,
...item
};
});
cb(results);
}).catch(error => {
console.error('查询诊疗目录失败:', error);
ElMessage.error('查询诊疗目录失败,请稍后重试');
cb([]);
});
};
// 添加新的套餐项目
let addingItem = false;
const addPackageItem = () => {
if (addingItem) return; // 防止重复调用
addingItem = true;
const newItem = {
name: '',
dosage: '',
route: '',
frequency: '',
days: '',
quantity: 1,
unit: '',
unitPrice: 0.00,
amount: 0.00,
serviceFee: 0.00,
totalAmount: 0.00,
origin: ''
};
packageItems.value.push(newItem);
// 延迟重置标志位,确保不会影响其他操作
setTimeout(() => {
addingItem = false;
}, 100);
};
// 删除套餐项目
const deletePackageItem = (index) => {
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
packageItems.value.splice(index, 1);
calculateAmounts();
ElMessage.success('删除成功');
}).catch(() => {
// 取消删除
});
};
// 更新项目金额(考虑折扣)
const updateItemAmount = (item) => {
// 计算项目原价金额
const originalAmount = (item.quantity || 1) * (item.unitPrice || 0.00);
// 应用折扣到项目金额
let discountedAmount = originalAmount;
if (discount.value && !isNaN(parseFloat(discount.value))) {
const discountRate = parseFloat(discount.value) / 100;
discountedAmount = originalAmount * (1 - discountRate);
}
// 更新项目金额
item.amount = parseFloat(discountedAmount.toFixed(2));
// 基于折扣后的金额计算服务费
item.serviceFee = calculateItemServiceFee(item);
// 更新项目总金额
updateItemTotalAmount(item);
// 重新计算套餐金额和服务费
calculateAmounts();
};
// 更新项目总金额
const updateItemTotalAmount = (item) => {
item.totalAmount = (item.amount || 0.00) + (item.serviceFee || 0.00);
};
// 处理编辑项目
const handleEditItem = (index) => {
if (editingRowId.value !== null && editingRowId.value !== index) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
if (editingRowId.value === index) {
// 保存编辑 - 验证并计算金额
const item = packageItems.value[index];
if (!item.name || item.name.trim() === '') {
ElMessage.error('请输入项目名称');
return;
}
if (!item.unit || item.unit.trim() === '') {
ElMessage.error('请输入单位');
return;
}
if (item.quantity <= 0) {
ElMessage.error('数量必须大于0');
return;
}
if (item.unitPrice <= 0) {
ElMessage.error('单价必须大于0');
return;
}
// 重新计算金额
updateItemAmount(item);
editingRowId.value = null;
ElMessage.success('保存成功');
} else {
// 进入编辑模式
editingRowId.value = index;
}
};
// 取消编辑项目
const cancelEditItem = (index) => {
editingRowId.value = null;
ElMessage.info('已取消编辑');
};
// 计算单个项目的服务费(基于折扣后的金额)
const calculateItemServiceFee = (item) => {
if (!generateServiceFee.value) return 0;
// 服务费是项目折扣后金额的10%
return parseFloat((item.amount * 0.1).toFixed(2));
};
// 重新分配所有项目的服务费(基于折扣后的金额)
const redistributeServiceFee = () => {
if (!generateServiceFee.value) {
// 如果不生成服务费将所有项目的服务费设为0
packageItems.value.forEach(item => {
item.serviceFee = 0;
updateItemTotalAmount(item);
});
return;
}
// 重新计算每个项目的服务费
packageItems.value.forEach(item => {
item.serviceFee = calculateItemServiceFee(item);
updateItemTotalAmount(item);
});
};
// 计算套餐金额和服务费
const calculateAmounts = () => {
// 更新每个项目的折扣金额
packageItems.value.forEach(item => {
// 计算项目原价金额
const originalAmount = (item.quantity || 1) * (item.unitPrice || 0.00);
// 应用折扣到项目金额
let discountedAmount = originalAmount;
if (discount.value && !isNaN(parseFloat(discount.value))) {
const discountRate = parseFloat(discount.value) / 100;
discountedAmount = originalAmount * (1 - discountRate);
}
// 更新项目金额
item.amount = parseFloat(discountedAmount.toFixed(2));
});
// 重新分配所有项目的服务费
redistributeServiceFee();
// 计算套餐总金额(基于项目的折扣后金额)
const totalAmount = packageItems.value.reduce((sum, item) => sum + (item.amount || 0), 0);
// 更新套餐金额
packageAmount.value = parseFloat(totalAmount.toFixed(2));
// 计算套餐总服务费
if (generateServiceFee.value) {
serviceFee.value = parseFloat(packageItems.value.reduce((sum, item) => sum + (item.serviceFee || 0), 0).toFixed(2));
} else {
serviceFee.value = 0;
}
};
// 检验类型相关方法
// 处理表格点击事件,用于自动删除空的编辑行
const handleTableClick = (event) => {
// 如果点击的是表格内部的元素,不处理
if (event.target.closest('input, select, button, .action-btn')) {
return;
}
// 如果当前有正在编辑的行,检查是否为空
if (editingRowId.value) {
const editingRow = tableData.value.find(row => row.id === editingRowId.value);
if (editingRow && (!editingRow.code || editingRow.code.trim() === '') &&
(!editingRow.name || editingRow.name.trim() === '') &&
(!editingRow.department || editingRow.department.trim() === '')) {
// 删除空的编辑行
const index = tableData.value.findIndex(row => row.id === editingRowId.value);
if (index !== -1) {
tableData.value.splice(index, 1);
editingRowId.value = null;
ElMessage.info('已自动删除空行');
}
}
}
};
const addNewRow = () => {
if (editingRowId.value) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
// department 在表格里是字符串(科室名称);这里不要直接塞对象,否则后续 trim/校验会异常
const defaultDeptName = departments.value?.[0]?.name || '';
// 新增大类时parentId 为 null
const newRow = { id: `temp_${Date.now()}`, code: '', name: '', department: defaultDeptName, sortOrder: tableData.value.length + 1, remark: '', parentId: null };
tableData.value.push(newRow);
editingRowId.value = newRow.id;
};
const handleEdit = (row) => {
if (editingRowId.value && editingRowId.value !== row.id) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
editingRowId.value = row.id;
};
const handleConfirm = (row) => {
// 子类序号必须与主类相同,且不可更改:保存时强制回填
const parts = parseCodeParts(row?.code);
if (parts.subRaw) {
const parentCode = parts.mainRaw;
const parent = tableData.value.find(r => (r?.code ?? '').toString().trim() === parentCode);
if (parent) {
row.sortOrder = parent.sortOrder;
}
}
// 准备提交给后端的数据保留sortOrder字段名后端会自动映射到数据库的order字段
const submitData = {
...row,
// 确保sortOrder字段存在且为数字类型
sortOrder: row.sortOrder ? Number(row.sortOrder) : 0
};
// 兼容 department 可能是对象的情况(例如 el-tree-select 返回节点对象)
if (submitData.department && typeof submitData.department === 'object') {
submitData.department = submitData.department.name || submitData.department.label || '';
}
// 验证必填字段:为空则提示并保留该行(不要直接删行,否则用户”点确定就没了”体验很差)
if (!submitData.code || submitData.code.trim() === '') {
ElMessage.warning('请输入大类编码');
return;
}
if (!submitData.name || submitData.name.trim() === '') {
ElMessage.warning('请输入大类项目名称');
return;
}
if (!submitData.department || String(submitData.department).trim() === '') {
ElMessage.warning('请选择执行科室');
return;
}
// 注意:编码唯一性由后端统一校验;这里不做硬编码拦截(避免误判)
// 去除code字段的前后空格确保唯一性验证准确
submitData.code = submitData.code.trim();
// 区分新增和更新操作:使用 temp_ 前缀的临时ID判断避免时间戳阈值在不同年份失效
if (isTempId(row.id)) { // 新增的临时ID
// 处理 parentId如果是子类且 parentId 是临时ID需要先找到父类的真实 id
let finalParentId = submitData.parentId;
if (finalParentId && isTempId(finalParentId)) {
// parentId 是临时ID说明父类还没保存这种情况不应该出现因为要求先保存父类
// 但为了容错,尝试通过 code 找到父类
const parts = parseCodeParts(submitData.code);
if (parts.subRaw) {
const parentCode = parts.mainRaw;
const parent = tableData.value.find(r =>
!isTempId(r.id) && (r?.code ?? '').toString().trim() === parentCode
);
if (parent) {
finalParentId = parent.id;
} else {
ElMessage.warning('请先保存父类,再保存子类');
return;
}
}
}
// 新增数据时移除临时ID让后端自动生成主键
const { id, ...newData } = submitData;
newData.parentId = finalParentId || null; // 确保 parentId 正确设置(大类为 null子类为父类 id
addInspectionType(newData).then(response => {
ElMessage.success('新增成功');
getInspectionTypeList();
}).catch(error => {
if (error.response && error.response.data) {
ElMessage.error(error.response.data.msg || '新增失败');
} else {
ElMessage.error('新增失败,请重试');
}
});
} else { // 更新操作
updateInspectionType(submitData).then(response => {
ElMessage.success('保存成功');
getInspectionTypeList();
}).catch(error => {
if (error.response && error.response.data) {
ElMessage.error(error.response.data.msg || '保存失败');
} else {
ElMessage.error('保存失败,请重试');
}
});
}
editingRowId.value = null;
};
const handleAdd = (row, index) => {
if (editingRowId.value) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
// 行内“+”:在当前大类下新增子类,编码按“父类编码-001”递增生成
const parentCode = (row?.code ?? '').toString().trim();
if (!parentCode) {
ElMessage.warning('请先填写并保存当前大类编码,再新增子类');
return;
}
// 找到该父类下已有的子类编码,取最大序号 + 1
// 支持如1-001、01-002 等
const prefix = `${parentCode}-`;
let maxSeq = 0;
for (const r of tableData.value) {
const code = (r?.code ?? '').toString();
if (code.startsWith(prefix)) {
const suffix = code.slice(prefix.length);
const n = Number.parseInt(suffix, 10);
if (!Number.isNaN(n)) {
maxSeq = Math.max(maxSeq, n);
}
}
}
const nextSeq = maxSeq + 1;
const childCode = `${parentCode}-${String(nextSeq).padStart(3, '0')}`;
// 新增子类时parentId 指向父类的 id如果父类已保存有真实 id否则先存父类的临时 id保存时再处理
const newRow = {
id: `temp_${Date.now()}`,
code: childCode,
name: '',
department: row.department,
// 序号字段保持原逻辑:插入到父类下一行,默认沿用父类序号
sortOrder: row.sortOrder,
remark: '',
parentId: row.id // 子类的 parentId 指向父类的 id可能是真实 id 或临时 id
};
tableData.value.splice(index + 1, 0, newRow);
editingRowId.value = newRow.id;
};
const handleDelete = (id) => {
ElMessageBox.confirm('确定要删除该检验类型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 确保ID是数字类型
const numericId = Number(id);
// 判断是否为临时ID前端 temp_ 前缀)
const isTemporaryId = isTempId(id);
if (!isTemporaryId) { // 真实数据库ID
delInspectionType(numericId).then(() => {
ElMessage.success('删除成功');
getInspectionTypeList();
}).catch(error => {
console.error('删除检验类型失败:', error);
ElMessage.error('删除失败: ' + (error.response?.data?.msg || '未知错误'));
});
} else {
// 删除临时新增的行
tableData.value = tableData.value.filter(row => row.id !== id);
ElMessage.success('删除成功');
}
}).catch(() => {
// 取消删除
});
};
// 检验项目相关方法
const addNewItem = () => {
if (editingRowId.value) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
const newItem = {
id: Date.now(),
code: '',
name: '',
testType: '',
package: '',
sampleType: '',
amount: 0.00,
sortOrder: inspectionItems.value.length + 1,
serviceRange: '全部',
sub医技Type: '',
remark: '',
status: true
};
inspectionItems.value.push(newItem);
editingRowId.value = newItem.id;
};
const editItem = (item) => {
if (editingRowId.value === item.id) {
// 如果当前行已经在编辑模式,点击编辑按钮则保存
saveItem(item);
} else {
// 否则进入编辑模式
editingRowId.value = item.id;
}
};
const updateAmountFromPackage = (item) => {
if (item.package) {
const selectedPackage = feePackages.value.find(pkg => pkg.id === item.package);
if (selectedPackage) {
// 套餐总金额 = 套餐金额 + 服务费
const packageAmount = parseFloat(selectedPackage.packageAmount || 0);
const serviceFee = parseFloat(selectedPackage.serviceFee || 0);
item.amount = parseFloat((packageAmount + serviceFee).toFixed(2));
}
} else {
item.amount = 0.00;
}
};
const saveItem = async (item) => {
// 验证必填字段
if (!item.code || item.code.trim() === '') {
ElMessage.error('小类编码不能为空');
return;
}
// 验证小类编码格式4位数字
const codeRegex = /^\d{4}$/;
if (!codeRegex.test(item.code.trim())) {
ElMessage.error('小类编码必须为4位数字');
return;
}
if (!item.name || item.name.trim() === '') {
ElMessage.error('小类项目名称不能为空');
return;
}
if (!item.testType) {
ElMessage.error('检验类型不能为空');
return;
}
if (!item.sampleType) {
ElMessage.error('样本类型不能为空');
return;
}
// 验证小类编码唯一性
const isDuplicate = inspectionItems.value.some(i =>
i.id !== item.id && i.code.trim() === item.code.trim()
);
if (isDuplicate) {
ElMessage.error('小类编码已存在');
return;
}
// 从费用套餐获取金额
updateAmountFromPackage(item);
try {
// 准备提交给后端的数据
const submitData = {
busNo: item.code.trim(),
name: item.name.trim(),
categoryCode: inspectionCategoryCode.value,
inspectionTypeId: item.inspectionTypeId || null, // 检验类型ID
specimenCode: item.sampleType,
retailPrice: item.amount,
descriptionText: item.remark,
typeEnum: 1,
statusEnum: 2,
sortOrder: item.sortOrder ? parseInt(item.sortOrder) : null,
serviceRange: item.serviceRange || '全部'
};
// 判断是新增还是更新
if (typeof item.id === 'number') { // 临时ID数字类型新增
const response = await addDiagnosisTreatment(submitData);
if (response.code === 200) {
ElMessage.success('添加成功');
await loadObservationItems();
} else {
ElMessage.error(response.msg || '添加失败');
}
} else { // 真实ID字符串类型更新
submitData.id = item.id;
const response = await editDiagnosisTreatment(submitData);
if (response.code === 200) {
ElMessage.success('更新成功');
await loadObservationItems();
} else {
ElMessage.error(response.msg || '更新失败');
}
}
editingRowId.value = null;
} catch (error) {
console.error('保存检验项目失败:', error);
ElMessage.error('保存失败,请稍后重试');
}
};
const deleteItem = async (id) => {
ElMessageBox.confirm('确定要删除该检验项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const response = await stopDiseaseTreatment([id]);
if (response.code === 200) {
ElMessage.success('删除成功');
await loadObservationItems();
} else {
ElMessage.error(response.msg || '删除失败');
}
} catch (error) {
console.error('删除检验项目失败:', error);
ElMessage.error('删除失败,请稍后重试');
}
}).catch(() => {
});
};
const cancelEdit = (item) => {
// 如果是新添加的行,则直接删除
if (item.id.toString().length > 10) { // 临时ID使用Date.now()生成
const index = inspectionItems.value.findIndex(i => i.id === item.id);
if (index !== -1) {
inspectionItems.value.splice(index, 1);
}
}
editingRowId.value = null;
};
// 导出表格数据
const exportTable = () => {
// 将表格数据转换为CSV格式
const headers = ['行号', '小类编码', '小类项目名称', '检验类型', '费用套餐', '样本类型', '金额', '序号', '服务范围', '下级医技类型', '备注'];
const csvContent = [
headers.join(','),
...filteredInspectionItems.value.map((item, index) => [
index + 1,
`"${item.code}"`,
`"${item.name}"`,
`"${item.testType}"`,
`"${item.package}"`,
`"${item.sampleType}"`,
item.amount,
item.sortOrder || 999999,
`"${item.serviceRange || '全部'}"`,
`"${item.sub医技Type || '-'}"`,
`"${item.remark || '-'}"`
].join(','))
].join('\n');
// 创建Blob对象
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// 创建下载链接
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `检验项目导出_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
// 添加到DOM并触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success('导出成功');
};
// 套餐相关方法
const handleSave = () => {
// 验证必填字段
if (!packageLevel.value) {
ElMessage.error('请选择套餐级别');
return;
}
if (!packageName.value || packageName.value.trim() === '') {
ElMessage.error('请输入套餐名称');
return;
}
if (packageItems.value.length === 0) {
ElMessage.error('请至少添加一个检验项目');
return;
}
// 验证套餐明细数据
for (let i = 0; i < packageItems.value.length; i++) {
const item = packageItems.value[i];
if (!item.name || item.name.trim() === '') {
ElMessage.error(`第${i + 1}行:请输入项目名称`);
return;
}
if (!item.unit || item.unit.trim() === '') {
ElMessage.error(`第${i + 1}行:请输入单位`);
return;
}
if (item.quantity <= 0) {
ElMessage.error(`第${i + 1}行数量必须大于0`);
return;
}
if (item.unitPrice <= 0) {
ElMessage.error(`第${i + 1}行单价必须大于0`);
return;
}
}
// 准备基本信息数据
const basicInfo = {
packageCategory: packageCategory.value,
packageLevel: packageLevel.value,
packageName: packageName.value.trim(),
department: department.value,
discount: discount.value || 0,
isDisabled: isDisabled.value,
showPackageName: showPackageName.value,
generateServiceFee: generateServiceFee.value,
enablePackagePrice: enablePackagePrice.value,
packageAmount: packageAmount.value,
serviceFee: serviceFee.value,
lisGroup: selectedLisGroup.value, // 从下拉框获取
bloodVolume: bloodVolume.value,
remarks: remarks.value,
orgName: userStore.orgName || '测试机构', // 卫生机构
createBy: userStore.nickName, // 制单人
createTime: new Date().toISOString(),
updateTime: new Date().toISOString()
};
// 根据套餐级别设置用户信息
if (packageLevel.value === '科室套餐' && department.value) {
// 科室套餐:设置科室信息
basicInfo.departmentId = department.value;
} else if (packageLevel.value === '个人套餐') {
// 个人套餐设置用户ID套餐所有者
basicInfo.userId = userStore.userId || userStore.nickName;
}
// 全院套餐:不需要额外设置用户信息
// 准备明细数据
const detailData = packageItems.value.map(item => ({
packageName: packageName.value.trim(),
itemName: item.name,
dosage: item.dosage,
route: item.route,
frequency: item.frequency,
days: item.days,
quantity: item.quantity,
unit: item.unit,
unitPrice: item.unitPrice,
amount: item.amount,
serviceFee: item.serviceFee,
totalAmount: item.totalAmount,
origin: item.origin,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString()
}));
// 调用保存API暂时使用模拟保存后续对接真实API
savePackageData(basicInfo, detailData);
};
// 保存套餐数据到数据库
const savePackageData = async (basicInfo, detailData) => {
// 显示保存进度
const loading = ElLoading.service({
lock: true,
text: '正在保存...',
background: 'rgba(0, 0, 0, 0.7)',
});
try {
// 1. 先保存基本信息
const basicResponse = await addInspectionPackage(basicInfo);
// 检查响应码
if (basicResponse.code !== 200) {
loading.close();
throw new Error(basicResponse.msg || '保存基本信息失败');
}
// 检查响应数据结构 - 兼容多种可能的响应格式
let packageId = null;
if (basicResponse.data) {
// 标准格式:{code: 200, data: {packageId: xxx}}
packageId = basicResponse.data.packageId || basicResponse.data.id;
} else if (basicResponse.packageId) {
// 如果data不存在尝试直接从响应根级别获取
packageId = basicResponse.packageId;
} else if (basicResponse.id) {
// 如果data不存在尝试直接从响应根级别获取id
packageId = basicResponse.id;
}
// 验证套餐ID是否存在
if (!packageId) {
loading.close();
console.error('无法从响应中获取套餐ID完整响应:', basicResponse);
throw new Error('保存成功但未返回套餐ID。请检查后端接口是否正确返回了packageId或id字段');
}
// 2. 分别保存每个明细数据到明细表
for (let i = 0; i < detailData.length; i++) {
const detailItem = {
...detailData[i],
packageId: packageId
};
const detailResponse = await saveInspectionPackageDetails(detailItem);
if (detailResponse.code !== 200) {
loading.close();
throw new Error(`保存第 ${i + 1} 个明细项失败: ${detailResponse.msg || '未知错误'}`);
}
}
// 关闭加载提示
loading.close();
ElMessage.success('保存成功');
// 保存成功后重置表单
doResetForm();
} catch (error) {
// 确保在错误时也关闭loading
loading.close();
console.error('保存失败:', error);
// 处理不同类型的错误
let errorMessage = '保存失败,请重试';
if (error.response) {
// 服务器返回错误状态码
const status = error.response.status;
const data = error.response.data;
if (status === 400) {
errorMessage = data.msg || '请求参数错误';
} else if (status === 401) {
errorMessage = '未授权,请重新登录';
} else if (status === 403) {
errorMessage = '没有权限执行此操作';
} else if (status === 500) {
errorMessage = '服务器内部错误';
} else {
errorMessage = data.msg || `请求失败 (${status})`;
}
} else if (error.request) {
// 网络错误
errorMessage = '网络连接失败,请检查网络设置';
} else {
// 其他错误包括我们throw的Error
errorMessage = error.message || '保存失败,请重试';
}
ElMessage.error(errorMessage);
}
};
// 重置表单(带确认对话框)
const resetForm = () => {
ElMessageBox.confirm('确定要重置表单吗?所有未保存的数据将丢失。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
doResetForm();
ElMessage.success('表单已重置');
}).catch(() => {
// 用户取消重置
});
};
// 执行表单重置(不带确认对话框,用于保存成功后自动重置)
const doResetForm = () => {
// 重置基本信息
packageLevel.value = '';
packageName.value = '';
department.value = '';
discount.value = '';
isDisabled.value = false;
showPackageName.value = true;
generateServiceFee.value = true;
enablePackagePrice.value = true;
packageAmount.value = 0.00;
serviceFee.value = 0.00;
selectedLisGroup.value = '';
bloodVolume.value = '';
remarks.value = '';
// 清空明细数据
packageItems.value = [];
// 重新计算金额
calculateAmounts();
};
// 加载检验套餐数据(用于编辑现有套餐)
const loadInspectionPackage = async (packageId) => {
try {
const loading = ElLoading.service({
lock: true,
text: '正在加载套餐数据...',
background: 'rgba(0, 0, 0, 0.7)',
});
// 获取基本信息
const basicResponse = await getInspectionPackage(packageId);
if (basicResponse.code !== 200) {
throw new Error(basicResponse.msg || '加载基本信息失败');
}
// 获取明细数据
const detailResponse = await listInspectionPackageDetails(packageId);
if (detailResponse.code !== 200) {
throw new Error(detailResponse.msg || '加载明细数据失败');
}
const basicData = basicResponse.data;
const detailData = detailResponse.data || [];
// 填充基本信息
packageLevel.value = basicData.packageLevel;
packageName.value = basicData.packageName;
department.value = basicData.department;
discount.value = basicData.discount || '';
isDisabled.value = basicData.isDisabled || false;
showPackageName.value = basicData.showPackageName !== false;
generateServiceFee.value = basicData.generateServiceFee !== false;
enablePackagePrice.value = basicData.enablePackagePrice !== false;
packageAmount.value = basicData.packageAmount || 0.00;
serviceFee.value = basicData.serviceFee || 0.00;
selectedLisGroup.value = basicData.lisGroup || '';
bloodVolume.value = basicData.bloodVolume || '';
remarks.value = basicData.remarks || '';
// 填充明细数据
packageItems.value = detailData.map(item => ({
name: item.itemName || item.name,
dosage: item.dosage || '',
route: item.route || '',
frequency: item.frequency || '',
days: item.days || '',
quantity: item.quantity || 1,
unit: item.unit || '',
unitPrice: parseFloat(item.unitPrice || 0),
amount: parseFloat(item.amount || 0),
serviceFee: parseFloat(item.serviceFee || 0),
totalAmount: parseFloat(item.totalAmount || 0),
origin: item.origin || ''
}));
loading.close();
ElMessage.success('套餐数据加载成功');
} catch (error) {
console.error('加载套餐数据失败:', error);
ElMessage.error(error.message || '加载套餐数据失败');
}
};
const handlePackageManagement = () => {
// 跳转到套餐管理页面
router.push({
path: '/maintainSystem/Inspection/PackageManagement'
});
};
const refreshPage = () => {
getInspectionTypeList();
// 刷新时也重新加载套餐项目
loadPackageItemsFromAPI();
};
// 页面加载时获取数据
onMounted(() => {
getInspectionTypeList();
getLisGroupList();
// 加载检验套餐明细项目
loadPackageItemsFromAPI();
// 初始化计算套餐金额和服务费
calculateAmounts();
});
// 监听检验分类代码,当字典数据加载完成后加载检验项目数据
watch(activity_category_code, (newVal) => {
if (newVal && newVal.length > 0 && inspectionItems.value.length === 0) {
loadObservationItems();
}
}, { immediate: true });
/**
* 关键修复:
* 从“套餐管理”跳转到这里时,通常只是改变 querypackageId/mode/tab组件不会重新挂载
* 所以仅靠 onMounted 读取一次 query 会导致“查看/修改”出现空白,刷新才正常。
* 这里监听 query 变化,自动切换 tab 并加载套餐数据。
*/
watch(
() => route.query.tab,
(tab) => {
if (tab === '0' || tab === '1' || tab === '2') {
activeNav.value = parseInt(String(tab));
}
},
{ immediate: true }
);
const applyRouteForPackage = async (query) => {
const packageId = query?.packageId;
if (!packageId) return;
activeNav.value = 2;
await nextTick();
loadInspectionPackage(String(packageId));
};
watch(
() => route.query.packageId,
async () => {
await applyRouteForPackage(route.query);
},
{ immediate: true, flush: 'post' }
);
// 兜底:如果该页面被 keep-alive 缓存,从别的页面返回时不会触发 onMounted
onActivated(() => {
applyRouteForPackage(route.query);
});
// 兜底:同一路由复用/仅 query 变化时,确保能触发加载
onBeforeRouteUpdate((to) => {
applyRouteForPackage(to.query);
});
// 监听生成服务费选项变更
watch(generateServiceFee, (newVal) => {
calculateAmounts();
});
// 监听套餐项目变化
watch(packageItems, (newVal) => {
calculateAmounts();
}, { deep: true });
</script>
<style>
/* Element UI 表格编辑行样式 */
.el-table .editing-row {
background-color: #fdf6ec !important;
}
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* 整体布局 */
.inspection-container {
width: 100%;
height: 100vh;
background-color: #f5f7fa;
}
/* 左侧边栏 */
.side-aside {
background-color: #fff;
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.05);
transition: width 0.3s;
overflow: hidden;
}
/* 左侧导航 */
.side-nav {
border-right: none;
height: 100%;
}
/* 右侧主内容 */
.main-content {
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
background-color: #f5f7fa;
}
/* 页面标题 */
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 头部操作按钮 */
.header-actions {
display: flex;
justify-content: flex-start;
margin-bottom: 20px;
}
/* 过滤区域 */
.filter-section {
width: 100%;
background-color: #fff;
padding: 16px;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 16px;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-item label {
font-size: 14px;
color: #333;
white-space: nowrap;
font-weight: 500;
}
.filter-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
/* 表格容器 */
.table-container {
width: 100%;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
overflow: hidden;
margin-bottom: 20px;
}
/* 数据表格 */
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
height: 36px;
padding: 0 12px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.data-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.data-table tr:hover {
background-color: #fafafa;
}
.data-table tr.editing {
background-color: #f0f7ff;
box-shadow: 0 0 0 1px #1890ff;
}
/* 操作列 */
.action-cell {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
position: relative;
z-index: 10;
height: 100%;
}
/* 套餐设置样式 */
.top-bar {
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #e8e8e8;
background: #fff;
margin-bottom: 16px;
gap: 16px;
}
.action-group {
display: flex;
gap: 12px;
}
/* 表单区域 */
.form-section {
width: 100%;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
padding: 16px;
margin-bottom: 16px;
box-sizing: border-box;
}
.section-title {
font-weight: 700;
font-size: 14px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
/* 弹性布局表单 */
.form-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.form-item {
display: flex;
align-items: center;
/* 每行4个总宽度 = 100% - 3*16px(gap) = 52px每项 = (100% - 48px) / 4 */
width: calc((100% - 48px) / 4);
min-width: 180px;
box-sizing: border-box;
}
.form-label {
width: 80px;
text-align: right;
padding-right: 12px;
color: #666;
flex-shrink: 0;
}
/* 响应式设计 */
@media (max-width: 1400px) {
/* 每行3个总gap = 2*16px = 32px */
.form-item {
width: calc((100% - 32px) / 3);
}
}
@media (max-width: 1200px) {
/* 每行2个总gap = 1*16px = 16px */
.form-item {
width: calc((100% - 16px) / 2);
}
}
@media (max-width: 992px) {
.side-aside {
display: none;
}
.form-item {
width: calc((100% - 16px) / 2);
}
}
@media (max-width: 768px) {
.form-item {
width: 100%;
min-width: 0;
}
.filter-section {
flex-direction: column;
gap: 12px;
}
.filter-item {
width: 100%;
}
.filter-actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
gap: 8px;
}
}
/* 表格内操作按钮 */
.table-actions {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
flex-wrap: nowrap;
}
/* 编辑模式行样式 */
.el-table .editing-row {
background-color: #f0f7ff;
}
</style>