2570 lines
84 KiB
Vue
2570 lines
84 KiB
Vue
<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 });
|
||
|
||
/**
|
||
* 关键修复:
|
||
* 从“套餐管理”跳转到这里时,通常只是改变 query(packageId/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> |