套餐设置功能前后端内容基本完成(细节未处理)

This commit is contained in:
2025-12-25 11:12:56 +08:00
parent 32d1673667
commit 55b3dfc077
12 changed files with 1380 additions and 25 deletions

View File

@@ -0,0 +1,79 @@
import request from '@/utils/request'
// 查询检验套餐列表
export function listInspectionPackage(query) {
return request({
url: '/system/inspection-package/list',
method: 'get',
params: query
})
}
// 查询检验套餐详细
export function getInspectionPackage(packageId) {
return request({
url: '/system/inspection-package/' + packageId,
method: 'get'
})
}
// 新增检验套餐基本信息
export function addInspectionPackage(data) {
return request({
url: '/system/inspection-package',
method: 'post',
data: data
})
}
// 修改检验套餐基本信息
export function updateInspectionPackage(data) {
return request({
url: '/system/inspection-package',
method: 'put',
data: data
})
}
// 删除检验套餐
export function delInspectionPackage(packageId) {
return request({
url: '/system/inspection-package/' + packageId,
method: 'delete'
})
}
// 保存检验套餐明细数据
export function saveInspectionPackageDetails(data) {
return request({
url: '/system/inspection-package/details',
method: 'post',
data: data
})
}
// 查询检验套餐明细列表
export function listInspectionPackageDetails(packageId) {
return request({
url: '/system/inspection-package/details/' + packageId,
method: 'get'
})
}
// 删除检验套餐明细
export function delInspectionPackageDetails(detailIds) {
return request({
url: '/system/inspection-package/details',
method: 'delete',
data: detailIds
})
}
// 批量保存检验套餐明细
export function batchSaveInspectionPackageDetails(data) {
return request({
url: '/system/inspection-package/details/batch',
method: 'post',
data: data
})
}

View File

@@ -389,6 +389,9 @@
<button class="btn btn-icon" @click="refreshPage">
<i></i> 刷新
</button>
<button class="btn btn-secondary" @click="resetForm">
<i>🔄</i> 重置
</button>
<button class="btn btn-success" @click="handlePackageManagement">套餐管理</button>
</div>
<button class="btn btn-lg" @click="handleSave">保存</button>
@@ -445,7 +448,7 @@
</div>
<div class="form-item">
<span class="form-label">卫生机构</span>
<input type="text" class="form-control" :value="userStore.orgName" readonly>
<input type="text" class="form-control" :value="userStore.orgName || '测试机构'" readonly>
</div>
<div class="form-item">
<span class="form-label">套餐金额</span>
@@ -616,18 +619,25 @@
</td>
<td>
<template v-if="editingRowId === index">
<input type="number" v-model.number="item.quantity" placeholder="数量" style="width: 100%;" @input="updateItemAmount(item)" :style="inputStyle">
<input type="number" v-model.number="item.quantity" placeholder="数量" style="width: 100%;" @input="updateItemAmount(item)" :style="inputStyle" min="0" step="1">
</template>
<template v-else>
{{ item.quantity || '-' }}
</template>
</td>
<td>{{ item.unit || '-' }}</td>
<td>{{ item.unitPrice.toFixed(2) }}</td>
<td>
<template v-if="editingRowId === index">
<input type="number" v-model.number="item.unitPrice" placeholder="单价" style="width: 100%;" @input="updateItemAmount(item)" :style="inputStyle" min="0" step="0.01">
</template>
<template v-else>
{{ item.unitPrice.toFixed(2) }}
</template>
</td>
<td>{{ item.amount.toFixed(2) }}</td>
<td>
<template v-if="editingRowId === index">
<input type="number" v-model.number="item.serviceFee" placeholder="服务费" style="width: 100%;" step="0.01" @input="updateItemTotalAmount(item)" :style="inputStyle">
<input type="number" v-model.number="item.serviceFee" placeholder="服务费" style="width: 100%;" step="0.01" @input="updateItemTotalAmount(item)" :style="inputStyle" min="0">
</template>
<template v-else>
{{ item.serviceFee.toFixed(2) }}
@@ -644,20 +654,32 @@
</td>
<td class="action-cell">
<div class="table-actions" style="display: flex; gap: 5px;">
<div class="action-btn edit-btn" title="编辑" @click="handleEditItem(index)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</div>
<div class="action-btn delete-btn" title="删除" @click="deletePackageItem(index)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</div>
<template v-if="editingRowId === index">
<!-- 编辑模式下的保存和取消按钮 -->
<div class="action-btn confirm-btn" title="保存" @click="handleEditItem(index)">
<span style="font-size: 12px;"></span>
</div>
<div class="action-btn cancel-btn" title="取消" @click="cancelEditItem(index)">
<span style="font-size: 12px;"></span>
</div>
</template>
<template v-else>
<!-- 非编辑模式下的编辑和删除按钮 -->
<div class="action-btn edit-btn" title="编辑" @click="handleEditItem(index)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</div>
<div class="action-btn delete-btn" title="删除" @click="deletePackageItem(index)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</div>
</template>
</div>
</td>
</tr>
@@ -672,13 +694,14 @@
<script setup>
import useUserStore from '@/store/modules/user';
import { ref, reactive, onMounted, watch, computed, nextTick, getCurrentInstance } from 'vue';
import { ElMessage, ElAutocomplete, ElMessageBox } from 'element-plus';
import { ElMessage, ElAutocomplete, ElMessageBox, ElLoading } from 'element-plus';
import * as echarts from 'echarts';
import { useRouter, useRoute } from 'vue-router';
import { formatDate } from '@/utils/index';
import request from '@/utils/request';
import { listInspectionType, getInspectionType, addInspectionType, updateInspectionType, delInspectionType } from '@/api/system/inspectionType';
import { listLisGroup } from '@/api/system/checkType';
import { addInspectionPackage, saveInspectionPackageDetails, batchSaveInspectionPackageDetails, listInspectionPackage, getInspectionPackage, listInspectionPackageDetails } from '@/api/system/inspectionPackage';
import { getDiagnosisTreatmentList } from '@/views/catalog/diagnosistreatment/components/diagnosistreatment';
import { getLocationTree } from '@/views/charge/outpatientregistration/components/outpatientregistration';
@@ -1137,7 +1160,28 @@ const handleEditItem = (index) => {
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 {
@@ -1146,6 +1190,12 @@ const handleEditItem = (index) => {
}
};
// 取消编辑项目
const cancelEditItem = (index) => {
editingRowId.value = null;
ElMessage.info('已取消编辑');
};
// 计算单个项目的服务费(基于折扣后的金额)
const calculateItemServiceFee = (item) => {
if (!generateServiceFee.value) return 0;
@@ -1601,14 +1651,274 @@ const exportTable = () => {
// 套餐相关方法
const handleSave = () => {
// 验证套餐级别是否选择
// 验证必填字段
if (!packageLevel.value) {
alert('请选择套餐级别');
ElMessage.error('请选择套餐级别');
return;
}
// 这里可以添加其他表单验证和保存逻辑
console.log('保存表单数据');
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,
userId: userStore.nickName, // 制单人
discount: discount.value || 0,
isDisabled: isDisabled.value,
showPackageName: showPackageName.value,
generateServiceFee: generateServiceFee.value,
enablePackagePrice: enablePackagePrice.value,
packageAmount: packageAmount.value,
serviceFee: serviceFee.value,
lisGroup: '', // 从下拉框获取
bloodVolume: bloodVolume.value,
remarks: remarks.value,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString()
};
// 如果是科室套餐,添加科室信息
if (packageLevel.value === '科室套餐' && department.value) {
basicInfo.departmentId = department.value;
}
// 如果是个人套餐,添加用户信息
if (packageLevel.value === '个人套餐') {
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()
}));
console.log('准备保存的基本信息:', basicInfo);
console.log('准备保存的明细数据:', detailData);
// 调用保存API暂时使用模拟保存后续对接真实API
savePackageData(basicInfo, detailData);
};
// 保存套餐数据到数据库
const savePackageData = async (basicInfo, detailData) => {
try {
// 显示保存进度
const loading = ElLoading.service({
lock: true,
text: '正在保存...',
background: 'rgba(0, 0, 0, 0.7)',
});
// 1. 先保存基本信息
const basicResponse = await addInspectionPackage(basicInfo);
if (basicResponse.code !== 200) {
throw new Error(basicResponse.msg || '保存基本信息失败');
}
const packageId = basicResponse.data.packageId || basicResponse.data.id;
// 2. 批量保存明细数据关联套餐ID
const detailDataWithPackageId = detailData.map(item => ({
...item,
packageId: packageId
}));
const detailResponse = await batchSaveInspectionPackageDetails({
packageId: packageId,
details: detailDataWithPackageId
});
if (detailResponse.code !== 200) {
throw new Error(detailResponse.msg || '保存明细数据失败');
}
// 关闭加载提示
loading.close();
ElMessage.success('保存成功');
// 保存成功后重置表单
doResetForm();
} catch (error) {
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 {
// 其他错误
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;
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;
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 = () => {
@@ -1940,6 +2250,12 @@ watch(packageItems, (newVal) => {
pointer-events: auto;
}
.cancel-btn {
background-color: #FFA500;
color: white;
font-size: 14px;
}
/* 分页 */
.pagination {
display: flex;