实现动态菜单的递归生成及菜单的折叠

87 阅读4分钟

1. 问题:请介绍一下你们项目中动态菜单的递归生成机制是如何实现的?

答案: 我们项目中的动态菜单递归生成主要通过 RecursiveMenu.vue 组件实现:

<template>
    <div class="recursive-menu">
        <template v-for="item in menuItems">
            <!-- 有子菜单的情况 -->
            <el-sub-menu v-if="item.children && item.children.length > 0" 
                         :key="item._id" 
                         :index="item.path">
                <template #title>
                    <el-icon v-if="item.icon">
                        <component :is="item.icon" />
                    </el-icon>
                    <span>{{ item.title }}</span>
                </template>
                
                <!-- 递归渲染子菜单 -->
                <RecursiveMenu :menu-items="item.children" :level="level + 1" />
            </el-sub-menu>
            
            <!-- 没有子菜单的情况 -->
            <el-menu-item v-else :key="item._id" :index="item.path">
                <el-icon v-if="item.icon">
                    <component :is="item.icon" />
                </el-icon>
                <span>{{ item.title }}</span>
            </el-menu-item>
        </template>
    </div>
</template>

核心机制:

  1. 递归条件判断:通过 item.children && item.children.length > 0 判断是否有子菜单
  2. 组件自调用:在子菜单中使用 <RecursiveMenu :menu-items="item.children" :level="level + 1" />
  3. 层级传递:通过 level 参数记录当前菜单层级,便于样式控制

2. 问题:菜单数据是如何从后端获取并管理的?

答案: 我们项目使用 Pinia 状态管理菜单数据,具体流程如下:

// store/modules/menu.ts
export const useMenuStore = defineStore('menu', {
  state: () => ({
    menuTree: [] as MenuItem[],
    menuList: [] as MenuItem[],
    loading: false,
    error: ''
  }),

  actions: {
    async fetchMenuTree() {
      this.loading = true;
      try {
        const response = await menuApi.getMenuTree();
        if (Array.isArray(response.data) && response.data.length === 0) {
          // 如果菜单为空,自动初始化
          const initRes = await menuApi.initDefaultMenus();
          if (initRes.code === 0) {
            const retryRes = await menuApi.getMenuTree();
            this.menuTree = retryRes.data;
          }
        } else {
          this.menuTree = response.data;
        }
      } catch (error) {
        this.error = error.message || '获取菜单失败';
      } finally {
        this.loading = false;
      }
    }
  }
});

SideMenu.vue 中使用:

onMounted(async () => {
    if (menuStore.getMenuTree.length === 0) {
        await menuStore.fetchMenuTree()
    }
})

3. 问题:菜单的权限控制是如何实现的?

答案: 我们项目的菜单权限控制通过以下机制实现:

// SideMenu.vue 中的权限过滤
const filteredMenuItems = computed(() => {
    const userRole = useTool.userInfo.role
    return menuStore.getMenuTree.filter((menu) => {
        if (menu.requireAdmin && userRole !== 1) {
            return false
        }
        return true
    })
})

权限控制特点:

  1. 角色判断:通过 userRole 判断用户角色(1为管理员,其他为普通用户)
  2. 菜单标记:菜单项通过 requireAdmin 字段标记是否需要管理员权限
  3. 视觉区分:管理员专用菜单有特殊样式标识
.admin-only {
    &.el-menu-item {
        background-color: rgba(255, 193, 7, 0.1);
        &:hover {
            background-color: rgba(255, 193, 7, 0.2) !important;
        }
    }
}

4. 问题:菜单折叠功能是如何实现的?

答案: 我们项目的菜单折叠功能通过以下机制实现:

// store/index.ts 中的折叠状态管理
export const useToolStore = defineStore('tool', {
  state: () => ({
    isCollapsed: false,
    // ...其他状态
  }),
  actions: {
    changeCollapsed() {
      this.isCollapsed = !this.isCollapsed;
    }
  },
  persist: {
    key: 'tool',
    storage: localStorage,
    paths: ['isCollapsed', 'userInfo'],
  },
});

SideMenu.vue 中应用折叠状态:

<el-aside :width="useTool.isCollapsed ? '64px' : '220px'">
    <el-menu :collapse="useTool.isCollapsed" 
             :collapse-transition="false">
        <RecursiveMenu :menu-items="filteredMenuItems" />
    </el-menu>
</el-aside>

TopHeader.vue 中触发折叠:

const handleCollapsed = () => {
    useTool.changeCollapsed()
}

5. 问题:菜单的响应式设计和移动端适配是如何处理的?

答案: 我们项目的菜单响应式设计通过以下方式实现:

// MainBox.vue 中的移动端处理
const handleResize = () => {
    isMobile.value = window.innerWidth <= 768
    // 在移动端自动收起侧边栏
    if (isMobile.value && !useTool.isCollapsed) {
        useTool.changeCollapsed()
    }
}

CSS 响应式样式:

// 移动端样式
@media screen and (max-width: 768px) {
    :deep(.el-aside) {
        position: fixed;
        height: 100vh;
        z-index: 1000;
    }
}

// 标题响应式
.title {
    font-size: 16px;
    &.mobile-title {
        font-size: 14px;
        max-width: 180px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

6. 问题:菜单的加载状态和错误处理是如何实现的?

答案: 我们项目的菜单加载状态和错误处理机制:

<!-- SideMenu.vue 中的加载状态 -->
<el-menu>
    <!-- 加载状态 -->
    <div v-if="menuStore.isLoading" class="menu-loading">
        <el-skeleton :rows="5" animated />
    </div>
    
    <!-- 动态菜单 -->
    <RecursiveMenu v-else :menu-items="filteredMenuItems" />
</el-menu>

状态管理中的错误处理:

// store/modules/menu.ts
async fetchMenuTree() {
  this.loading = true;
  this.error = '';
  try {
    const response = await menuApi.getMenuTree();
    // 处理响应...
  } catch (error: any) {
    this.error = error.message || '获取菜单失败';
    console.error('获取菜单树失败:', error);
  } finally {
    this.loading = false;
  }
}

7. 问题:菜单数据的持久化是如何实现的?

答案: 我们项目使用 Pinia 的持久化插件实现菜单数据持久化:

// store/modules/menu.ts
export const useMenuStore = defineStore('menu', {
  // ... state 和 actions
  
  persist: {
    key: 'menu',
    storage: localStorage,
    paths: ['menuTree'] // 只持久化菜单树数据
  }
});

// store/index.ts 中的折叠状态持久化
export const useToolStore = defineStore('tool', {
  // ... state 和 actions
  
  persist: {
    key: 'tool',
    storage: localStorage,
    paths: ['isCollapsed', 'userInfo'], // 持久化折叠状态和用户信息
  },
});

持久化配置:

// store/index.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const store = createPinia();
store.use(piniaPluginPersistedstate);

8. 问题:菜单的图标系统是如何设计的?

答案: 我们项目的菜单图标系统通过动态组件实现:

<!-- RecursiveMenu.vue 中的图标渲染 -->
<template #title>
    <el-icon v-if="item.icon">
        <component :is="item.icon" />
    </el-icon>
    <span>{{ item.title }}</span>
</template>

图标数据结构:

// types/menu.ts
export interface MenuItem {
  _id: string;
  name: string;
  path: string;
  component: string;
  icon: string; // 图标名称,对应 Element Plus 图标
  title: string;
  // ...其他字段
}

图标样式优化:

.el-icon {
    font-size: 18px;
    margin-right: 5px;
    vertical-align: middle;
}

9. 问题:菜单的路由集成和激活状态是如何处理的?

答案: 我们项目的菜单路由集成通过以下方式实现:

<!-- SideMenu.vue 中的路由集成 -->
<el-menu :router="true" 
         :default-active="route.fullPath">
    <RecursiveMenu :menu-items="filteredMenuItems" />
</el-menu>

路由激活状态处理:

import { useRoute } from 'vue-router'
const route = useRoute()

// 菜单项的路由配置
<el-menu-item :index="item.path"> // index 对应路由路径

菜单项的路由结构:

// 菜单数据结构中的路由信息
{
  _id: "1",
  name: "news-manage",
  path: "/news-manage", // 对应路由路径
  component: "NewsManage", // 对应路由组件
  title: "新闻管理",
  // ...
}

10. 问题:菜单的性能优化和缓存策略有哪些?

答案: 我们项目的菜单性能优化策略:

  1. 计算属性缓存
// 使用 computed 缓存过滤后的菜单数据
const filteredMenuItems = computed(() => {
    const userRole = useTool.userInfo.role
    return menuStore.getMenuTree.filter((menu) => {
        if (menu.requireAdmin && userRole !== 1) {
            return false
        }
        return true
    })
})
  1. 条件渲染优化
<!-- 只在非加载状态时渲染菜单 -->
<RecursiveMenu v-else :menu-items="filteredMenuItems" />
  1. 数据持久化
persist: {
  key: 'menu',
  storage: localStorage,
  paths: ['menuTree'] // 避免重复请求
}
  1. 懒加载策略
onMounted(async () => {
    if (menuStore.getMenuTree.length === 0) {
        await menuStore.fetchMenuTree() // 只在需要时加载
    }
})
  1. 组件复用
<!-- 使用 key 确保组件正确复用 -->
<el-sub-menu :key="item._id" :index="item.path">