Files
his/openhis-ui-vue3/src/views/doctorstation/components/adviceBaseList.vue
chenqi f3eeee7405 refactor(doctorstation): 优化医嘱基础列表组件性能和数据处理
- 实现虚拟滚动表格以提升大数据量渲染性能
- 添加数据缓存机制减少重复API请求
- 增强节流防抖功能优化搜索响应
- 重构数据过滤逻辑支持本地快速检索
- 添加加载状态提示改善用户体验
- 优化表格列宽度设置提升界面美观度
- 修复医保等级显示和价格获取逻辑
- 后端服务增加分批处理避免大量参数问题
- 添加空值安全检查防止运行时错误
- 统一数据结构处理药品耗材诊疗不同类型
2026-01-19 10:37:46 +08:00

439 lines
14 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 @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>