208 检验项目设置-》套餐设置:项目名称字段未实现取值于《诊疗目录》做字典库

This commit is contained in:
2026-03-26 16:58:21 +08:00
parent 188b907907
commit d7c15848f0

View File

@@ -595,16 +595,120 @@
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="项目名称/规格">
<el-table-column label="项目名称/规格" min-width="200">
<template #default="{ row, $index }">
<template v-if="editingRowId === $index">
<div class="project-selector-cell">
<el-input
v-model="row.name"
placeholder="请输入或选择项目名称"
placeholder="点击选择项目"
size="small"
:ref="(el) => setItemNameRef(el, $index)"
@blur="validateItemName(row, $index)"
readonly
@click="openProjectSelector(row)"
style="width: 100%; cursor: pointer;"
>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<!-- 自定义 Popover 弹窗 -->
<el-popover
placement="bottom-start"
width="700"
trigger="manual"
v-model:visible="showProjectPopover"
popper-class="diagnosis-project-popover"
>
<div class="popover-container" v-loading="tableLoading">
<!-- 左侧:分类导航 -->
<div class="category-sidebar">
<div
v-for="cat in categoryList"
:key="cat.value"
class="category-item"
:class="{ active: currentCategoryCode === cat.value }"
@click="fetchProjectsByCategory(cat.value, true)"
>
{{ cat.label }}
</div>
</div>
<!-- 右侧:数据列表 -->
<div class="project-list-area">
<el-input
v-model="popoverSearchKey"
placeholder="搜索项目名称..."
size="small"
clearable
@input="handlePopoverSearch"
style="margin-bottom: 8px;"
>
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-table
:data="projectTableData"
:key="tableKey"
height="300"
highlight-current-row
@row-click="handleSelectProject"
style="width: 100%"
size="small"
>
<el-table-column prop="name" label="项目名称" show-overflow-tooltip min-width="150" />
<el-table-column prop="retailPrice" label="单价" width="80" align="right">
<template #default="{ row }">
<span v-if="row.retailPrice">¥{{ row.retailPrice }}</span>
</template>
</el-table-column>
<el-table-column prop="permittedUnitCode_dictText" label="单位" width="60" />
<el-table-column prop="categoryName" label="类别" width="80" />
</el-table>
<div v-if="projectTableData.length === 0 && !tableLoading" class="empty-tip">
暂无相关项目
</div>
<!-- 分页条(在列表下方) -->
<div @mousedown.stop @click.stop style="margin-top: 8px; display: flex; align-items: center; gap: 6px; justify-content: flex-end;">
<!-- 每页条数:独立 el-selectplacement=top 向上弹出teleported=false 保持在 popover DOM 内 -->
<el-select
v-if="projectTableData.length > 0 || popoverTotal > 0"
v-model="popoverPageSize"
size="small"
style="width: 110px; flex-shrink: 0;"
placement="top"
:teleported="false"
@change="handlePopoverSizeChange"
>
<el-option :value="10" label="10/" />
<el-option :value="20" label="20/" />
<el-option :value="50" label="50/" />
</el-select>
<el-pagination
v-if="projectTableData.length > 0 || popoverTotal > 0"
v-model:current-page="popoverPageNo"
:page-size="popoverPageSize"
:total="popoverTotal || projectTableData.length"
layout="prev, pager, next, jumper"
small
background
:teleported="false"
style="justify-content: flex-end;"
@current-change="handlePopoverPageChange"
/>
</div>
</div>
</div>
<template #reference>
<div style="display: none;"></div>
</template>
</el-popover>
<!-- 关闭由 document mousedown 监听处理 -->
</div>
</template>
<template v-else>
{{ row.name || '-' }}
@@ -766,7 +870,7 @@
<script setup>
import request from '@/utils/request';
import useUserStore from '@/store/modules/user';
import {computed, getCurrentInstance, nextTick, onActivated, onMounted, ref, watch} from 'vue';
import {computed, getCurrentInstance, nextTick, onActivated, onMounted, onUnmounted, ref, watch} from 'vue';
import {ElLoading, ElMessage, ElMessageBox} from 'element-plus';
import {Check, Edit, Plus, Delete, Refresh, RefreshRight, Search, Download, Close} from '@element-plus/icons-vue';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
@@ -780,7 +884,8 @@ import {
getDiagnosisTreatmentList,
addDiagnosisTreatment,
editDiagnosisTreatment,
stopDiseaseTreatment
stopDiseaseTreatment,
getDiseaseTreatmentInit
} from '@/views/catalog/diagnosistreatment/components/diagnosistreatment';
import {listLisGroup} from '@/api/system/checkType';
import {
@@ -902,15 +1007,10 @@ function isChildTypeRow(row) {
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);
return tableData.value.slice(start, start + typePageSize.value);
});
function typeGoPage(p) {
if (p < 1 || p > typeTotalPages.value) return;
@@ -1446,81 +1546,6 @@ 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 {
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 => {
ElMessage.error('查询诊疗目录失败,请稍后重试');
cb([]);
});
};
let addingItem = false;
const addPackageItem = () => {
if (addingItem) return;
@@ -1730,13 +1755,7 @@ const calculateAmounts = () => {
}
};
const itemNameRefs = ref([]); // 存储每行项目名称输入框的 DOM 引用
const setItemNameRef = (el, index) => {
if (el) {
itemNameRefs.value[index] = el;
}
};
const itemNameRefs = ref([]);
// 检验类型相关方法
@@ -2294,24 +2313,6 @@ const handleSave = () => {
savePackageData(basicInfo, detailData);
};
//定义表单数据
const form = ref({
tenantId: undefined, // 卫生机构 ID
packageType: '检验套餐', // 套餐类别
packageLevel: '', // 套餐级别
packageName: '', // 套餐名称
amount: 0.00, // 套餐金额
discount: '', // 折扣 %
creator: '超级管理员', // 制单人
remark: '', // 备注
isDisabled: false, // 是否停用 (false=启用)
showPackageName: true, // 显示套餐名
generateServiceFee: true, // 生成服务费
enablePackagePrice: true, // 套餐价格启用
serviceFee: 0.00, // 服务费
lisGroup: '', // lis分组
bloodVolume: '' // 血量
});
const fetchTenantList = async () => {
if (loadingTenant.value) return;
loadingTenant.value = true;
@@ -2354,22 +2355,12 @@ const fetchTenantList = async () => {
const zhonglianHospital = activeTenants.find(item => item.id === 1);
if (zhonglianHospital) {
form.value.tenantId = 1;
selectedTenantId.value = 1;
} else {
// 增加 userInfoStore 的存在性检查
// 防止 store 未初始化导致报错
const userStore = userInfoStore; // 获取 store 引用
if (userStore && userStore.tenantId) {
const currentTenant = activeTenants.find(item => item.id === userStore.tenantId);
form.value.tenantId = currentTenant ? currentTenant.id : activeTenants[0].id;
} else {
// 如果没有用户信息或 store 未就绪,直接选第一个
form.value.tenantId = activeTenants[0].id;
}
selectedTenantId.value = activeTenants[0].id;
}
} else {
form.value.tenantId = undefined;
selectedTenantId.value = null;
}
} catch (error) {
@@ -2380,7 +2371,274 @@ const fetchTenantList = async () => {
loadingTenant.value = false;
}
};
const showProjectPopover = ref(false);
const categoryList = ref([]);
const currentCategoryCode = ref('');
const projectTableData = ref([]);
const tableLoading = ref(false);
const popoverSearchKey = ref('');
const currentEditingRow = ref(null);
const isSelectingProject = ref(false);
const tableKey = ref(0);
// 弹窗内项目列表分页
const popoverPageNo = ref(1);
const popoverPageSize = ref(10);
const popoverTotal = ref(0);
/**
* 点击输入框时:记录当前行并打开弹窗
*/
const openProjectSelector = (row) => {
if (isSelectingProject.value) return;
currentEditingRow.value = row;
showProjectPopover.value = true;
if (categoryList.value.length === 0) {
initDiagnosisCategories();
} else {
fetchProjectsByCategory(currentCategoryCode.value, true);
}
};
// 监听 popover 开关,动态注册/注销 document 级监听
watch(showProjectPopover, (val) => {
if (val) {
// nextTick 后注册,避免打开时的点击事件立即触发关闭
nextTick(() => {
document.addEventListener('mousedown', _captureMousedown, true);
document.addEventListener('mousedown', onDocumentMousedown);
});
} else {
document.removeEventListener('mousedown', _captureMousedown, true);
document.removeEventListener('mousedown', onDocumentMousedown);
}
});
const handleSelectProject = (selectedItem) => {
if (!currentEditingRow.value) {
ElMessage.warning('未检测到编辑行,请重新点击输入框');
return;
}
const row = currentEditingRow.value;
// --- 数据回填逻辑 ---
row.name = selectedItem.name;
row.spec = selectedItem.spec || '';
row.unitPrice = parseFloat(selectedItem.retailPrice || 0);
row.unit = selectedItem.permittedUnitCode_dictText || selectedItem.unit || '次';
if (!row.quantity) row.quantity = 1;
row.amount = row.unitPrice * row.quantity;
row.totalAmount = row.amount + (row.serviceFee || 0);
// 1. 开启“防抖”锁,阻止 openProjectSelector 执行
isSelectingProject.value = true;
// 2. 关闭弹窗
showProjectPopover.value = false;
// 3. 延迟一小段时间后解锁并清空当前行
// 100ms 足够让浏览器的 click/focus 事件处理完毕
setTimeout(() => {
currentEditingRow.value = null;
isSelectingProject.value = false;
}, 100);
ElMessage.success(`已选择:${row.name}`);
};
const initDiagnosisCategories = async () => {
if (categoryList.value.length > 0) return;
try {
const res = await getDiseaseTreatmentInit();
if (res.code === 200 && res.data?.diagnosisCategoryOptions) {
categoryList.value = res.data.diagnosisCategoryOptions.map(item => ({
value: item.value,
label: item.info
}));
if (categoryList.value.length > 0) {
currentCategoryCode.value = categoryList.value[0].value;
fetchProjectsByCategory(currentCategoryCode.value, true);
}
}
} catch (e) {
ElMessage.error('加载分类失败,请稍后重试');
}
};
/**
* 根据分类代码加载项目列表(支持分页)
*/
// 分页数据缓存key = `${categoryCode}_${pageNo}_${pageSize}_${searchKey}`
const _pageCache = new Map();
const _cacheKey = (code, pageNo, pageSize, searchKey) =>
`${code}_${pageNo}_${pageSize}_${searchKey || ''}`;
/** 静默预加载单页,结果存入缓存 */
const _prefetchPage = (code, pageNo, pageSize, searchKey) => {
const key = _cacheKey(code, pageNo, pageSize, searchKey);
if (_pageCache.has(key)) return;
const params = { pageNo, pageSize, statusEnum: 2, categoryCode: code, searchKey: searchKey || undefined };
getDiagnosisTreatmentList(params).then(response => {
if (response.code === 200) _pageCache.set(key, response.data);
}).catch(() => {});
};
/** 静默并行预加载多页当前页前后各2页 + 后4页覆盖常用跳转范围 */
const _prefetchPages = (code, currentPage, pageSize, searchKey, totalPages) => {
const pagesToFetch = new Set();
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages || 999, currentPage + 4); i++) {
pagesToFetch.add(i);
}
pagesToFetch.forEach(pageNo => _prefetchPage(code, pageNo, pageSize, searchKey));
};
/** 第1页加载完成后并行预取所有页上限20页覆盖任意跳转 */
const _prefetchAllPages = (code, pageSize, searchKey, total) => {
const totalPages = Math.min(Math.ceil(total / pageSize), 80);
for (let i = 2; i <= totalPages; i++) {
_prefetchPage(code, i, pageSize, searchKey);
}
};
const fetchProjectsByCategory = async (code, resetPage = false) => {
currentCategoryCode.value = code;
if (resetPage) {
popoverPageNo.value = 1;
// 切换分类/搜索时清空缓存
_pageCache.clear();
}
const pageNo = popoverPageNo.value;
const pageSize = popoverPageSize.value;
const searchKey = popoverSearchKey.value || '';
const key = _cacheKey(code, pageNo, pageSize, searchKey);
if (_pageCache.has(key)) {
// 命中缓存,直接渲染,无需 loading
const cached = _pageCache.get(key);
if (cached && cached.records) {
projectTableData.value = cached.records;
popoverTotal.value = cached.total != null ? cached.total : cached.records.length;
} else if (Array.isArray(cached)) {
projectTableData.value = cached;
popoverTotal.value = cached.length;
}
// 预加载周边页
_prefetchPages(code, pageNo, pageSize, searchKey, Math.ceil(popoverTotal.value / pageSize));
return;
}
tableLoading.value = true;
try {
const params = {
pageNo,
pageSize,
statusEnum: 2,
categoryCode: code,
searchKey: searchKey || undefined
};
const response = await getDiagnosisTreatmentList(params);
if (response.code === 200) {
let list = [];
if (response.data && response.data.records) {
list = response.data.records;
popoverTotal.value = (response.data.total != null ? response.data.total : list.length);
} else if (Array.isArray(response.data)) {
list = response.data;
popoverTotal.value = list.length;
}
projectTableData.value = list;
// 写入缓存
_pageCache.set(key, response.data);
// 预加载周边页
const totalPages = Math.ceil(popoverTotal.value / pageSize);
_prefetchPages(code, pageNo, pageSize, searchKey, totalPages);
} else {
projectTableData.value = [];
popoverTotal.value = 0;
ElMessage.warning(response.msg || '暂无数据');
}
} catch (error) {
ElMessage.error('查询失败');
projectTableData.value = [];
popoverTotal.value = 0;
} finally {
tableLoading.value = false;
}
};
/**
* 弹窗内搜索触发(重置页码)
*/
const handlePopoverSearch = () => {
if (currentCategoryCode.value) {
fetchProjectsByCategory(currentCategoryCode.value, true);
}
};
// 捕获阶段记录真实点击目标(在 DOM 变化前)
let _capturedTarget = null;
const _captureMousedown = (e) => { _capturedTarget = e.target; };
// 分页操作时暂时屏蔽 document mousedown 关闭逻辑
let _ignoreMaskOnce = false;
/**
* 弹窗内分页页码变化
*/
const handlePopoverPageChange = (page) => {
_ignoreMaskOnce = true;
popoverPageNo.value = page;
fetchProjectsByCategory(currentCategoryCode.value);
};
/**
* 弹窗内每页条数变化
*/
const handlePopoverSizeChange = (size) => {
_ignoreMaskOnce = true;
popoverPageSize.value = size;
popoverPageNo.value = 1;
fetchProjectsByCategory(currentCategoryCode.value);
};
/**
* document 级 mousedown 监听:点击 popover 面板及所有浮层之外时关闭
* 使用捕获阶段记录的真实目标,避免 DOM 销毁后 contains 检测失效
*/
const onDocumentMousedown = (e) => {
if (!showProjectPopover.value) return;
// 分页操作触发时,忽略本次关闭
if (_ignoreMaskOnce) {
_ignoreMaskOnce = false;
return;
}
// 使用捕获阶段记录的目标DOM 变化前的真实元素)
const target = _capturedTarget || e.target;
// 判断点击目标是否在 popover 面板内
const popoverEl = document.querySelector('.diagnosis-project-popover');
if (popoverEl && popoverEl.contains(target)) return;
// 判断点击目标是否在任意浮层内el-select-dropdown / el-popper 等)
const floatEls = document.querySelectorAll('.el-select-dropdown, .el-popper, .el-picker-panel');
for (const el of floatEls) {
if (el.contains(target)) return;
}
// 用坐标兜底:浮层可能已销毁,检查元素是否曾属于浮层类名
if (target && target.closest && target.closest('.el-select-dropdown, .el-popper, .el-picker-panel')) return;
showProjectPopover.value = false;
};
// 保存套餐数据到数据库
@@ -2601,8 +2859,6 @@ const handlePackageManagement = () => {
const refreshPage = () => {
getInspectionTypeList();
// 刷新时也重新加载套餐项目
// loadPackageItemsFromAPI();
};
@@ -2693,7 +2949,7 @@ onMounted(async () => {
// 3. 等待 Vue 完成下一轮的 DOM 更新
await nextTick();
// 4. 【核心逻辑】强制默认选中中联医院 (ID = 1)
// 4. 【核心逻辑】强制默认选中"中联医院" (ID = 1)
// 检查下拉列表中是否存在 value 为 1 的选项
const hasZhonglian = tenantOptions.value.some(item => item.value === 1);
@@ -2714,15 +2970,18 @@ onMounted(async () => {
selectedTenantId.value = tenantOptions.value[0].value;
}
}
});
onUnmounted(() => {
document.removeEventListener('mousedown', _captureMousedown, true);
document.removeEventListener('mousedown', onDocumentMousedown);
});
</script>
<style>
/* Element UI 表格编辑行样式 */
/* 编辑模式行样式 */
.el-table .editing-row {
background-color: #fdf6ec !important;
}
@@ -2985,8 +3244,83 @@ onMounted(async () => {
flex-wrap: nowrap;
}
/* 编辑模式行样式 */
.el-table .editing-row {
background-color: #f0f7ff;
/* 诊疗项目选择器单元格 */
.project-selector-cell {
position: relative;
width: 100%;
}
</style>
/* 全屏遮罩,确保点击弹窗外任意位置关闭 */
.popover-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999; /* 低于 popover 的 z-index */
background: transparent;
}
/* Popover 内部容器布局 */
.popover-container {
display: flex;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: #fff;
}
/* 左侧分类栏 */
.category-sidebar {
width: 120px;
background-color: #f5f7fa;
border-right: 1px solid #e4e7ed;
max-height: 400px;
overflow-y: auto;
flex-shrink: 0;
}
.category-item {
padding: 12px 15px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.category-item:hover {
background-color: #ecf5ff;
color: #409EFF;
}
.category-item.active {
background-color: #ecf5ff;
color: #409EFF;
border-left-color: #409EFF;
font-weight: 600;
}
/* 右侧列表区域 */
.project-list-area {
flex: 1;
padding: 10px;
max-height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.empty-tip {
text-align: center;
color: #909399;
margin-top: 20px;
font-size: 13px;
}
/* 全局样式:调整 popover 的 padding 和 z-index */
.diagnosis-project-popover {
padding: 0 !important;
z-index: 2000 !important;
}</style>