feat(menu): 优化菜单服务性能并新增医生排班功能

- 添加菜单缓存注解以提升查询性能
- 实现菜单完整路径计算优化,解决 N+1 查询问题
- 新增 selectAllMenus 方法供路径计算使用
- 添加今日医生排班查询功能
- 重构前端图标显示逻辑,使用 SVG 图标替代 Element 图标
- 添加前端菜单数据本地缓存机制
- 更新菜单管理界面的表单组件绑定方式
- 新增预约管理、门诊管理和药房管理路由配置
This commit is contained in:
2026-02-02 08:46:33 +08:00
parent 669d669422
commit 5534a71c7d
20 changed files with 1156 additions and 228 deletions

View File

@@ -39,9 +39,7 @@
<span class="custom-tree-node">
<div class="tree-node-info">
<div class="node-main">
<el-icon :size="16" :color="getIconColor(data)">
<component :is="getIconComponent(data.icon)" />
</el-icon>
<svg-icon :icon-class="data.icon" :style="{ color: getIconColor(data) }" />
<span class="menu-label" style="margin-left: 8px;">{{ node.label }}</span>
<el-tag v-if="data.fullPath" type="info" size="small" effect="plain" class="path-tag-inline">
{{ data.fullPath }}
@@ -88,9 +86,7 @@
class="selected-function-item"
>
<div class="function-info">
<el-icon :size="16" :color="getIconColor(item)">
<component :is="getIconComponent(item.icon)" />
</el-icon>
<svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
<div class="function-details">
<span class="function-name">{{ item.menuName }}</span>
<el-tag v-if="item.fullPath" type="info" size="small" class="function-path-below">
@@ -193,6 +189,7 @@ import {
SuccessFilled,
QuestionFilled as QuestionFilledIcon
} from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
// 添加 loading 状态
const loading = ref(false)
@@ -214,81 +211,6 @@ watch(filterText, (val) => {
treeRef.value?.filter(val)
})
// 图标映射
const iconMap = {
'menu': Menu,
'grid': Grid,
'folder': Folder,
'tickets': Tickets,
'document': Document,
'setting': Setting,
'user': User,
'goods': Goods,
'chat-dot-square': ChatDotSquare,
'histogram': Histogram,
'wallet': Wallet,
'office-building': OfficeBuilding,
'postcard': Postcard,
'collection': Collection,
'video-play': VideoPlay,
'camera': Camera,
'headset': Headset,
'phone': Phone,
'message': Message,
'chat-line-square': ChatLineSquare,
'chat-round': ChatRound,
'guide': Guide,
'help': Help,
'info-filled': InfoFilled,
'circle-check': CircleCheck,
'circle-close': CircleClose,
'warning': Warning,
'question-filled': QuestionFilled,
'star': Star,
'link': Link,
'position': Position,
'picture': Picture,
'upload': Upload,
'download': Download,
'caret-left': CaretLeft,
'caret-right': CaretRight,
'more': More,
'close': Close,
'check': Check,
'arrow-up': ArrowUp,
'arrow-down': ArrowDown,
'arrow-left': ArrowLeft,
'arrow-right': ArrowRight,
'plus': Plus,
'minus': Minus,
'zoom-in': ZoomIn,
'zoom-out': ZoomOut,
'refresh': Refresh,
'search': Search,
'edit': Edit,
'delete': Delete,
'share': Share,
'view': View,
'switch-button': SwitchButton,
'hide': Hide,
'finished': Finished,
'circle-plus': CirclePlus,
'remove': Remove,
'circle-check-filled': CircleCheckFilled,
'circle-close-filled': CircleCloseFilled,
'warning-filled': WarningFilled,
'info-filled-icon': InfoFilledIcon,
'success-filled': SuccessFilled,
'question-filled-icon': QuestionFilledIcon
}
// 获取图标组件
const getIconComponent = (iconName) => {
if (!iconName) return Document
// 移除前缀,如 fa-, el-icon-
const cleanIconName = iconName.replace(/^(fa-|el-icon-)/, '').toLowerCase()
return iconMap[cleanIconName] || Document
}
// 获取图标颜色
const getIconColor = (data) => {
@@ -302,6 +224,24 @@ const getIconColor = (data) => {
const loadMenuData = async () => {
loading.value = true
try {
// 尝试从本地缓存获取菜单数据
const cachedMenuData = localStorage.getItem('menuTreeCache');
const cacheTimestamp = localStorage.getItem('menuTreeCacheTimestamp');
// 检查缓存是否有效24小时内
if (cachedMenuData && cacheTimestamp) {
const cacheAge = Date.now() - parseInt(cacheTimestamp);
if (cacheAge < 24 * 60 * 60 * 1000) { // 24小时
menuTree.value = JSON.parse(cachedMenuData);
// 展开所有节点
expandedKeys.value = getAllNodeIds(menuTree.value);
// 获取已保存的配置
await loadSavedConfig();
loading.value = false;
return;
}
}
const response = await listMenu({})
if (response.code === 200) {
// 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项
@@ -311,6 +251,10 @@ const loadMenuData = async () => {
// 展开所有节点
expandedKeys.value = getAllNodeIds(filteredMenus)
// 将菜单数据缓存到本地存储
localStorage.setItem('menuTreeCache', JSON.stringify(filteredMenus));
localStorage.setItem('menuTreeCacheTimestamp', Date.now().toString());
// 获取已保存的配置
await loadSavedConfig()
} else {
@@ -389,7 +333,7 @@ const saveConfig = async () => {
fullPath: fullPath,
menuName: node.menuName,
path: node.path,
icon: node.icon, // 保存图标信息
icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息
};
@@ -406,7 +350,7 @@ const saveConfig = async () => {
fullPath: node.path,
menuName: node.menuName,
path: node.path,
icon: node.icon, // 保存图标信息
icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息
};
console.log(`构造的菜单项对象(错误处理):`, menuItem);
@@ -435,6 +379,9 @@ const saveConfig = async () => {
if (saveResult.code === 200) {
// 只有在数据库保存成功后,才保存到本地存储
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
// 清除菜单树缓存,以便下次加载最新数据
localStorage.removeItem('menuTreeCache');
localStorage.removeItem('menuTreeCacheTimestamp');
ElMessage.success('配置保存成功')
// 触发全局事件,通知首页更新快捷功能
window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
@@ -493,6 +440,36 @@ const filterNode = (value, data) => {
// 加载已保存的配置
const loadSavedConfig = async () => {
try {
// 尝试从本地缓存获取配置
const cachedConfig = localStorage.getItem('homeFeaturesConfigCache');
const cacheTimestamp = localStorage.getItem('homeFeaturesConfigCacheTimestamp');
// 检查缓存是否有效1小时内
if (cachedConfig && cacheTimestamp) {
const cacheAge = Date.now() - parseInt(cacheTimestamp);
if (cacheAge < 60 * 60 * 1000) { // 1小时
const parsedConfig = JSON.parse(cachedConfig);
// 检查数据格式如果是包含对象的数组新格式提取菜单ID
if (parsedConfig && Array.isArray(parsedConfig)) {
if (parsedConfig.length > 0 && typeof parsedConfig[0] === 'object' && parsedConfig[0].hasOwnProperty('menuId')) {
// 新格式:[{menuId: 1, fullPath: "...", ...}, ...]
checkedKeys.value = parsedConfig.map(item => item.menuId);
} else {
// 旧格式:[1, 2, 3, ...]
checkedKeys.value = parsedConfig;
}
// 根据保存的配置初始化已选择的功能
const allNodes = getAllNodes(menuTree.value)
const checkedNodes = allNodes.filter(node =>
checkedKeys.value.includes(node.menuId) && node.menuType === 'C'
)
selectedFunctions.value = checkedNodes
}
return; // 使用缓存数据,直接返回
}
}
// 优先从数据库获取已保存的配置
const response = await getCurrentUserConfig('homeFeaturesConfig')
let savedConfig = null;
@@ -539,6 +516,10 @@ const loadSavedConfig = async () => {
checkedKeys.value.includes(node.menuId) && node.menuType === 'C'
)
selectedFunctions.value = checkedNodes
// 将配置缓存到本地存储
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(parsedConfig));
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
} else {
// 如果解析失败,使用默认配置
const defaultSelections = getDefaultSelections(menuTree.value)