- 实现虚拟滚动表格以提升大数据量渲染性能 - 添加数据缓存机制减少重复API请求 - 增强节流防抖功能优化搜索响应 - 重构数据过滤逻辑支持本地快速检索 - 添加加载状态提示改善用户体验 - 优化表格列宽度设置提升界面美观度 - 修复医保等级显示和价格获取逻辑 - 后端服务增加分批处理避免大量参数问题 - 添加空值安全检查防止运行时错误 - 统一数据结构处理药品耗材诊疗不同类型
439 lines
14 KiB
Vue
439 lines
14 KiB
Vue
<template>
|
||
<div @keyup="handleKeyDown" tabindex="0" ref="tableWrapper">
|
||
<!-- 医保等级测试区域已隐藏 -->
|
||
<!--
|
||
<div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; background: #f5f5f5;">
|
||
<h3>医保等级测试</h3>
|
||
<div>
|
||
<div>1 -> {{ getMedicalInsuranceLevel({chrgitmLv: '1'}) }}</div>
|
||
<div>2 -> {{ getMedicalInsuranceLevel({chrgitmLv: '2'}) }}</div>
|
||
<div>3 -> {{ getMedicalInsuranceLevel({chrgitmLv: '3'}) }}</div>
|
||
<div>甲 -> {{ getMedicalInsuranceLevel({chrgitmLv: '甲'}) }}</div>
|
||
<div>乙 -> {{ getMedicalInsuranceLevel({chrgitmLv: '乙'}) }}</div>
|
||
<div>自 -> {{ getMedicalInsuranceLevel({chrgitmLv: '自'}) }}</div>
|
||
<div>甲类 -> {{ getMedicalInsuranceLevel({chrgitmLv: '甲类'}) }}</div>
|
||
<div>乙类 -> {{ getMedicalInsuranceLevel({chrgitmLv: '乙类'}) }}</div>
|
||
<div>自费 -> {{ getMedicalInsuranceLevel({chrgitmLv: '自费'}) }}</div>
|
||
</div>
|
||
</div>
|
||
-->
|
||
|
||
<!-- 使用Element Plus的虚拟滚动表格 -->
|
||
<el-table
|
||
ref="adviceBaseRef"
|
||
height="400"
|
||
:data="filteredAdviceBaseList"
|
||
highlight-current-row
|
||
@current-change="handleCurrentChange"
|
||
row-key="adviceDefinitionId"
|
||
@cell-click="clickRow"
|
||
v-loading="loading"
|
||
>
|
||
<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" 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>
|
||
{{
|
||
!scope.row.dose || isNaN(parseFloat(scope.row.dose))
|
||
? '-'
|
||
: parseFloat(scope.row.dose).toFixed(2) === '0.00'
|
||
? '-'
|
||
: parseFloat(scope.row.dose).toFixed(2) + scope.row.doseUnitCode_dictText
|
||
}}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<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" width="100">
|
||
<template #default="scope">
|
||
<span>
|
||
{{ getPriceFromInventory(scope.row) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<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" 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" 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>
|
||
<span v-else>{{ '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { getCurrentInstance, nextTick, onMounted, ref, computed, watch } from 'vue';
|
||
import { getAdviceBaseInfo, getDeviceList } from './api';
|
||
import { throttle, debounce } from 'lodash-es';
|
||
|
||
const { proxy } = getCurrentInstance();
|
||
// 使用系统统一的数据字典
|
||
const { med_category_code, chrgitm_lv } = proxy.useDict('med_category_code', 'chrgitm_lv');
|
||
|
||
// 医保等级映射(作为后备使用)
|
||
const medicalInsuranceLevelMap = {
|
||
'1': '甲类',
|
||
'2': '乙类',
|
||
'3': '自费'
|
||
};
|
||
|
||
// 获取药品分类名称 - 使用系统统一的数据字典
|
||
function getCategoryName(row) {
|
||
if (row.adviceType === 1) { // 药品类型
|
||
// 优先使用系统统一的药品分类数据字典
|
||
if (med_category_code.value && med_category_code.value.length > 0) {
|
||
const found = med_category_code.value.find(item => String(item.value) === String(row.categoryCode));
|
||
if (found) {
|
||
return found.label;
|
||
}
|
||
}
|
||
|
||
// 兼容处理:对于中草药可能的多种编码形式
|
||
const herbCodes = ['3', '03', '4', 3, 4];
|
||
if (herbCodes.includes(row.categoryCode)) {
|
||
return '中草药';
|
||
}
|
||
|
||
return '-';
|
||
} else if (row.adviceType === 2) { // 耗材类型
|
||
return '耗材';
|
||
} else if (row.adviceType === 3) { // 诊疗类型
|
||
return '诊疗';
|
||
}
|
||
return row.activityType_enumText || '-';
|
||
}
|
||
|
||
// 获取医保等级 - 简单直接的实现
|
||
function getMedicalInsuranceLevel(record) {
|
||
// 非常简单直接的实现:直接返回值或转换为中文
|
||
const value = record.chrgitmLv || record.insuranceClass || '';
|
||
|
||
if (!value) return '无';
|
||
|
||
// 简单映射
|
||
const map = {
|
||
'1': '甲类',
|
||
'2': '乙类',
|
||
'3': '自费',
|
||
'甲': '甲类',
|
||
'乙': '乙类',
|
||
'自': '自费',
|
||
'甲类': '甲类',
|
||
'乙类': '乙类',
|
||
'自费': '自费'
|
||
};
|
||
|
||
return map[value] || value;
|
||
}
|
||
|
||
const props = defineProps({
|
||
adviceQueryParams: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
patientInfo: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
});
|
||
const emit = defineEmits(['selectAdviceBase']);
|
||
|
||
// 使用缓存机制
|
||
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(
|
||
async () => {
|
||
// 只有在没有进行中的请求时才执行
|
||
if (!isRequestInProgress.value) {
|
||
await getList();
|
||
}
|
||
},
|
||
500, // 增加到500ms,减少频繁请求
|
||
{ leading: false, trailing: true }
|
||
);
|
||
|
||
// 监听参数变化 - 使用防抖优化
|
||
watch(
|
||
() => props.adviceQueryParams,
|
||
(newValue) => {
|
||
// 只有在搜索关键词长度达到一定要求时才触发搜索
|
||
if (newValue.searchKey && newValue.searchKey.length < 2) {
|
||
adviceBaseList.value = [];
|
||
return;
|
||
}
|
||
|
||
throttledGetList();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 获取数据
|
||
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) {
|
||
const price = row.priceList[0].price;
|
||
// 检查价格是否为有效数字
|
||
if (price !== undefined && price !== null && !isNaN(price) && isFinite(price)) {
|
||
return Number(price).toFixed(2) + ' 元';
|
||
}
|
||
// 如果价格无效,尝试从其他可能的字段获取价格
|
||
if (row.totalPrice !== undefined && row.totalPrice !== null && !isNaN(row.totalPrice) && isFinite(row.totalPrice)) {
|
||
return Number(row.totalPrice).toFixed(2) + ' 元';
|
||
}
|
||
if (row.price !== undefined && row.price !== null && !isNaN(row.price) && isFinite(row.price)) {
|
||
return Number(row.price).toFixed(2) + ' 元';
|
||
}
|
||
}
|
||
return '-';
|
||
}
|
||
|
||
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 = filteredAdviceBaseList.value;
|
||
|
||
if (data.length === 0) return;
|
||
|
||
switch (key) {
|
||
case 'ArrowUp': // 上箭头
|
||
event.preventDefault();
|
||
if (currentIndex.value > 0) {
|
||
currentIndex.value--;
|
||
currentSelectRow.value = data[currentIndex.value];
|
||
}
|
||
break;
|
||
case 'ArrowDown': // 下箭头
|
||
event.preventDefault();
|
||
if (currentIndex.value < data.length - 1) {
|
||
currentIndex.value++;
|
||
currentSelectRow.value = data[currentIndex.value];
|
||
}
|
||
break;
|
||
case 'Enter': // 回车键
|
||
event.preventDefault();
|
||
if (currentSelectRow.value) {
|
||
emit('selectAdviceBase', currentSelectRow.value);
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
// 当前行变化时更新索引
|
||
const handleCurrentChange = (currentRow) => {
|
||
currentIndex.value = filteredAdviceBaseList.value.findIndex((item) => item.adviceDefinitionId === currentRow.adviceDefinitionId);
|
||
currentSelectRow.value = currentRow;
|
||
};
|
||
|
||
// 点击行
|
||
function clickRow(row) {
|
||
emit('selectAdviceBase', row);
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
preloadData(); // 预加载数据
|
||
getList(); // 获取初始数据
|
||
});
|
||
|
||
defineExpose({
|
||
handleKeyDown,
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 保留原有的表格样式 */
|
||
</style>
|