Files
his/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue
duzhongxu 6d23d36a9c 211
检验项目设置-》套餐管理:点击【新增】跳转至套餐设置界面系统未进行初始化新增模式界面数据
212
检验项目设置-》套餐管理:点击行【编辑】跳转至套餐设置编辑模式该行的套餐数据未正确引入
213
检验项目设置-》套餐管理:点击行【查看】跳转至套餐设置界面套餐内容显示错误并且未进入只读模式
2026-03-27 16:39:41 +08:00

3046 lines
100 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%;" filterable
clearable
@change="(val) => handleParentChange(val, row)">
<el-option label="选择检验类型" value="" />
<!-- 使用 name 作为显示id 作为值 -->
<el-option
v-for="item in parentTypeOptions"
:key="item.id"
:label="item.name"
:value="item.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.feePackageId" placeholder="选择费用套餐" size="small" style="width: 100%;" filterable clearable
@change="(val) => { const pkg = feePackages.find(p => String(p.id) === String(val)); row.package = pkg ? pkg.packageName : ''; 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="String(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-select
v-model="row.subItemId"
placeholder="请先选大类"
size="small"
style="width: 100%"
filterable
clearable
:disabled="!row.inspectionTypeId"
:loading="row.loadingSubItems"
@change="handleEditRow"
>
<!-- 数据源来自行内动态加载的 subItemOptions -->
<el-option
v-for="item in (row.subItemOptions || [])"
:key="item.id"
:label="item.name"
:value="Number(item.id)"
/>
</el-select>
</template>
<template v-else>
{{ row.subItemName || '-' }}
</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 v-if="!isViewMode" type="success" size="large" @click="handleSave">{{ packageMode === 'edit' ? '更新' : '保存' }}</el-button>
<el-button v-else type="primary" size="large" @click="handleBackToManagement">返回管理</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%;"
:disabled="isViewMode"
@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
:disabled="isViewMode"
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="请输入套餐名称" :disabled="isViewMode" />
<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-select
v-model="selectedTenantId"
placeholder="请选择卫生机构"
style="width: 100%;"
@change="handleTenantChange"
clearable
filterable
:disabled="isViewMode"
:loading="loadingTenant"
no-data-text="暂无数据请稍后重试"
@visible-change="handleTenantVisibleChange"
>
<el-option
v-for="item in tenantOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</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" :disabled="isViewMode" />
</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="请输入备注" :disabled="isViewMode" />
</div>
<div class="form-item">
<span class="form-label">是否停用</span>
<el-radio-group v-model="isDisabled" :disabled="isViewMode">
<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" :disabled="isViewMode">
<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" :disabled="isViewMode">
<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" :disabled="isViewMode">
<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%;" :disabled="isViewMode">
<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" :disabled="isViewMode" />
</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 v-if="!isViewMode" 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="200">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-autocomplete
v-model="row.name"
:fetch-suggestions="(query, cb) => handleProjectInlineSearch(query, cb, row)"
placeholder="输入名称或首字母搜索"
size="small"
clearable
highlight-first-item
:debounce="300"
style="width: 100%;"
popper-class="project-inline-dropdown"
@select="(item) => handleProjectInlineSelect(item, row)"
@clear="() => { row.name = ''; row.unitPrice = 0; row.unit = ''; row.amount = 0; row.totalAmount = row.serviceFee || 0; }"
>
<template #default="{ item }">
<div class="project-inline-item">
<span class="item-name">{{ item.name }}</span>
<span class="item-code" v-if="item.code">{{ item.code }}</span>
<span class="item-price">¥{{ item.retailPrice || 0 }}</span>
<span class="item-unit">{{ item.permittedUnitCode_dictText || item.unit || '' }}</span>
</div>
</template>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-autocomplete>
</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="80" align="center">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<el-select
v-model="row.unit"
placeholder="单位"
size="small"
allow-create
filterable
default-first-option
style="width: 70px;"
>
<!-- 使用 v-for 遍历外部定义的 unitOptions -->
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<template v-else>
{{ row.unit || '-' }}
</template>
</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, onUnmounted, 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,
getDiseaseTreatmentInit
} from '@/views/catalog/diagnosistreatment/components/diagnosistreatment';
import {listLisGroup} from '@/api/system/checkType';
import {
addInspectionPackage,
updateInspectionPackage,
getInspectionPackage,
listInspectionPackageDetails,
saveInspectionPackageDetails,
batchSaveInspectionPackageDetails
} from '@/api/system/inspectionPackage';
import { getTenantPage } from '@/api/system/tenant';
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 pagedTypeRows = computed(() => {
const start = (typeCurrentPage.value - 1) * typePageSize.value;
return tableData.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;
}
*/
};
// 获取当前登录用户信息
// --- 新增:存储租户/机构列表和当前选中的值 ---
const tenantOptions = ref([]); // 存储从后端获取的租户列表
const selectedTenantId = ref(null); // 存储当前选中的租户ID
// --- 新增:控制租户列表加载状态 ---
const loadingTenant = ref(false); // 控制下拉框的加载状态
// 展开时若列表为空则加载数据
const handleTenantVisibleChange = async (visible) => {
if (visible && tenantOptions.value.length === 0) { // 仅在展开且列表为空时加载
await fetchTenantList();
}
};
/**
* 获取数据方法
* 这里不再接收搜索关键词,而是直接拉取所有启用的套餐
* 如果需要后端支持模糊搜索,可以配合 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
// ==============================
// 【修复】级联下拉框逻辑 (包含直接请求代码)
// ==============================
// 1. 定义响应式数据
const parentTypeOptions = ref([]);
// 2. 【核心】直接发送请求的方法
// 这里直接使用 request 工具,不依赖外部 api 文件
const fetchInspectionTypesRequest = (params) => {
return request({
url: '/system/inspection-type/list', // 你的后端接口地址
method: 'get',
params: params
});
};
// 3. 加载所有顶级大类
const loadParentTypes = async () => {
try {
// 【发送请求】不传参数,获取所有数据
const res = await fetchInspectionTypesRequest({});
let allData = [];
// 兼容多种返回格式
if (res.code === 200) {
if(res.data && Array.isArray(res.data)) allData = res.data;
else if(res.data && res.data.rows) allData = res.data.rows;
else if(res.data && res.data.data && Array.isArray(res.data.data)) allData = res.data.data;
}
// 过滤:只保留 parentId 为 null/undefined 的记录作为“大类”
parentTypeOptions.value = allData.filter(item =>
item.parentId === null || item.parentId === undefined || item.parentId === ''
);
} catch (error) {
ElMessage.error('网络异常,加载大类失败');
}
};
// 4. 处理大类改变,联动加载子类
const handleParentChange = async (selectedId, row) => {
// A. 更新当前行的显示名称
const selected = parentTypeOptions.value.find(t => t.id === selectedId);
row.inspectionTypeName = selected ? selected.name : '';
// B. 【重要】重置下级数据
row.subItemId = '';
row.subItemName = '';
row.subItemOptions = [];
if (!selectedId) return;
// C. 设置加载状态
row.loadingSubItems = true;
try {
// 【发送请求】传入 parentId 参数获取子类
const res = await fetchInspectionTypesRequest({ parentId: selectedId });
let subData = [];
if (res.code === 200) {
if(res.data && Array.isArray(res.data)) subData = res.data;
else if(res.data && res.data.rows) subData = res.data.rows;
else if(res.data && res.data.data && Array.isArray(res.data.data)) subData = res.data.data;
row.subItemOptions = subData;
// 可选优化:如果子类只有一个,自动选中
if (subData.length === 1) {
row.subItemId = subData[0].id;
row.subItemName = subData[0].name;
}
} else {
ElMessage.info('该分类下暂无子项目');
}
} catch (error) {
ElMessage.error('加载子项目失败');
} finally {
row.loadingSubItems = false;
}
};
// 5. 点击编辑按钮时触发
const handleEditRow = (row) => {
if (editingRowId.value && editingRowId.value !== row.id) {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
editingRowId.value = row.id;
// 初始化行内属性
if (!row.subItemOptions) row.subItemOptions = [];
// 预加载费用套餐列表确保下拉框能显示名称而不是ID
if (feePackages.value.length === 0) {
getFeePackages();
}
// 如果已有子项ID但选项列表为空且有大类ID则主动加载子类列表
if (row.inspectionTypeId && row.subItemOptions.length === 0) {
row.loadingSubItems = true;
fetchInspectionTypesRequest({ parentId: row.inspectionTypeId }).then(res => {
let subData = [];
if (res.code === 200) {
if (res.data && Array.isArray(res.data)) subData = res.data;
else if (res.data && res.data.rows) subData = res.data.rows;
else if (res.data && res.data.data && Array.isArray(res.data.data)) subData = res.data.data;
}
row.subItemOptions = subData;
}).finally(() => {
row.loadingSubItems = false;
});
}
// 回显检验大类名称
if (row.inspectionTypeId && !row.inspectionTypeName) {
const p = parentTypeOptions.value.find(i => i.id === row.inspectionTypeId);
if (p) row.inspectionTypeName = p.name;
}
};
// 样本类型数据
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 ? Number(item.inspectionTypeId) : null,
package: item.packageName || '',
feePackageId: item.feePackageId ? String(item.feePackageId) : null,
sampleType: item.specimenCode || '',
amount: parseFloat(item.retailPrice || 0),
sortOrder: item.sortOrder || null,
serviceRange: item.serviceRange || '全部',
subItemName: item.subItemName || '',
subItemId: item.subItemId ? Number(item.subItemId) : null,
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 => {
// 按费用套餐过滤(用 feePackageId 与选中的套餐 id 比对)
if (packageFilter.value && String(item.feePackageId) !== String(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([]);
// 套餐当前模式: add-新增, edit-编辑, view-查看
const packageMode = ref('add');
// 当前编辑的套餐ID用于编辑和查看模式
const currentPackageId = ref(null);
// 正在从后端加载套餐数据的标志,加载期间跳过 calculateAmounts 防止覆盖后端金额
const isLoadingPackage = ref(false);
// 是否为查看模式(只读)
const isViewMode = computed(() => packageMode.value === 'view');
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);
editingRowId.value = packageItems.value.length - 1; // 进入编辑模式
// 【关键】等待 DOM 更新后,聚焦到新行的输入框
nextTick(() => {
const newIndex = packageItems.value.length - 1;
const inputEl = itemNameRefs.value[newIndex];
if (inputEl && inputEl.$el) {
// Element Plus 的 el-input 组件,实际 input 在 $el 内部
const realInput = inputEl.$el.querySelector('input');
if (realInput) {
realInput.focus();
}
}
addingItem = false;
});
};
// 删除套餐项目
const deletePackageItem = (index) => {
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
packageItems.value.splice(index, 1);
itemNameRefs.value.splice(index, 1); // 同步移除 ref
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) => {
const item = packageItems.value[index];
if (!item.name) {
ElMessage.warning('请输入项目名称');
if (itemNameRefs.value[index]) {
itemNameRefs.value[index].focus();
}
return;
}
if (editingRowId.value === index) {
// 验证数量、单价等...
if (!item.quantity || item.quantity <= 0) {
ElMessage.warning('请输入正确的数量');
return;
}
if (!item.unitPrice || item.unitPrice < 0) {
ElMessage.warning('请输入正确的单价');
return;
}
// 计算单项金额
updateItemAmount(item);
// 【重要】保存成功后,清除之前备份的原始数据
if (item._originalData) {
delete item._originalData;
}
editingRowId.value = null;
ElMessage.success('保存成功');
} else {
// 进入编辑模式:深拷贝当前行数据作为备份,以便取消时恢复
item._originalData = JSON.parse(JSON.stringify(item));
editingRowId.value = index;
}
};
// 取消编辑项目
const cancelEditItem = (index) => {
const row = packageItems.value[index];
// 判断依据:有 _originalData 备份说明是从编辑模式进入的无论是否有ID还原数据
// 没有备份说明是刚新增、第一次进入编辑、从未保存过,才删除
if (row._originalData) {
// 编辑已有数据 → 用备份数据还原,严禁清空字段
Object.assign(row, row._originalData);
delete row._originalData;
ElMessage.info('已取消编辑');
} else {
// 全新新增行(无备份)→ 直接删除,并清理 ref 引用
packageItems.value.splice(index, 1);
if (itemNameRefs.value[index]) {
delete itemNameRefs.value[index];
}
ElMessage.info('已取消新增');
}
// 退出编辑模式
editingRowId.value = null;
// 重新计算总金额
calculateAmounts();
};
// 计算单个项目的服务费(基于折扣后的金额)
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();
// 计算套餐总服务费
if (generateServiceFee.value) {
serviceFee.value = parseFloat(packageItems.value.reduce((sum, item) => sum + (item.serviceFee || 0), 0).toFixed(2));
} else {
serviceFee.value = 0;
}
// 计算套餐总金额(折扣后金额 + 服务费)
const totalAmount = packageItems.value.reduce((sum, item) => sum + (item.amount || 0), 0);
packageAmount.value = parseFloat((totalAmount + serviceFee.value).toFixed(2));
};
const itemNameRefs = ref([]);
// 检验类型相关方法
// 处理表格点击事件,用于自动删除空的编辑行
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: '全部',
subItemName: '',
remark: '',
status: true
};
inspectionItems.value.push(newItem);
editingRowId.value = newItem.id;
};
const editItem = async (item) => {
if (editingRowId.value === item.id) {
saveItem(item);
return;
}
// 初始化行内属性
if (!item.subItemOptions) item.subItemOptions = [];
// 并行预加载:费用套餐列表 + 当前行的子类列表
const tasks = [];
if (feePackages.value.length === 0) {
tasks.push(getFeePackages());
}
if (item.inspectionTypeId && item.subItemOptions.length === 0) {
item.loadingSubItems = true;
tasks.push(
fetchInspectionTypesRequest({ parentId: item.inspectionTypeId }).then(res => {
let subData = [];
if (res.code === 200) {
if (res.data && Array.isArray(res.data)) subData = res.data;
else if (res.data && res.data.rows) subData = res.data.rows;
else if (res.data && res.data.data && Array.isArray(res.data.data)) subData = res.data.data;
}
item.subItemOptions = subData;
}).finally(() => {
item.loadingSubItems = false;
})
);
}
// 等所有数据加载完再切换编辑态,确保 el-select 能回显正确文字
await Promise.all(tasks);
editingRowId.value = item.id;
};
const updateAmountFromPackage = (item) => {
if (item.feePackageId) {
const selectedPackage = feePackages.value.find(pkg => String(pkg.id) === String(item.feePackageId));
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.inspectionTypeId) {
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;
}
// 【新增】验证小类项目名称唯一性
// 逻辑:遍历列表,排除当前正在编辑的行(id不同),且名称(trim后)相同
const isDuplicateName = inspectionItems.value.some(i =>
i.id !== item.id && i.name.trim() === item.name.trim()
);
if (isDuplicateName) {
ElMessage.error('小类项目名称已存在');
return;
}
// 从费用套餐获取金额
updateAmountFromPackage(item);
try {
// 准备提交给后端的数据
const submitData = {
busNo: item.code.trim(),
name: item.name.trim(),
categoryCode: inspectionCategoryCode.value,
inspectionTypeId: item.inspectionTypeId || null,
feePackageId: item.feePackageId || null,
subItemId: item.subItemId || null,
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) => {
// 如果是新添加的行则直接删除临时ID为数字类型
if (typeof item.id === 'number') {
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.subItemName || '-'}"`,
`"${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: parseFloat(String(discount.value).replace('%', '').trim()) || 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: (tenantOptions.value.find(t => t.value === selectedTenantId.value)?.label) || 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 fetchTenantList = async () => {
if (loadingTenant.value) return;
loadingTenant.value = true;
try {
const response = await getTenantPage({ pageNum: 1, pageSize: 100 });
// 1. 检查业务状态码
if (response.code !== 200) {
throw new Error(response.msg || '获取机构列表失败');
}
// 2. 安全提取数据列表
let tenantData = [];
const data = response.data;
if (Array.isArray(data)) {
tenantData = data;
} else if (data && typeof data === 'object') {
tenantData = data.records || data.rows || data.list || [];
}
if (!Array.isArray(tenantData)) {
tenantData = [];
}
// 3. 过滤启用的机构 (status === "0")
// 根据您提供的JSON这里会成功筛选出 ID=1 的 "中联医院"
const activeTenants = tenantData.filter(item => item && item.status === "0");
// 4. 生成下拉选项
tenantOptions.value = activeTenants.map(item => ({
value: item.id,
label: item.tenantName || item.name || item.orgName || String(item.id)
}));
// 5. 默认选中逻辑
if (activeTenants.length > 0) {
// 优先选中 ID 为 1 的 "中联医院"
const zhonglianHospital = activeTenants.find(item => item.id === 1);
if (zhonglianHospital) {
selectedTenantId.value = 1;
} else {
selectedTenantId.value = activeTenants[0].id;
}
} else {
selectedTenantId.value = null;
}
} catch (error) {
// 打印详细错误,方便调试
ElMessage.error('获取机构列表失败:' + (error.message || '未知错误'));
tenantOptions.value = [];
} finally {
loadingTenant.value = false;
}
};
const currentEditingRow = ref(null);
/**
* el-autocomplete 行内搜索回调
* query: 用户输入的关键词cb: 回调函数,将结果数组传入即可
*/
const handleProjectInlineSearch = async (query, cb, row) => {
currentEditingRow.value = row;
try {
const params = { pageNo: 1, pageSize: 20, statusEnum: 2, searchKey: query || undefined };
const response = await getDiagnosisTreatmentList(params);
if (response.code === 200) {
let list = [];
if (response.data && response.data.records) {
list = response.data.records;
} else if (Array.isArray(response.data)) {
list = response.data;
}
// el-autocomplete 要求每项必须有 value 字段(显示在输入框中)
cb(list.map(item => ({ ...item, value: item.name })));
} else {
cb([]);
}
} catch {
cb([]);
}
};
/**
* el-autocomplete 选中某条记录后回填数据
*/
const handleProjectInlineSelect = (selectedItem, row) => {
row.name = selectedItem.name;
row.spec = selectedItem.spec || '';
row.unitPrice = parseFloat(selectedItem.retailPrice || 0);
row.unit = selectedItem.permittedUnitCode_dictText || selectedItem.unit || '次';
if (!row.quantity) row.quantity = 1;
row.amount = parseFloat((row.unitPrice * row.quantity).toFixed(2));
row.totalAmount = parseFloat((row.amount + (row.serviceFee || 0)).toFixed(2));
calculateAmounts();
};
// 保存套餐数据到数据库
const savePackageData = async (basicInfo, detailData) => {
// 显示保存进度
const loading = ElLoading.service({
lock: true,
text: '正在保存...',
background: 'rgba(0, 0, 0, 0.7)',
});
try {
// 判断是新增还是编辑模式
const isEditMode = packageMode.value === 'edit' && currentPackageId.value;
// 如果是编辑模式设置packageId到basicInfo中
if (isEditMode) {
basicInfo.packageId = currentPackageId.value;
}
// 1. 先保存或更新基本信息
let packageId = null;
let basicResponse;
if (isEditMode) {
// 编辑模式调用更新API
basicResponse = await updateInspectionPackage(basicInfo);
if (basicResponse.code !== 200) {
loading.close();
throw new Error(basicResponse.msg || '更新基本信息失败');
}
packageId = currentPackageId.value;
} else {
// 新增模式调用新增API
basicResponse = await addInspectionPackage(basicInfo);
// 检查响应码
if (basicResponse.code !== 200) {
loading.close();
throw new Error(basicResponse.msg || '保存基本信息失败');
}
// 检查响应数据结构 - 兼容多种可能的响应格式
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();
throw new Error('保存成功但未返回套餐ID。请检查后端接口是否正确返回了packageId或id字段');
}
}
// 2. 保存明细数据:编辑模式用批量接口(先删旧后插新),新增模式逐条新增
if (isEditMode) {
// 编辑模式:调用批量保存接口,后端会先删除旧明细再插入新明细
const batchPayload = {
basicInformationId: packageId,
details: detailData.map(item => ({ ...item, packageId: packageId }))
};
const batchResponse = await batchSaveInspectionPackageDetails(batchPayload);
if (batchResponse.code !== 200) {
loading.close();
throw new Error(batchResponse.msg || '保存明细数据失败');
}
} else {
// 新增模式:逐条保存并回填后端生成的明细 id
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 || '未知错误'}`);
}
// 回填后端生成的明细 id防止取消编辑时被误判为新增行
if (detailResponse.data && (detailResponse.data.detailId || detailResponse.data.id)) {
packageItems.value[i].id = detailResponse.data.detailId || detailResponse.data.id;
}
}
}
// 关闭加载提示
loading.close();
ElMessage.success('保存成功');
if (isEditMode) {
// 编辑模式:重新加载最新数据,保持在编辑页面
loadInspectionPackage(String(packageId));
} else {
// 新增模式:保存成功后重置表单
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 = [];
// 重置为新增模式(仅在外部未指定 mode 时,由调用方负责设置 packageMode
packageMode.value = 'add';
currentPackageId.value = null;
// 重新计算金额
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 || [];
// 【关键】暂停 packageItems 的 watch防止赋值时触发 calculateAmounts 覆盖后端返回的金额
isLoadingPackage.value = true;
// 填充基本信息
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 = parseFloat(basicData.packageAmount || 0);
serviceFee.value = parseFloat(basicData.serviceFee || 0);
selectedLisGroup.value = basicData.lisGroup || '';
bloodVolume.value = basicData.bloodVolume || '';
remarks.value = basicData.remarks || '';
// 填充明细数据(必须映射 id否则取消编辑时会被误判为新增行删除
packageItems.value = detailData.map(item => ({
id: item.detailId || item.id,
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 || ''
}));
// 恢复监听
isLoadingPackage.value = false;
loading.close();
ElMessage.success('套餐数据加载成功');
} catch (error) {
isLoadingPackage.value = false;
ElMessage.error(error.message || '加载套餐数据失败');
}
};
const handlePackageManagement = () => {
// 跳转到套餐管理页面
router.push({
path: '/maintainSystem/Inspection/PackageManagement'
});
};
const handleBackToManagement = () => {
// 返回套餐管理页面
router.push({
path: '/maintainSystem/Inspection/PackageManagement'
});
};
const refreshPage = () => {
getInspectionTypeList();
};
// 监听检验分类代码,当字典数据加载完成后加载检验项目数据
watch(activity_category_code, (newVal) => {
if (newVal && newVal.length > 0 && inspectionItems.value.length === 0) {
loadObservationItems();
}
}, { immediate: true });
/**
* 统一监听 query 变化,处理 tab 切换和套餐数据加载。
* 使用 watch(route) 深度监听,确保在 keep-alive 场景下同路由 query 变化也能响应。
*/
const applyRouteForPackage = async (query) => {
const tab = query?.tab;
const packageId = query?.packageId;
const mode = query?.mode || 'add';
// 先切换 tab
if (tab === '0' || tab === '1' || tab === '2') {
activeNav.value = parseInt(tab);
};
// 只有 tab=2套餐设置才处理套餐数据
if (activeNav.value !== 2) return;
// 新增模式:重置表单,然后明确设置 mode
if (mode === 'add' || !packageId) {
doResetForm(); // 内部会设 packageMode='add',符合预期
return;
}
// 编辑/查看模式:先设置 mode 和 ID再加载数据
packageMode.value = mode;
currentPackageId.value = String(packageId);
await nextTick();
loadInspectionPackage(String(packageId));
};
// 监听整个 route.query确保任何 query 变化tab/packageId/mode都能触发
watch(
() => route.query,
(newQuery) => {
applyRouteForPackage(newQuery);
},
{ immediate: true, deep: true }
);
// 兜底:如果该页面被 keep-alive 缓存,从别的页面返回时不会触发 onMounted
onActivated(() => {
applyRouteForPackage(route.query);
});
// 兜底:同一路由复用/仅 query 变化时,确保能触发加载
onBeforeRouteUpdate((to) => {
applyRouteForPackage(to.query);
});
// 监听生成服务费选项变更
watch(generateServiceFee, (newVal) => {
if (isLoadingPackage.value) return;
calculateAmounts();
});
// 监听套餐项目变化
watch(packageItems, (newVal) => {
// 加载期间跳过计算,防止覆盖后端返回的金额
if (isLoadingPackage.value) return;
calculateAmounts();
}, { deep: true });
// 样本类型数据
// 【新增】定义通用单位列表
const unitOptions = ref([
{ value: '片', label: '片' },
{ value: '粒', label: '粒' },
{ value: 'ml', label: 'ml' },
{ value: 'g', label: 'g' },
{ value: 'mg', label: 'mg' },
{ value: '支', label: '支' },
{ value: '盒', label: '盒' },
{ value: '次', label: '次' },
{ value: '天', label: '天' }
]);
onMounted(async () => {
// 1. 启动其他基础数据的加载(这些不需要等待,并行执行即可)
loadParentTypes();
getInspectionTypeList();
getLisGroupList();
// 2. 【关键】等待机构列表加载完成
// 等函数执行完成
await fetchTenantList();
// 3. 等待 Vue 完成下一轮的 DOM 更新
await nextTick();
// 4. 【核心逻辑】强制默认选中"中联医院" (ID = 1)
// 检查下拉列表中是否存在 value 为 1 的选项
const hasZhonglian = tenantOptions.value.some(item => item.value === 1);
if (hasZhonglian) {
// 直接赋值给模板中 v-model 绑定的变量
selectedTenantId.value = 1;
// 💡 防御性编程:防止因渲染延迟导致界面未刷新
// 如果赋值后界面还没变100毫秒后再强制赋一次值
setTimeout(() => {
if (selectedTenantId.value !== 1) {
selectedTenantId.value = 1;
}
}, 100);
} else {
// (可选) 如果没有中联医院,且列表不为空,默认选中第一个
if (tenantOptions.value.length > 0 && !selectedTenantId.value) {
selectedTenantId.value = tenantOptions.value[0].value;
}
}
});
onUnmounted(() => {});
</script>
<style>
/* 编辑模式行样式 */
.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;
}
/* 行内搜索下拉项样式(挂载到 body 的 popper */
.project-inline-dropdown .el-autocomplete-suggestion__list li {
padding: 0;
}
.project-inline-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
}
.project-inline-item .item-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-inline-item .item-code {
color: #409EFF;
font-size: 12px;
flex-shrink: 0;
}
.project-inline-item .item-price {
color: #f56c6c;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.project-inline-item .item-unit {
color: #909399;
font-size: 12px;
flex-shrink: 0;
}</style>