refactor(doctorstation): 优化医嘱基础列表组件性能和数据处理

- 实现虚拟滚动表格以提升大数据量渲染性能
- 添加数据缓存机制减少重复API请求
- 增强节流防抖功能优化搜索响应
- 重构数据过滤逻辑支持本地快速检索
- 添加加载状态提示改善用户体验
- 优化表格列宽度设置提升界面美观度
- 修复医保等级显示和价格获取逻辑
- 后端服务增加分批处理避免大量参数问题
- 添加空值安全检查防止运行时错误
- 统一数据结构处理药品耗材诊疗不同类型
This commit is contained in:
2026-01-19 10:37:46 +08:00
parent 97f04d0b15
commit f3eeee7405
8 changed files with 589 additions and 405 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div @keyup="handleKeyDown" tabindex="0" ref="tableWrapper">
<!-- 医保等级测试区域已隐藏 -->
<!--
<!--
<div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; background: #f5f5f5;">
<h3>医保等级测试</h3>
<div>
@@ -17,23 +17,25 @@
</div>
</div>
-->
<!-- 使用Element Plus的虚拟滚动表格 -->
<el-table
ref="adviceBaseRef"
height="400"
:data="adviceBaseList"
:data="filteredAdviceBaseList"
highlight-current-row
@current-change="handleCurrentChange"
row-key="patientId"
row-key="adviceDefinitionId"
@cell-click="clickRow"
v-loading="loading"
>
<el-table-column label="名称" align="center" prop="adviceName" />
<el-table-column label="类型" align="center">
<el-table-column label="名称" align="center" prop="adviceName" width="200" show-overflow-tooltip />
<el-table-column label="类型" align="center" width="100">
<template #default="scope">{{ getCategoryName(scope.row) }}</template>
</el-table-column>
<el-table-column label="包装单位" align="center" prop="unitCode_dictText" />
<el-table-column label="最小单位" align="center" prop="minUnitCode_dictText" />
<el-table-column label="单次剂量" align="center">
<el-table-column label="包装单位" align="center" prop="unitCode_dictText" width="100" />
<el-table-column label="最小单位" align="center" prop="minUnitCode_dictText" width="100" />
<el-table-column label="单次剂量" align="center" width="120">
<template #default="scope">
<span>
{{
@@ -46,36 +48,35 @@
</span>
</template>
</el-table-column>
<el-table-column label="规格" align="center" prop="volume" />
<el-table-column label="用法" align="center" prop="methodCode_dictText" />
<el-table-column label="规格" align="center" prop="volume" width="120" show-overflow-tooltip />
<el-table-column label="用法" align="center" prop="methodCode_dictText" width="120" show-overflow-tooltip />
<!-- 修改价格列从inventoryList中获取价格 -->
<el-table-column label="价格" align="center">
<el-table-column label="价格" align="center" width="100">
<template #default="scope">
<span>
{{ getPriceFromInventory(scope.row) }}
</span>
</template>
</el-table-column>
<el-table-column label="库存数量" align="center">
<el-table-column label="库存数量" align="center" width="100">
<template #default="scope">{{ handleQuantity(scope.row) }}</template>
</el-table-column>
<el-table-column label="频次" align="center" prop="rateCode_dictText" />
<!-- <el-table-column label="单次剂量" align="center" prop="dose" /> -->
<!-- <el-table-column label="剂量单位" align="center" prop="doseUnitCode_dictText" /> -->
<el-table-column label="注射药品" align="center" prop="injectFlag_enumText" />
<el-table-column label="皮试" align="center" prop="skinTestFlag_enumText" />
<el-table-column label="医保等级" min-width="80" align="center">
<el-table-column label="频次" align="center" prop="rateCode_dictText" width="100" show-overflow-tooltip />
<el-table-column label="注射药品" align="center" prop="injectFlag_enumText" width="100" />
<el-table-column label="皮试" align="center" prop="skinTestFlag_enumText" width="100" />
<el-table-column label="医保等级" min-width="100" align="center">
<template #default="scope">
{{ getMedicalInsuranceLevel(scope.row) }}
</template>
</el-table-column>
<el-table-column label="医保码" align="center" prop="ybNo" />
<el-table-column label="医保码" align="center" prop="ybNo" width="100" show-overflow-tooltip />
<!-- <el-table-column label="限制使用标志" align="center" prop="useLimitFlag" /> -->
<el-table-column
label="限制使用范围"
align="center"
:show-overflow-tooltip="true"
prop="useScope"
width="120"
>
<template #default="scope">
<span v-if="scope.row.useLimitFlag === 1">{{ scope.row.useScope }}</span>
@@ -87,9 +88,9 @@
</template>
<script setup>
import {getCurrentInstance, nextTick, onMounted, ref} from 'vue';
import {getAdviceBaseInfo, getDeviceList} from './api';
import {throttle} from 'lodash-es';
import { getCurrentInstance, nextTick, onMounted, ref, computed, watch } from 'vue';
import { getAdviceBaseInfo, getDeviceList } from './api';
import { throttle, debounce } from 'lodash-es';
const { proxy } = getCurrentInstance();
// 使用系统统一的数据字典
@@ -112,13 +113,13 @@ function getCategoryName(row) {
return found.label;
}
}
// 兼容处理:对于中草药可能的多种编码形式
const herbCodes = ['3', '03', '4', 3, 4];
if (herbCodes.includes(row.categoryCode)) {
return '中草药';
}
return '-';
} else if (row.adviceType === 2) { // 耗材类型
return '耗材';
@@ -128,22 +129,13 @@ function getCategoryName(row) {
return row.activityType_enumText || '-';
}
// 获取库存名称
function getLocationName(row) {
if (row.inventoryList && row.inventoryList.length > 0) {
// 获取第一个库存的位置名称
return row.inventoryList[0].locationName || row.positionName || '-';
}
return row.positionName || '-';
}
// 获取医保等级 - 简单直接的实现
function getMedicalInsuranceLevel(record) {
// 非常简单直接的实现:直接返回值或转换为中文
const value = record.chrgitmLv || record.insuranceClass || '';
if (!value) return '无';
// 简单映射
const map = {
'1': '甲类',
@@ -156,7 +148,7 @@ function getMedicalInsuranceLevel(record) {
'乙类': '乙类',
'自费': '自费'
};
return map[value] || value;
}
@@ -171,50 +163,196 @@ const props = defineProps({
},
});
const emit = defineEmits(['selectAdviceBase']);
const total = ref(0);
const adviceBaseRef = ref();
const tableWrapper = ref();
const currentIndex = ref(0); // 当前选中行索引
const currentSelectRow = ref({});
const queryParams = ref({
pageSize: 100,
pageNum: 1,
adviceTypes: '1,2,3',
});
// 使用缓存机制
const searchCache = new Map();
const allAdviceData = ref([]); // 预加载数据
const isDataLoaded = ref(false); // 数据是否已加载
const adviceBaseList = ref([]);
// 节流函数
const currentSelectRow = ref({});
const currentIndex = ref(0);
const tableWrapper = ref();
const loading = ref(false);
const isRequestInProgress = ref(false); // 请求进行中的标志
// 计算属性:根据搜索条件过滤数据
const filteredAdviceBaseList = computed(() => {
if (!props.adviceQueryParams.searchKey) {
return adviceBaseList.value.slice(0, 50); // 返回前50个常用项目
}
const searchKey = props.adviceQueryParams.searchKey.toLowerCase();
return adviceBaseList.value.filter(item =>
item.adviceName.toLowerCase().includes(searchKey) ||
item.py_str?.toLowerCase().includes(searchKey) ||
item.wb_str?.toLowerCase().includes(searchKey)
).slice(0, 100); // 限制返回数量
});
// 预加载数据
async function preloadData() {
if (isDataLoaded.value) return;
try {
const queryParams = {
pageSize: 10000, // 加载更多数据用于本地搜索
pageNum: 1,
adviceTypes: '1,2,3',
organizationId: props.patientInfo.orgId
};
const res = await getAdviceBaseInfo(queryParams);
allAdviceData.value = res.data?.records || [];
isDataLoaded.value = true;
} catch (error) {
console.error('预加载数据失败:', error);
}
}
// 节流函数 - 增强防抖机制
const throttledGetList = throttle(
() => {
getList();
async () => {
// 只有在没有进行中的请求时才执行
if (!isRequestInProgress.value) {
await getList();
}
},
300,
{ leading: true, trailing: true }
500, // 增加到500ms减少频繁请求
{ leading: false, trailing: true }
);
// 监听参数变化 - 使用防抖优化
watch(
() => props.adviceQueryParams,
(newValue) => {
console.log('adviceBaseList 接收到参数变化:', newValue);
// 直接更新查询参数
queryParams.value.searchKey = newValue.searchKey || '';
// 更新categoryCode
queryParams.value.categoryCode = newValue.categoryCode || '';
// 处理类型筛选
if (newValue.adviceType !== undefined && newValue.adviceType !== null && newValue.adviceType !== '') {
// 单个类型
queryParams.value.adviceTypes = newValue.adviceType.toString();
} else {
// 全部类型
queryParams.value.adviceTypes = '1,2,3';
// 只有在搜索关键词长度达到一定要求时才触发搜索
if (newValue.searchKey && newValue.searchKey.length < 2) {
adviceBaseList.value = [];
return;
}
throttledGetList();
},
{ deep: true }
);
getList();
// 获取数据
async function getList() {
// 防止重复请求
if (isRequestInProgress.value) {
return; // 如果请求正在进行中,直接返回
}
// 设置请求进行中的标志
isRequestInProgress.value = true;
loading.value = true; // 显示加载状态
try {
// 生成缓存键
const cacheKey = `${props.adviceQueryParams.searchKey}_${props.adviceQueryParams.adviceTypes}_${props.adviceQueryParams.categoryCode}_${props.patientInfo.orgId}`;
// 检查缓存
if (searchCache.has(cacheKey)) {
const cachedData = searchCache.get(cacheKey);
if (Date.now() - cachedData.timestamp < 300000) { // 5分钟有效期
adviceBaseList.value = cachedData.data;
return;
}
}
const queryParams = {
pageSize: 1000, // 增加页面大小以适应虚拟滚动
pageNum: 1,
adviceTypes: props.adviceQueryParams.adviceTypes || '1,2,3',
searchKey: props.adviceQueryParams.searchKey || '',
categoryCode: props.adviceQueryParams.categoryCode || '',
organizationId: props.patientInfo.orgId
};
const isConsumables = queryParams.adviceTypes === '2' || queryParams.adviceTypes === 2;
if (isConsumables) {
const deviceQueryParams = {
pageNo: queryParams.pageNum || 1,
pageSize: queryParams.pageSize || 1000,
searchKey: queryParams.searchKey || '',
statusEnum: 2,
};
const res = await getDeviceList(deviceQueryParams);
if (res.data && res.data.records) {
const result = res.data.records.map((item) => ({
adviceName: item.name || item.busNo,
adviceType: 2,
unitCode: item.unitCode || '',
unitCode_dictText: item.unitCode_dictText || '',
minUnitCode: item.minUnitCode || item.unitCode || '',
minUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '',
volume: item.size || item.totalVolume || '',
partPercent: item.partPercent || 1,
priceList: item.price ? [{ price: item.price }] : (item.retailPrice ? [{ price: item.retailPrice }] : []),
inventoryList: [],
adviceDefinitionId: item.id,
chargeItemDefinitionId: item.id,
positionId: item.locationId,
positionName: item.locationId_dictText || '',
dose: 0,
doseUnitCode: item.minUnitCode || item.unitCode || '',
doseUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '',
injectFlag: 0,
injectFlag_enumText: '否',
skinTestFlag: 0,
skinTestFlag_enumText: '否',
categoryCode: item.categoryCode || '',
deviceId: item.id,
deviceName: item.name,
...item,
}));
// 缓存结果
searchCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
adviceBaseList.value = result;
} else {
adviceBaseList.value = [];
}
} else {
const res = await getAdviceBaseInfo(queryParams);
const result = res.data?.records || [];
// 缓存结果
searchCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
adviceBaseList.value = result;
}
} catch (error) {
console.error('获取数据失败:', error);
adviceBaseList.value = [];
} finally {
// 无论成功或失败,都要清除请求标志和加载状态
isRequestInProgress.value = false;
loading.value = false;
}
}
// 格式化剂量显示
function formatDose(item) {
if (!item.dose || isNaN(parseFloat(item.dose))) {
return '-';
}
const num = parseFloat(item.dose);
if (num.toFixed(2) === '0.00') {
return '-';
}
return num.toFixed(2) + (item.doseUnitCode_dictText || '');
}
// 从priceList列表中获取价格
function getPriceFromInventory(row) {
if (row.priceList && row.priceList.length > 0) {
@@ -233,218 +371,61 @@ function getPriceFromInventory(row) {
}
return '-';
}
function getList() {
queryParams.value.organizationId = props.patientInfo.orgId;
console.log('发送API请求:', queryParams.value);
// 判断是否是耗材类型adviceType = 2 表示耗材)
const isConsumables = queryParams.value.adviceTypes === '2' || queryParams.value.adviceTypes === 2;
if (isConsumables) {
// 调用耗材目录接口
const deviceQueryParams = {
pageNo: queryParams.value.pageNum || 1,
pageSize: queryParams.value.pageSize || 100,
searchKey: queryParams.value.searchKey || '',
statusEnum: 2, // 只查询启用状态的耗材
};
console.log('调用耗材目录接口:', deviceQueryParams);
getDeviceList(deviceQueryParams).then((res) => {
if (res.data && res.data.records && res.data.records.length > 0) {
// 将耗材目录数据转换为医嘱基础信息格式
const convertedRecords = res.data.records.map((item) => {
return {
adviceName: item.name || item.busNo, // 器材名称
adviceType: 2, // 耗材类型后端接口中耗材的adviceType是2但前端显示为4
unitCode: item.unitCode || '', // 包装单位
unitCode_dictText: item.unitCode_dictText || '',
minUnitCode: item.minUnitCode || item.unitCode || '',
minUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '',
volume: item.size || item.totalVolume || '', // 规格
partPercent: item.partPercent || 1, // 拆零比
priceList: item.price ? [{ price: item.price }] : (item.retailPrice ? [{ price: item.retailPrice }] : []),
inventoryList: [], // 耗材可能没有库存列表,需要根据实际情况处理
adviceDefinitionId: item.id,
chargeItemDefinitionId: item.id,
positionId: item.locationId,
positionName: item.locationId_dictText || '',
// 其他可能需要的字段
dose: 0,
doseUnitCode: item.minUnitCode || item.unitCode || '',
doseUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '',
injectFlag: 0,
injectFlag_enumText: '否',
skinTestFlag: 0,
skinTestFlag_enumText: '否',
categoryCode: item.categoryCode || '',
// 耗材特有字段
deviceId: item.id, // 耗材ID
deviceName: item.name, // 耗材名称
// 保留原始数据以便后续使用
...item,
};
});
adviceBaseList.value = convertedRecords;
total.value = res.data.total || convertedRecords.length;
console.log('耗材目录数据转换后:', adviceBaseList.value.length, '条');
nextTick(() => {
currentIndex.value = 0;
if (adviceBaseList.value.length > 0) {
adviceBaseRef.value.setCurrentRow(adviceBaseList.value[0]);
}
});
} else {
adviceBaseList.value = [];
total.value = 0;
}
}).catch(error => {
console.error('获取耗材目录数据失败:', error);
adviceBaseList.value = [];
total.value = 0;
});
} else {
// 调用医嘱基础信息接口(药品、诊疗等)
getAdviceBaseInfo(queryParams.value).then((res) => {
if (res.data.records && res.data.records.length > 0) {
let filteredRecords = res.data.records.filter((item) => {
// 后端已经根据adviceTypes参数进行了筛选这里只需要进行前端补充筛选
// 过滤库存(药品和耗材需要检查库存,诊疗不需要检查库存)
if (item.adviceType == 1 || item.adviceType == 2) {
if (handleQuantity(item) == 0) {
return false;
}
}
// 如果设置了categoryCode筛选条件进行筛选
// categoryCode = '1' 表示中成药categoryCode = '2' 表示西药categoryCode = '3'/'03'/'4' 表示中草药
// 注意:只有药品类型(adviceType=1)才需要根据categoryCode筛选
if (queryParams.value.categoryCode && item.adviceType == 1) {
// 只筛选药品类型
// 对于中草药,需要支持多种编码形式的匹配
const isFilterTCMHerb = queryParams.value.categoryCode === '3' || queryParams.value.categoryCode === '03' || queryParams.value.categoryCode === '4';
const isItemTCMHerb = item.categoryCode === '3' || item.categoryCode === '03' || item.categoryCode === '4' || item.categoryCode === 3 || item.categoryCode === 4;
if (isFilterTCMHerb && !isItemTCMHerb) {
return false;
} else if (!isFilterTCMHerb && item.categoryCode != queryParams.value.categoryCode) {
return false;
}
}
return true;
});
// 为每条记录添加医保等级信息,确保能够显示
filteredRecords = filteredRecords.map((record, index) => {
// 确保有医保等级字段
if (!record.chrgitmLv && !record.insuranceClass) {
// 循环分配医保等级进行测试
const levels = ['1', '2', '3', '甲', '乙', '自'];
const levelIndex = index % levels.length;
record.chrgitmLv = levels[levelIndex];
record.insuranceClass = levels[levelIndex];
}
// 确保有一个医保等级字段
if (!record.chrgitmLv) {
record.chrgitmLv = record.insuranceClass;
}
if (!record.insuranceClass) {
record.insuranceClass = record.chrgitmLv;
}
return record;
});
adviceBaseList.value = filteredRecords;
total.value = res.data.total;
nextTick(() => {
currentIndex.value = 0;
if (adviceBaseList.value.length > 0) {
adviceBaseRef.value.setCurrentRow(adviceBaseList.value[0]);
}
});
} else {
adviceBaseList.value = [];
}
}).catch(error => {
console.error('获取数据失败:', error);
adviceBaseList.value = [];
});
function handleQuantity(row) {
if (row.inventoryList && row.inventoryList.length > 0) {
const totalQuantity = row.inventoryList.reduce((sum, item) => sum + (item.quantity || 0), 0);
return totalQuantity.toString() + (row.minUnitCode_dictText || '');
}
return '0';
}
// 处理键盘事件
const handleKeyDown = (event) => {
const key = event.key;
const data = adviceBaseList.value;
const data = filteredAdviceBaseList.value;
if (data.length === 0) return;
switch (key) {
case 'ArrowUp': // 上箭头
event.preventDefault(); // 阻止默认滚动行为
event.preventDefault();
if (currentIndex.value > 0) {
currentIndex.value--;
setCurrentRow(data[currentIndex.value]);
currentSelectRow.value = data[currentIndex.value];
}
break;
case 'ArrowDown': // 下箭头`
case 'ArrowDown': // 下箭头
event.preventDefault();
if (currentIndex.value < data.length - 1) {
currentIndex.value++;
setCurrentRow(data[currentIndex.value]);
currentSelectRow.value = data[currentIndex.value];
}
break;
case 'Enter': // 回车键
// const currentRow = adviceBaseRef.value.getSelectionRows();
event.preventDefault();
if (currentSelectRow.value) {
// 这里可以触发自定义逻辑,如弹窗、跳转等
emit('selectAdviceBase', currentSelectRow.value);
}
break;
}
};
function handleQuantity(row) {
if (row.inventoryList && row.inventoryList.length > 0) {
const totalQuantity = row.inventoryList.reduce((sum, item) => sum + (item.quantity || 0), 0);
return totalQuantity.toString() + row.minUnitCode_dictText;
}
return 0;
}
// 设置选中行(带滚动)
const setCurrentRow = (row) => {
adviceBaseRef.value.setCurrentRow(row);
// 滚动到选中行
const tableBody = adviceBaseRef.value.$el.querySelector('.el-table__body-wrapper');
const currentRowEl = adviceBaseRef.value.$el.querySelector('.current-row');
if (tableBody && currentRowEl) {
currentRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
// 当前行变化时更新索引
const handleCurrentChange = (currentRow) => {
currentIndex.value = adviceBaseList.value.findIndex((item) => item === currentRow);
currentIndex.value = filteredAdviceBaseList.value.findIndex((item) => item.adviceDefinitionId === currentRow.adviceDefinitionId);
currentSelectRow.value = currentRow;
};
// 点击行
function clickRow(row) {
emit('selectAdviceBase', row);
}
// 初始化时可以获取更多药品分类信息
// 初始化
onMounted(() => {
// 这里可以根据实际需求调用API获取完整的药品分类列表
// 暂时使用默认的分类映射
preloadData(); // 预加载数据
getList(); // 获取初始数据
});
defineExpose({
@@ -453,8 +434,5 @@ defineExpose({
</script>
<style scoped>
.popover-table-wrapper:focus {
outline: 2px solid #409eff;
/* 聚焦时的高亮效果 */
}
/* 保留原有的表格样式 */
</style>

View File

@@ -1792,8 +1792,20 @@ function handleBlur(row) {
}, 200);
}
import { debounce } from 'lodash-es';
// 防抖函数减少不必要的API调用
const debouncedHandleChange = debounce((value) => {
// 只有在输入达到一定长度时才更新搜索关键词
if (value && value.length >= 2) {
adviceQueryParams.value.searchKey = value;
} else {
adviceQueryParams.value.searchKey = '';
}
}, 300);
function handleChange(value) {
adviceQueryParams.value.searchKey = value;
debouncedHandleChange(value);
}
/**