Files
his/openhis-ui-vue3/src/views/maintainSystem/checkprojectSettings/components/PackageSettings.vue
Ranyunqiao b5527cc07f 294 检查项目设置-》套餐设置:基本信息服务费字段的值系统没有自动合计套餐明细服务费字段所有行的值
295 检查项目设置-》套餐设置:套餐明细数量字段后面需要增加单位字段
2026-03-30 09:03:49 +08:00

1420 lines
48 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>
<div class="package-settings">
<!-- 顶部操作按钮区 -->
<div class="header-actions">
<el-button type="primary" @click="handlePackageManagement">套餐管理</el-button>
<el-button type="primary" v-if="!isReadOnly" @click="handleRefresh" :loading="loading">刷新</el-button>
<el-button v-if="!isReadOnly" type="success" @click="handleSave">保存</el-button>
</div>
<!-- 基本信息表单区 -->
<div class="basic-info-section">
<h3 class="section-title">基本信息</h3>
<el-form
ref="basicFormRef"
:model="formData"
:rules="formRules"
label-width="120px"
class="basic-form"
>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="套餐类别" prop="packageType">
<el-select v-model="formData.packageType" placeholder="请选择套餐类别" style="width: 100%" :disabled="isReadOnly">
<el-option label="检查套餐" value="检查套餐" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="套餐级别" prop="packageLevel">
<el-select
v-model="formData.packageLevel"
placeholder="请选择套餐级别"
style="width: 100%"
@change="handlePackageLevelChange"
:disabled="isReadOnly"
>
<el-option
v-for="item in packageLevelOptions"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="formData.packageLevel === '2'" :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="科室选择" prop="department">
<el-select v-model="formData.department" placeholder="请选择科室" style="width: 100%" :disabled="isReadOnly">
<el-option
v-for="dept in departments"
:key="dept.deptCode || dept.dictValue"
:label="dept.dictLabel"
:value="dept.deptCode || dept.busNoPrefix || dept.rawOrg?.busNo || dept.dictValue"
/>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="formData.packageLevel === '3'" :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="用户选择" prop="user">
<el-select v-model="formData.user" placeholder="请选择用户" style="width: 100%" :disabled="isReadOnly">
<el-option label="当前用户" value="当前用户" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="套餐名称" prop="packageName">
<el-input v-model="formData.packageName" placeholder="请输入套餐名称" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="卫生机构">
<el-select v-model="formData.organization" placeholder="请选择卫生机构" style="width: 100%" :disabled="isReadOnly">
<el-option
v-for="org in organizationOptions"
:key="org.value"
:label="org.label"
:value="org.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="套餐金额">
<el-input v-model="packagePriceDisplay" :disabled="true" placeholder="自动计算">
<template #append></template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="折扣">
<el-input
v-model="formData.discount"
placeholder="请输入折扣"
@input="handleDiscountChange"
:disabled="isReadOnly"
>
<template #append>%</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="制单人">
<el-input v-model="formData.creator" :disabled="true" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="是否停用">
<el-radio-group v-model="formData.isDisabled" :disabled="isReadOnly">
<el-radio :value="0">启用</el-radio>
<el-radio :value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="显示套餐名">
<el-radio-group v-model="formData.showPackageName" :disabled="isReadOnly">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="生成服务费">
<el-radio-group v-model="formData.generateServiceFee" :disabled="isReadOnly">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="套餐价格">
<el-radio-group v-model="formData.packagePriceEnabled" :disabled="isReadOnly">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="服务费">
<el-input-number
v-model="formData.serviceFee"
:precision="2"
:min="0"
placeholder="自动合计"
style="width: 100%"
disabled
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12">
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" :disabled="isReadOnly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<!-- 套餐明细表格区 -->
<div class="package-detail-section">
<h3 class="section-title">套餐明细</h3>
<el-table
:data="detailData"
border
style="width: 100%"
:max-height="450"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="code" label="编号" width="120" align="center">
<template #default="{ row }">
<el-input v-if="row.editing" v-model="row.code" placeholder="请输入编号" />
<span v-else>{{ row.code }}</span>
</template>
</el-table-column>
<el-table-column prop="itemName" label="项目名称/规格" min-width="200">
<template #default="{ row }">
<div v-if="row.editing">
<el-select
v-model="row.itemId"
placeholder="输入名称/首字母/编号搜索"
filterable
remote
:remote-method="(query) => handleProjectSearch(query, row)"
style="width: 100%"
@change="handleItemSelect(row)"
@focus="initializeSearchList(row)"
:loading="diagnosisTreatmentList.length === 0"
clearable
>
<el-option
v-for="item in (row.filteredList || diagnosisTreatmentList)"
:key="item.id"
:label="(item.name || item.itemName || '未命名') + (item.busNo ? ' [' + item.busNo + ']' : '')"
:value="item.id"
>
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<span>{{ item.name || item.itemName }}</span>
<span style="display: flex; align-items: center; gap: 8px;">
<span style="color: #8492a6; font-size: 13px;">{{ item.busNo || item.code }}</span>
<span style="color: #E6A23C; font-size: 13px; font-weight: 500;">¥{{ item.retailPrice || item.price || item.unitPrice || 0 }}</span>
</span>
</div>
</el-option>
<template #empty>
<div style="padding: 10px; text-align: center; color: #999;">
<div v-if="diagnosisTreatmentList.length > 0">
无匹配项目<br/>
<small>请尝试其他关键词</small>
</div>
<div v-else>
暂无项目数据<br/>
<small>请先在【系统管理-目录管理-诊疗项目】中添加项目</small>
</div>
</div>
</template>
</el-select>
</div>
<span v-else>{{ row.itemName }}</span>
</template>
</el-table-column>
<el-table-column prop="dose" label="剂量" width="100" align="center">
<template #default="{ row }">
<el-input
v-if="row.editing"
v-model="row.dose"
placeholder="-"
@input="validateTableNumberInput($event, row, 'dose')"
/>
<span v-else>{{ row.dose || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="method" label="途径" width="80" align="center">
<template #default="{ row }">
<el-input v-if="row.editing" v-model="row.method" placeholder="-" />
<span v-else>{{ row.method || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="frequency" label="频次" width="80" align="center">
<template #default="{ row }">
<el-input v-if="row.editing" v-model="row.frequency" placeholder="-" />
<span v-else>{{ row.frequency || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="days" label="天数" width="100" align="center">
<template #default="{ row }">
<el-input
v-if="row.editing"
v-model="row.days"
placeholder="-"
@input="validateTableNumberInput($event, row, 'days')"
/>
<span v-else>{{ row.days || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="100" align="center">
<template #default="{ row }">
<el-input-number
v-if="row.editing"
v-model="row.quantity"
:min="0"
:precision="0"
placeholder="请输入数量"
style="width: 100%"
:controls="false"
@change="calculateAmount(row)"
/>
<span v-else>{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="80" align="center">
<template #default="{ row }">
<span v-if="row.editing">{{ row.unit || '-' }}</span>
<span v-else>{{ row.unit || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" width="150" align="center">
<template #default="{ row }">
<el-input
v-if="row.editing"
v-model="row.unitPrice"
placeholder="自动获取"
style="width: 100%"
size="small"
disabled
/>
<span v-else>{{ row.unitPrice?.toFixed(6) }}</span>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="100" align="center">
<template #default="{ row }">
{{ row.amount?.toFixed(2) || '0.00' }}
</template>
</el-table-column>
<el-table-column prop="serviceCharge" label="服务费" width="100" align="center">
<template #default="{ row }">
<el-input-number
v-if="row.editing"
v-model="row.serviceCharge"
:min="0"
:precision="2"
placeholder="服务费"
style="width: 100%"
:controls="false"
@input="(val) => handleServiceChargeInput(val, row)"
/>
<span v-else>{{ row.serviceCharge?.toFixed(2) || '0.00' }}</span>
</template>
</el-table-column>
<el-table-column prop="total" label="总金额" width="100" align="center">
<template #default="{ row }">
{{ row.total?.toFixed(2) || '0.00' }}
</template>
</el-table-column>
<el-table-column prop="origin" label="产地" width="120">
<template #default="{ row }">
<el-input v-if="row.editing" v-model="row.origin" placeholder="-" />
<span v-else>{{ row.origin || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" v-if="!isReadOnly" width="150" align="center" fixed="right">
<template #default="{ row, $index }">
<div class="actions">
<el-button
v-if="!row.editing"
class="btn btn-edit"
size="small"
circle
@click="handleEditRow(row)"
title="编辑"
>
✏️
</el-button>
<el-button
v-if="row.editing"
class="btn btn-confirm"
size="small"
circle
@click="handleConfirmRow(row)"
title="保存"
>
</el-button>
<el-button
class="btn btn-add"
size="small"
circle
@click="handleAddRow"
title="添加"
>
+
</el-button>
<el-button
class="btn btn-delete"
size="small"
circle
@click="handleDeleteRow($index)"
title="删除"
>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import {computed, onMounted, reactive, ref, watch} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {getDicts} from '@/api/system/dict/data'
import {listDept} from '@/api/system/dept'
import {addCheckPackage, updateCheckPackage} from '@/api/system/checkType'
import {getDiagnosisTreatmentSimpleList} from '@/views/catalog/diagnosistreatment/components/diagnosistreatment'
import {getTenantPage} from '@/api/system/tenant'
import useUserStore from '@/store/modules/user'
import request from '@/utils/request'
import cache from '@/plugins/cache'
// 接收props
const props = defineProps({
mode: {
type: String,
default: 'add', // add-新增, edit-编辑, view-查看
validator: (value) => ['add', 'edit', 'view'].includes(value)
},
packageData: {
type: Object,
default: null
}
})
// 定义emit
const emit = defineEmits(['switch-to-management', 'save-success'])
const userStore = useUserStore()
// 页面标题
const pageTitle = computed(() => {
switch (props.mode) {
case 'add':
return '套餐设置'
case 'edit':
return '编辑套餐'
case 'view':
return '查看套餐'
default:
return '套餐设置'
}
})
// 是否只读模式
const isReadOnly = computed(() => props.mode === 'view')
// 表单引用
const basicFormRef = ref(null)
// 表单数据
const formData = reactive({
id: null,
packageType: '检查套餐',
packageLevel: '',
department: '',
user: '',
packageName: '',
organization: '',
packagePrice: 0,
discount: '',
creator: userStore.name || '当前用户',
isDisabled: 0,
showPackageName: 1,
generateServiceFee: 0,
packagePriceEnabled: 1,
serviceFee: 0,
remark: '',
createDate: new Date().toISOString().split('T')[0] // 自动生成当前系统日期
})
// 套餐金额显示值(格式化后的字符串)
const packagePriceDisplay = computed({
get: () => formData.packagePrice?.toFixed(2) || '0.00',
set: (value) => {
// 只读,不允许直接修改
}
})
// 表单验证规则
const formRules = {
packageType: [
{ required: true, message: '请选择套餐类别', trigger: 'change' }
],
packageLevel: [
{ required: true, message: '请选择套餐级别', trigger: 'change' }
],
packageName: [
{ required: true, message: '请输入套餐名称', trigger: 'blur', type: 'string' }
]
}
// 套餐级别选项
const packageLevelOptions = ref([])
// 科室选项
const departments = ref([])
// 卫生机构选项
const organizationOptions = ref([])
// 诊疗项目列表
const diagnosisTreatmentList = ref([])
// 过滤后的诊疗项目列表(用于搜索)
const filteredDiagnosisList = ref([])
// 加载状态
const loading = ref(false)
// 明细数据
const detailData = ref([])
// 初始化数据
// 监听packageData变化加载数据
watch(() => props.packageData, (newData) => {
if (newData) {
loadPackageData(newData)
}
}, { immediate: true })
// 加载套餐数据到表单
function loadPackageData(data) {
if (!data) return
console.log('=== 加载套餐数据 ===')
console.log('套餐数据:', data)
// 填充基本信息
Object.keys(formData).forEach(key => {
if (data[key] !== undefined) {
// 特殊处理科室字段:检查编码格式并匹配
if (key === 'department' && data[key]) {
const deptValue = String(data[key]).trim()
const isOldFormat = /^[A-Z]\d{2}$/.test(deptValue)
if (isOldFormat && departments.value.length > 0) {
// 旧编码格式:在下拉框中添加临时选项
const tempDept = {
deptCode: deptValue,
dictLabel: `旧编码:${deptValue} (请重新选择)`,
dictValue: deptValue,
isOldFormat: true
}
const exists = departments.value.find(d => d.deptCode === deptValue)
if (!exists) {
departments.value.unshift(tempDept)
}
formData[key] = deptValue
} else {
formData[key] = data[key]
}
} else {
formData[key] = data[key]
}
}
})
// 填充明细数据
if (data.items && Array.isArray(data.items)) {
detailData.value = data.items.map(item => ({
code: item.itemCode || '',
itemId: item.checkItemId,
itemName: item.itemName || '',
dose: item.dose || '',
method: item.method || '',
frequency: item.frequency || '',
days: item.days || '',
quantity: item.quantity || 1,
unitPrice: item.unitPrice || 0,
unit: item.unit || '',
amount: item.amount || 0,
serviceCharge: item.serviceCharge || 0,
total: item.total || 0,
origin: item.origin || '',
editing: false,
filteredList: diagnosisTreatmentList.value
}))
console.log('明细数据加载成功,条数:', detailData.value.length)
} else {
console.log('没有明细数据')
}
console.log('formData 加载后:', formData)
console.log('detailData 加载后:', detailData.value)
// 加载数据后自动计算总服务费
calculateTotalServiceFee()
}
onMounted(async () => {
console.log('=== PackageSettings 组件开始初始化 ===')
console.log('当前模式:', props.mode)
try {
// 新增模式初始化 formData
if (props.mode === 'add') {
console.log('新增模式:初始化 formData')
// 确保表单字段正确初始化
formData.id = null
formData.packageType = '检查套餐'
formData.packageLevel = ''
formData.department = ''
formData.user = ''
formData.packageName = ''
formData.organization = ''
formData.packagePrice = 0
formData.discount = ''
formData.isDisabled = 0
formData.showPackageName = 1
formData.generateServiceFee = 0
formData.packagePriceEnabled = 1
formData.serviceFee = 0
formData.remark = ''
formData.createDate = new Date().toISOString().split('T')[0]
// 清空明细数据并初始化一行
detailData.value = []
handleAddRow()
}
// 获取套餐级别字典
try {
const levelResponse = await getDicts('examination_item_package_level')
if (levelResponse && levelResponse.data) {
packageLevelOptions.value = levelResponse.data
console.log('✓ 套餐级别数据加载成功:', packageLevelOptions.value.length)
}
} catch (error) {
console.error('✗ 获取套餐级别字典失败:', error)
}
// 获取科室列表 - 使用Organization完整API包含编码和名称
try {
// 使用Organization完整API获取科室列表包含busNo编码
const orgResponse = await request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: {
pageNo: 1,
pageSize: 1000 // 获取足够多的数据
}
})
console.log('科室Organization完整API响应:', orgResponse)
let orgList = []
if (orgResponse) {
// 处理分页响应格式:{ code: 200, data: { records: [...], total: ... } }
if (orgResponse.data) {
if (orgResponse.data.records && Array.isArray(orgResponse.data.records)) {
// 分页格式提取records
orgList = orgResponse.data.records
} else if (Array.isArray(orgResponse.data)) {
orgList = orgResponse.data
}
} else if (Array.isArray(orgResponse)) {
orgList = orgResponse
}
}
// 如果是树结构需要展开所有节点包括children并过滤出科室类型typeEnum=2
if (orgList && orgList.length > 0) {
const flattenList = []
function flatten(nodes) {
nodes.forEach(node => {
// 只包含科室类型typeEnum=2且有busNo的节点
if (node.typeEnum === 2 && node.busNo && node.name) {
flattenList.push(node)
}
if (node.children && node.children.length > 0) {
flatten(node.children)
}
})
}
flatten(orgList)
orgList = flattenList
console.log('展开后的科室列表包含busNo:', orgList.length, orgList.slice(0, 3).map(o => ({ busNo: o.busNo, name: o.name })))
}
if (orgList && orgList.length > 0) {
// 使用Organization数据包含编码和名称
departments.value = orgList.map(org => {
const busNo = (org.busNo || org.code || '').trim()
const name = (org.name || org.deptName || '').trim()
// 如果busNo是层级编码如 "A01.001"),同时存储前缀("A01")用于匹配
const busNoPrefix = busNo ? busNo.split('.')[0] : ''
return {
dictValue: name,
dictLabel: name,
deptId: org.id || org.deptId,
deptCode: busNo || name, // 优先使用busNo如果没有则使用名称
busNoPrefix: busNoPrefix, // 存储前缀用于匹配
rawOrg: org // 保存原始数据用于调试
}
})
console.log('✓ 科室数据加载成功Organization:', departments.value.length)
console.log('科室映射关系前5个:', departments.value.slice(0, 5).map(d => ({
deptCode: `"${d.deptCode}"`,
busNoPrefix: `"${d.busNoPrefix}"`,
name: d.dictLabel,
rawBusNo: d.rawOrg?.busNo
})))
} else {
console.warn('Organization API没有返回科室数据尝试使用系统部门API')
// 如果Organization API没有数据使用系统部门API
const deptResponse = await listDept()
console.log('科室API响应:', deptResponse)
let deptList = []
if (deptResponse) {
if (Array.isArray(deptResponse)) {
deptList = deptResponse
} else if (deptResponse.data) {
if (Array.isArray(deptResponse.data)) {
deptList = deptResponse.data
} else if (deptResponse.data.data && Array.isArray(deptResponse.data.data)) {
deptList = deptResponse.data.data
}
} else if (deptResponse.code === 200 && deptResponse.data) {
deptList = Array.isArray(deptResponse.data) ? deptResponse.data : []
}
}
if (deptList && deptList.length > 0) {
departments.value = deptList.map(dept => ({
dictValue: dept.deptName || dept.name,
dictLabel: dept.deptName || dept.name,
deptId: dept.deptId || dept.id,
deptCode: dept.deptName || dept.name // 如果没有编码,使用名称
}))
console.log('✓ 科室数据加载成功Dept:', departments.value.length)
} else {
console.warn('科室列表为空,尝试使用字典方式')
// 如果获取失败,尝试使用字典方式
try {
const dictResponse = await getDicts('dept')
if (dictResponse && dictResponse.data) {
departments.value = dictResponse.data
console.log('✓ 使用字典方式加载科室数据成功:', departments.value.length)
}
} catch (dictError) {
console.error('✗ 获取科室字典也失败:', dictError)
}
}
}
} catch (error) {
console.error('✗ 获取科室列表失败:', error)
// Fallback to dicts if API fails
try {
const dictResponse = await getDicts('dept')
if (dictResponse && dictResponse.data) {
departments.value = dictResponse.data
}
} catch (dictError) {
console.error('✗ 获取科室字典也失败:', dictError)
}
}
// 加载诊疗项目列表默认加载500条
await loadDiagnosisTreatmentList(false)
// 加载卫生机构列表(只获取启用的租户)
await loadOrganizationList()
} catch (error) {
console.error('✗ 初始化数据失败:', error)
}
})
// 加载卫生机构列表(从租户列表获取,只获取启用的)
async function loadOrganizationList() {
try {
const response = await getTenantPage({
pageNo: 1,
pageSize: 1000,
status: '0'
})
if (response && response.code === 200) {
const records = response.data?.records || response.data || []
organizationOptions.value = records.map(item => ({
value: item.tenantName || item.name,
label: item.tenantName || item.name,
tenantId: item.tenantId
}))
console.log('✓ 卫生机构数据加载成功:', organizationOptions.value.length)
// 优先使用当前登录账号的机构名称作为默认值
const currentOrgName = userStore.tenantName
if (currentOrgName && organizationOptions.value.find(o => o.value === currentOrgName)) {
formData.organization = currentOrgName
} else if (!formData.organization && organizationOptions.value.length > 0) {
// 其次设置为列表第一个
formData.organization = organizationOptions.value[0].value
}
}
} catch (error) {
console.error('✗ 获取卫生机构列表失败:', error)
}
}
// 诊疗项目缓存key和过期时间
const DIAGNOSIS_TREATMENT_CACHE_KEY = 'check_package_diagnosis_treatment_cache'
const CACHE_EXPIRE_TIME = 30 * 60 * 1000 // 30分钟
// 诊疗项目ID到项目信息的映射缓存用于选择后获取详情
const diagnosisTreatmentCache = ref({})
// 加载诊疗项目详情到缓存
async function loadDiagnosisTreatmentItem(itemId, itemData) {
if (!itemData) return
diagnosisTreatmentCache.value[itemId] = itemData
}
// 加载诊疗项目列表默认加载500条支持缓存
async function loadDiagnosisTreatmentList(forceRefresh = false) {
// 如果不是强制刷新且已有数据且未过期,直接返回
if (!forceRefresh && diagnosisTreatmentList.value.length > 0) {
// 由于缓存过期时间改为0始终视为过期需要重新加载
// 这里直接跳过,不返回,让它重新加载
}
// 从session缓存读取
let useCache = false
try {
const cachedData = cache.session.getJSON(DIAGNOSIS_TREATMENT_CACHE_KEY)
if (cachedData && cachedData.timestamp) {
const now = Date.now()
const cacheAge = now - cachedData.timestamp
if (cacheAge < CACHE_EXPIRE_TIME) {
diagnosisTreatmentList.value = cachedData.data || []
return
}
}
} catch (error) {
console.error('读取缓存失败:', error)
}
// 从API加载数据
loading.value = true
try {
const response = await getDiagnosisTreatmentSimpleList(2)
let allItems = []
if (response && response.code === 200 && response.data) {
allItems = Array.isArray(response.data) ? response.data : response.data.records || []
}
if (allItems.length > 0) {
diagnosisTreatmentList.value = allItems
// 保存到缓存
cache.session.setJSON(DIAGNOSIS_TREATMENT_CACHE_KEY, {
data: allItems,
timestamp: Date.now()
})
// 只在强制刷新时显示成功消息
if (forceRefresh) {
ElMessage.success(`成功加载${allItems.length}个诊疗项目`)
}
} else if (forceRefresh) {
ElMessage.warning('未获取到诊疗项目数据')
}
} catch (error) {
console.error('获取诊疗项目列表失败:', error)
if (forceRefresh) {
ElMessage.error('获取诊疗项目列表失败')
}
} finally {
loading.value = false
}
}
// 套餐级别变更处理
function handlePackageLevelChange(value) {
// 重置关联字段
if (value !== '2') {
formData.department = ''
}
if (value !== '3') {
formData.user = ''
}
// 动态更新验证规则
if (value === '2') {
// 科室套餐,科室必填
formRules.department = [{ required: true, message: '请选择科室', trigger: 'change' }]
formRules.user = []
} else if (value === '3') {
// 个人套餐,用户必填
formRules.user = [{ required: true, message: '请选择用户', trigger: 'change' }]
formRules.department = []
// 自动获取当前登录账号名称赋值给用户选择字段
if (!formData.user && userStore.name) {
formData.user = userStore.name
}
} else {
// 全院套餐,都不必填
formRules.department = []
formRules.user = []
}
}
// 添加行
function handleAddRow() {
detailData.value.push({
code: '',
itemId: null,
itemName: '',
dose: '',
method: '',
frequency: '',
days: '',
quantity: 1,
unit: '',
unitPrice: 0,
amount: 0,
serviceCharge: 0,
total: 0,
origin: '',
editing: true,
filteredList: diagnosisTreatmentList.value // 初始化过滤列表
})
}
// 编辑行
function handleEditRow(row) {
row.editing = true
}
// 确认编辑
function handleConfirmRow(row) {
if (!row.itemName) {
ElMessage.warning('请选择项目')
return
}
if (!row.quantity || row.quantity <= 0) {
ElMessage.warning('请输入有效数量')
return
}
row.editing = false
}
// 删除行
function handleDeleteRow(index) {
ElMessageBox.confirm('确定要删除这一行吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
detailData.value.splice(index, 1)
calculatePackagePrice()
calculateTotalServiceFee()
ElMessage.success('删除成功')
}).catch(() => {})
}
// 获取拼音首字母
function getPinYinFirstLetter(str) {
if (!str) return ''
// 简单的拼音首字母映射表(常用汉字)
const pinyinMap = {
'啊': 'A', '阿': 'A', '癌': 'A', '按': 'A', '暗': 'A',
'八': 'B', '白': 'B', '百': 'B', '班': 'B', '帮': 'B', '保': 'B', '报': 'B', '杯': 'B', '本': 'B', '鼻': 'B', '比': 'B', '变': 'B', '标': 'B', '表': 'B', '病': 'B', '部': 'B', '不': 'B',
'彩': 'C', '超': 'C', '查': 'C', '常': 'C', '肠': 'C', '成': 'C', '穿': 'C', '床': 'C', '次': 'C',
'大': 'D', '带': 'D', '单': 'D', '胆': 'D', '蛋': 'D', '导': 'D', '道': 'D', '的': 'D', '低': 'D', '地': 'D', '电': 'D', '点': 'D', '定': 'D', '动': 'D', '度': 'D', '多': 'D',
'二': 'E', '耳': 'E',
'发': 'F', '法': 'F', '反': 'F', '防': 'F', '房': 'F', '肺': 'F', '分': 'F', '风': 'F', '腹': 'F', '妇': 'F', '复': 'F',
'肝': 'G', '感': 'G', '高': 'G', '骨': 'G', '功': 'G', '宫': 'G', '管': 'G', '光': 'G', '广': 'G', '过': 'G',
'核': 'H', '黑': 'H', '红': 'H', '后': 'H', '化': 'H', '换': 'H', '患': 'H', '黄': 'H', '回': 'H', '会': 'H',
'基': 'J', '及': 'J', '急': 'J', '疾': 'J', '甲': 'J', '检': 'J', '简': 'J', '见': 'J', '降': 'J', '交': 'J', '结': 'J', '介': 'J', '金': 'J', '进': 'J', '经': 'J', '精': 'J', '颈': 'J', '静': 'J',
'开': 'K', '抗': 'K', '康': 'K', '科': 'K', '可': 'K', '口': 'K',
'来': 'L', '类': 'L', '理': 'L', '力': 'L', '例': 'L', '粒': 'L', '连': 'L', '量': 'L', '疗': 'L', '淋': 'L', '临': 'L', '流': 'L', '瘤': 'L', '路': 'L', '卵': 'L',
'慢': 'M', '门': 'M', '免': 'M', '面': 'M', '敏': 'M', '明': 'M', '模': 'M', '目': 'M',
'内': 'N', '脑': 'N', '尿': 'N', '凝': 'N', '女': 'N',
'排': 'P', '培': 'P', '皮': 'P', '片': 'P', '平': 'P', '普': 'P',
'期': 'Q', '其': 'Q', '前': 'Q', '强': 'Q', '切': 'Q', '清': 'Q', '球': 'Q', '全': 'Q',
'染': 'R', '热': 'R', '人': 'R', '容': 'R', '溶': 'R', '肉': 'R', '入': 'R', '乳': 'R',
'三': 'S', '色': 'S', '扫': 'S', '伤': 'S', '上': 'S', '少': 'S', '肾': 'S', '生': 'S', '声': 'S', '时': 'S', '实': 'S', '室': 'S', '试': 'S', '视': 'S', '手': 'S', '术': 'S', '双': 'S', '水': 'S', '速': 'S', '酸': 'S',
'胎': 'T', '糖': 'T', '套': 'T', '特': 'T', '体': 'T', '铁': 'T', '听': 'T', '通': 'T', '痛': 'T', '头': 'T', '图': 'T',
'外': 'W', '胃': 'W', '卫': 'W', '温': 'W', '五': 'W', '无': 'W',
'西': 'X', '洗': 'X', '系': 'X', '细': 'X', '下': 'X', '显': 'X', '线': 'X', '腺': 'X', '相': 'X', '消': 'X', '小': 'X', '心': 'X', '新': 'X', '型': 'X', '性': 'X', '胸': 'X', '血': 'X',
'压': 'Y', '炎': 'Y', '眼': 'Y', '验': 'Y', '腰': 'Y', '药': 'Y', '液': 'Y', '一': 'Y', '医': 'Y', '疫': 'Y', '阴': 'Y', '引': 'Y', '应': 'Y', '影': 'Y', '用': 'Y', '右': 'Y', '于': 'Y', '预': 'Y', '原': 'Y', '月': 'Y', '孕': 'Y', '运': 'Y',
'早': 'Z', '增': 'Z', '诊': 'Z', '正': 'Z', '症': 'Z', '支': 'Z', '脂': 'Z', '指': 'Z', '质': 'Z', '治': 'Z', '中': 'Z', '肿': 'Z', '重': 'Z', '注': 'Z', '主': 'Z', '状': 'Z', '子': 'Z', '自': 'Z', '总': 'Z', '组': 'Z', '左': 'Z', '做': 'Z'
}
let result = ''
for (let i = 0; i < str.length; i++) {
const char = str[i]
if (pinyinMap[char]) {
result += pinyinMap[char]
} else if (/[a-zA-Z]/.test(char)) {
result += char.toUpperCase()
} else if (/[0-9]/.test(char)) {
result += char
}
}
return result
}
// 初始化搜索列表聚焦时显示默认加载的500条数据
function initializeSearchList(row) {
row.filteredList = diagnosisTreatmentList.value
}
// 项目搜索处理(支持名称/编号/拼音模糊搜索)
async function handleProjectSearch(query, row) {
// 如果输入为空,显示默认列表
if (!query || query.trim() === '') {
row.filteredList = diagnosisTreatmentList.value
return
}
const searchText = query.trim()
// 调用后端接口搜索
loading.value = true
try {
const response = await getDiagnosisTreatmentSimpleList(2, searchText)
let items = []
if (response && response.code === 200 && response.data) {
items = Array.isArray(response.data) ? response.data : []
}
// 如果搜索结果为空,显示默认列表
if (items.length === 0) {
row.filteredList = diagnosisTreatmentList.value
} else {
row.filteredList = items
}
console.log(`搜索"${searchText}"找到 ${items.length} 个结果`)
} catch (error) {
console.error('搜索失败:', error)
row.filteredList = diagnosisTreatmentList.value
} finally {
loading.value = false
}
}
// 项目选择处理(从搜索结果或缓存中获取)
function handleItemSelect(row) {
console.log('选择项目ID:', row.itemId)
// 优先从搜索结果中获取
let item = row.filteredList?.find(i => i.id === row.itemId)
// 其次从缓存中获取
if (!item) {
item = diagnosisTreatmentCache.value[row.itemId]
}
// 最后从诊断列表中获取
if (!item) {
item = diagnosisTreatmentList.value.find(i => i.id === row.itemId)
}
console.log('找到的项目:', item)
if (item) {
row.itemName = item.name || item.itemName || ''
row.code = item.busNo || item.code || item.itemCode || ''
row.unitPrice = parseFloat(item.retailPrice || item.unitPrice || item.price || 0)
// permittedUnitCode_dictText是字典翻译后的值permittedUnitCode是后端返回的原始值
row.unit = item.permittedUnitCode_dictText || item.permittedUnitCode || ''
// 缓存选中的项目
loadDiagnosisTreatmentItem(row.itemId, item)
calculateAmount(row)
} else {
ElMessage.warning('未找到该项目信息')
}
}
// 计算金额
function calculateAmount(row) {
row.amount = (row.quantity || 0) * (row.unitPrice || 0)
calculateTotal(row)
}
// 处理服务费输入
function handleServiceChargeInput(val, row) {
row.serviceCharge = val || 0
calculateTotal(row)
}
// 计算总金额
function calculateTotal(row) {
row.total = (row.amount || 0) + (row.serviceCharge || 0)
calculatePackagePrice()
calculateTotalServiceFee()
}
// 计算总服务费(合计所有明细行的服务费)
function calculateTotalServiceFee() {
const totalServiceFee = detailData.value.reduce((sum, item) => sum + (item.serviceCharge || 0), 0)
formData.serviceFee = totalServiceFee
}
// 计算套餐金额(应用折扣)
function calculatePackagePrice() {
// 计算所有明细项目的总金额
const total = detailData.value.reduce((sum, item) => sum + (item.total || 0), 0)
// 如果有折扣,应用折扣计算
let finalPrice = total
if (formData.discount && parseFloat(formData.discount) > 0 && parseFloat(formData.discount) <= 100) {
const discountRate = parseFloat(formData.discount) / 100 // 将百分比转换为小数(如 10% -> 0.1
const discountAmount = total * discountRate // 折扣金额
finalPrice = total - discountAmount // 折扣后金额
}
formData.packagePrice = finalPrice
}
// 折扣变更处理
function handleDiscountChange(value) {
// 验证并清理输入
const regex = /^\d*\.?\d*$/
if (!regex.test(value)) {
formData.discount = value.replace(/[^\d.]/g, '')
return
}
// 验证折扣范围
const discountValue = parseFloat(formData.discount)
if (discountValue && (discountValue < 0 || discountValue > 100)) {
ElMessage.warning('折扣必须在0-100之间')
// 重置为有效值
if (discountValue < 0) {
formData.discount = '0'
} else if (discountValue > 100) {
formData.discount = '100'
}
}
// 重新计算套餐金额
calculatePackagePrice()
}
// 套餐管理
// 套餐管理 - 切换到套餐管理界面
function handlePackageManagement() {
emit('switch-to-management')
}
// 刷新 - 清除缓存并重新加载诊疗项目数据
async function handleRefresh() {
// 清除缓存
cache.session.remove(DIAGNOSIS_TREATMENT_CACHE_KEY)
// 强制刷新加载
await loadDiagnosisTreatmentList(true)
}
// 数字输入验证 - 用于表单字段
function validateNumberInput(value, field) {
// 只允许数字和小数点
const regex = /^\d*\.?\d*$/
if (!regex.test(value)) {
formData[field] = value.replace(/[^\d.]/g, '')
}
}
// 数字输入验证 - 用于表格字段
function validateTableNumberInput(value, row, field) {
// 只允许数字和小数点
const regex = /^\d*\.?\d*$/
if (!regex.test(value)) {
row[field] = value.replace(/[^\d.]/g, '')
}
}
// 保存
async function handleSave() {
try {
console.log('开始保存,当前表单数据:', JSON.parse(JSON.stringify(formData)))
// 验证基本信息
try {
await basicFormRef.value.validate()
console.log('✓ 基本信息验证通过')
} catch (validationError) {
console.error('✗ 表单验证失败:', validationError)
ElMessage.error('请完善必填项:' + Object.keys(validationError).join(', '))
return
}
// 验证明细数据
if (detailData.value.length === 0) {
ElMessage.warning('请至少添加一条套餐明细')
return
}
console.log('明细数据条数:', detailData.value.length)
// 检查是否有未确认的编辑行
const hasEditingRow = detailData.value.some(row => row.editing)
if (hasEditingRow) {
ElMessage.warning('请先确认所有正在编辑的行')
return
}
console.log('✓ 所有行已确认')
// 检查明细数据完整性
const hasEmptyItem = detailData.value.some(row => !row.itemName || !row.quantity)
if (hasEmptyItem) {
console.error('✗ 存在空项目:', detailData.value.filter(row => !row.itemName || !row.quantity))
ElMessage.warning('请完善所有明细项目信息(项目名称和数量为必填项)')
return
}
console.log('✓ 明细数据完整性验证通过')
// 更新创建日期为当前系统时间
formData.createDate = new Date().toISOString().split('T')[0]
// 验证折扣范围
if (formData.discount && (parseFloat(formData.discount) < 0 || parseFloat(formData.discount) > 100)) {
ElMessage.warning('折扣必须在0-100之间')
return
}
// 构建保存数据
const saveData = {
id: formData.id,
packageName: String(formData.packageName || ''),
code: formData.code || '',
description: formData.description || '',
packageType: formData.packageType,
packageLevel: formData.packageLevel,
department: formData.department ? String(formData.department).trim() : '',
user: formData.user || '',
organization: formData.organization,
packagePrice: parseFloat(formData.packagePrice) || 0,
discount: formData.discount ? parseFloat(formData.discount) : null,
creator: formData.creator,
isDisabled: formData.isDisabled,
showPackageName: formData.showPackageName,
generateServiceFee: formData.generateServiceFee,
packagePriceEnabled: formData.packagePriceEnabled,
serviceFee: parseFloat(formData.serviceFee) || 0,
remark: formData.remark || '',
createDate: formData.createDate,
items: detailData.value.map((item, index) => ({
// 基本字段(与检查套餐 CheckPackageDetail 对应)
itemCode: item.code || '',
itemName: item.itemName || '',
checkItemId: item.itemId || null,
dose: item.dose || '',
method: item.method || '',
frequency: item.frequency || '',
days: item.days || '',
quantity: parseInt(item.quantity) || 1,
unit: item.unit || '',
unitPrice: parseFloat(item.unitPrice) || 0,
amount: parseFloat(item.amount) || 0,
serviceCharge: parseFloat(item.serviceCharge) || 0,
total: parseFloat(item.total) || 0,
origin: item.origin || '',
orderNum: index + 1,
// 兼容字段(部分日志/历史代码使用的命名dosage/route/serviceFee/totalAmount
// 后端当前不会用到这些别名字段,但保留便于排查和兼容
dosage: item.dose || '',
route: item.method || '',
serviceFee: parseFloat(item.serviceCharge) || 0,
totalAmount: parseFloat(item.total) || 0
}))
}
console.log('构建的保存数据:', JSON.parse(JSON.stringify(saveData)))
// 调用API保存 - 根据是否有ID判断新增还是更新
let response
try {
if (formData.id) {
console.log('执行更新操作...')
response = await updateCheckPackage(saveData)
} else {
console.log('执行新增操作...')
response = await addCheckPackage(saveData)
}
console.log('API响应:', response)
if (response && (response.code === 200 || response.code === 0)) {
ElMessage.success('套餐数据已保存')
emit('save-success')
// 如果有返回ID更新formData.id以便后续更新
if (response.data && !formData.id) {
formData.id = response.data
console.log('新增成功套餐ID:', formData.id)
}
} else {
console.error('保存失败,响应:', response)
ElMessage.error(response?.msg || response?.message || '保存失败,请检查表单数据')
}
} catch (apiError) {
console.error('API调用失败:', apiError)
throw apiError
}
} catch (error) {
console.error('保存过程出错:', error)
if (error !== 'cancel') {
const errorMsg = error.response?.data?.msg || error.message || '保存失败,请检查表单数据'
ElMessage.error('保存失败: ' + errorMsg)
}
}
}
</script>
<style scoped>
.package-settings {
padding: 24px;
background-color: #f5f7fa;
}
.header-actions {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.basic-info-section,
.package-detail-section {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
}
.section-title {
font-size: 16px;
font-weight: 500;
color: #000000;
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 1px solid #e8e8e8;
}
.basic-form {
margin-top: 16px;
}
.el-table {
margin-top: 16px;
}
/* 统一的操作按钮样式 */
.actions {
display: flex;
justify-content: center;
gap: 6px;
position: relative;
z-index: 10;
}
.btn {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
font-weight: bold;
position: relative;
z-index: 10;
color: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.btn:hover {
transform: translateY(-2px) scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}
.btn:active {
transform: translateY(0) scale(0.95);
}
.btn-confirm {
background: linear-gradient(135deg, #52C41A 0%, #73d13d 100%);
}
.btn-confirm:hover {
background: linear-gradient(135deg, #389E0D 0%, #52C41A 100%);
}
.btn-edit {
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
}
.btn-edit:hover {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
}
.btn-add {
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
}
.btn-add:hover {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
}
.btn-delete {
background: linear-gradient(135deg, #FF4D4F 0%, #ff7875 100%);
z-index: 20;
pointer-events: auto;
}
.btn-delete:hover {
background: linear-gradient(135deg, #CF1322 0%, #FF4D4F 100%);
}
/* 响应式设计 */
@media (max-width: 768px) {
.package-settings {
padding: 16px;
}
.header-actions {
flex-direction: column;
}
.header-actions .el-button {
width: 100%;
}
.basic-info-section,
.package-detail-section {
padding: 16px;
}
}
</style>