Vue之动态菜单

简单易用的Vue动态菜单,采用了iview插件库,只做了菜单的展示,其他的功能都很简单,读者可以自己尝试。

1、main.js加入如下依赖

import Vuefrom 'vue'

import Appfrom './App'

import ViewUI from 'view-design'

import 'view-design/dist/styles/iview.css'

import {Message}from 'element-ui'

import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ViewUI);

Vue.component(Message);

Vue.prototype.$message =Message;

2、在菜单页加上如下代码

<template>
  <Menu ref="asideMenu" :active-name="activeMenuName" theme="dark" width="auto"
        accordion
        @on-select="goPage">
    <div v-for="(menu1) in menuList">
      <MenuItem v-if="menu1.children==null" v-bind:name="menu1.name">
          <Icon v-bind:type="menu1.icon" />
          {{menu1.title}}
      </MenuItem>
      <Submenu v-if="menu1.children!=null" v-bind:name="menu1.name">
        <template slot="title">
          <Icon v-bind:type="menu1.icon"/>
          {{menu1.title}}
        </template>
        <div v-for="(menu2) in menu1.children">
          <MenuItem v-bind:name="menu2.name">{{menu2.title}}</MenuItem>
        </div>
      </Submenu>
    </div>
  </Menu>
</template>
<script>
    export default {
        data() {
            return {
                menuList: [{"name":"首页","title":"首页","icon":"ios-home","path":"/home"},{"name":"用户管理","title":"用户管理","icon":"ios-people","path":"/user","children":[{"name":"客户管理","title":"客户管理","icon":null,"path":"/customer","children":null},{"name":"系统用户管理","title":"系统用户管理","icon":null,"path":"/admin","children":null}]}],
                activeMenuName: '首页',
            }
        },
        methods: {
            goPage(key) {
                this.$message.info(key);
            }
        }
    }
</script>

3、运行效果
演示.gif

<think>我们被要求实现Vue3中的动态菜单功能。动态菜单通常意味着菜单项不是写死在代码中,而是根据用户权限、角色或从服务器获取的数据动态生成的。结合之前的引用[1]和引用[2],我们可以看出实现动态菜单主要涉及以下几个步骤: 1. **用户登录与权限获取**:用户登录后,获取token,然后根据token获取用户的权限信息(包括可访问的菜单)。 2. **菜单数据获取**:通常从后端API获取菜单数据,这些数据可能是树形结构,包含路由信息、组件路径等。 3. **动态路由添加**:将菜单数据转换成路由配置,并使用Vue Router的`addRoute`方法动态添加路由。 4. **菜单渲染**:根据菜单数据在侧边栏或导航栏中渲染菜单项。 下面我们一步步详细说明如何实现。 ### 1. 定义路由和菜单数据结构 首先,我们需要定义菜单项的数据结构。通常,一个菜单项可能包含以下字段: - `path`: 路由路径 - `name`: 路由名称(唯一) - `component`: 对应的组件(动态导入) - `meta`: 元信息,例如菜单标题、图标、是否隐藏等 - `children`: 子菜单项 例如,从引用[1]中我们可以看到,他们使用一个对象`compMoudles`来映射菜单代码(menuCode)到动态导入的组件。因此,我们可以这样设计菜单数据: ```typescript // 假设从后端获取的菜单数据格式如下 interface MenuItem { id: number; parentId: number; name: string; path: string; component?: string; // 组件路径,对应前端映射的组件 redirect?: string; meta: { title: string; icon?: string; hidden?: boolean; }; children?: MenuItem[]; } ``` ### 2. 动态导入组件的映射 在引用[1]中,他们定义了一个`compMoudles`对象,将菜单代码(如`kweichowMt`)映射到动态导入的组件函数。我们可以采用类似的方式: ```typescript // 在router目录下创建componentMap.ts export const compMoudles = { kweichowMt: () => import('../views/personCenter/myCollection.vue'), vueBlog: () => import('../views/article/index.vue'), addBlog: () => import('../views/article/addBlog.vue'), blogList: () => import('../views/article/blogList.vue') }; ``` 或者,我们可以使用更通用的方式,根据菜单项的`component`字段(字符串)来动态导入对应的组件。但是注意,动态导入的路径必须是明确的字符串,不能直接使用变量(因为Webpack在构建时无法确定变量值)。因此,我们通常使用映射对象的方式。 ### 3. 将菜单数据转换为路由配置 我们需要将后端返回的菜单数据转换成Vue Router的路由配置。转换函数示例: ```typescript import { RouteRecordRaw } from 'vue-router'; import { compMoudles } from './componentMap'; // 将菜单数据转换为路由配置 export function transformMenuToRoutes(menuList: MenuItem[]): RouteRecordRaw[] { const routes: RouteRecordRaw[] = []; menuList.forEach(menu => { const route: RouteRecordRaw = { path: menu.path, name: menu.name, meta: menu.meta, // 如果有redirect,则设置redirect redirect: menu.redirect, // 如果有component,则从映射中获取组件 component: compMoudles[menu.component as keyof typeof compMoudles], children: menu.children ? transformMenuToRoutes(menu.children) : [] }; routes.push(route); }); return routes; } ``` 注意:这里假设每个菜单项的`component`字段都是`compMoudles`中的一个有效键。如果菜单项没有组件(比如目录),则可以不设置`component`,但通常需要设置一个布局组件(如Layout)或者使用`redirect`。 ### 4. 动态添加路由 在用户登录后,我们获取菜单数据,然后转换成路由配置,并动态添加到路由实例中。通常在全局路由守卫中进行,或者在初始化应用时进行(如引用[2]中的`init`函数)。 ```typescript // 初始化函数,在应用启动时调用 import { useMeanStore } from '@/store/routes/menus' import { useUserStore } from '@/store/routes/user' export default async function init() { const token = localStorage.getItem('token'); if (!token) { return Promise.resolve(); } // 获取用户信息和菜单 const userStore = useUserStore(); const menuStore = useMeanStore(); try { // 获取用户权限信息(如果需要) await userStore.getAuthorizationsList(); // 获取菜单数据 await menuStore.getMenu(); // 将菜单数据转换为路由 const routes = transformMenuToRoutes(menuStore.menuList); // 动态添加路由 routes.forEach(route => { router.addRoute(route); // 注意:如果路由有嵌套,需要处理父路由和子路由的关系 }); } catch (error) { console.error('初始化失败', error); } } ``` 注意:在Vue Router 4中,使用`addRoute`添加路由。如果路由有嵌套,需要在转换时处理好父子关系(上面的递归转换已经处理了)。 ### 5. 渲染动态菜单 在侧边栏组件中,我们使用菜单数据(存储在Pinia store中)来渲染菜单。可以使用递归组件来渲染多级菜单。 #### 使用Pinia存储菜单状态 首先,定义一个Pinia store来存储菜单数据: ```typescript // store/menuStore.ts import { defineStore } from 'pinia'; export const useMenuStore = defineStore('menu', { state: () => ({ menuList: [] as MenuItem[], // 菜单数据 }), actions: { // 设置菜单数据 setMenuList(list: MenuItem[]) { this.menuList = list; }, // 从后端获取菜单数据 async getMenu() { const res = await api.getMenu(); // 调用API this.setMenuList(res.data); } } }); ``` #### 递归菜单组件 在侧边栏组件中,遍历菜单数据,递归渲染子菜单。 ```vue <template> <div class="sidebar"> <template v-for="item in menuList" :key="item.id"> <!-- 如果有子菜单,使用el-submenu(以Element Plus为例) --> <el-submenu v-if="item.children && item.children.length" :index="item.path"> <template #title> <i :class="item.meta.icon"></i> <span>{{ item.meta.title }}</span> </template> <!-- 递归调用自身组件 --> <menu-item :menu-list="item.children"></menu-item> </el-submenu> <!-- 没有子菜单,直接显示菜单项 --> <el-menu-item v-else :index="item.path"> <i :class="item.meta.icon"></i> <template #title>{{ item.meta.title }}</template> </el-menu-item> </template> </div> </template> <script lang="ts"> import { defineComponent, PropType } from 'vue'; export default defineComponent({ name: 'MenuItem', props: { menuList: { type: Array as PropType<MenuItem[]>, required: true } } }); </script> ``` 注意:这里使用了Element Plus的菜单组件作为示例,实际可以根据项目使用的UI库调整。 ### 6. 权限控制 在动态菜单中,权限控制通常由后端返回的菜单数据决定。如果用户没有某个菜单的权限,后端就不会返回该菜单项,因此前端也就不会渲染和添加路由。同时,我们还需要在路由守卫中检查用户是否有权限访问某个路由。 ```typescript // 路由守卫 router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('token'); if (token) { if (!userStore.userInfo) { // 如果没有用户信息,则调用初始化 await init(); // 动态路由添加后,需要重新匹配路由 return next(to.fullPath); } // 检查用户是否有权限访问该路由 if (to.matched.length === 0) { next('/404'); // 跳转到404页面 } else { next(); } } else { if (to.path === '/login') { next(); } else { next('/login'); } } }); ``` ### 总结 在Vue3中实现动态菜单,主要步骤包括: 1. 用户登录后获取菜单数据(通常为树形结构)。 2. 将菜单数据转换为路由配置,并使用`addRoute`动态添加路由。 3. 使用Pinia存储菜单状态,并在侧边栏组件中递归渲染菜单。 4. 在路由守卫中处理权限和初始化。 通过以上步骤,我们可以实现一个灵活的动态菜单系统,根据用户权限展示不同的菜单项。 ### 相关问题 1. 如何实现Vue3动态路由的按需加载? 2. 在动态菜单中,如何实现菜单项的权限控制? 3. 如何处理动态添加路由后,页面刷新时路由丢失的问题? 4. Vue3中如何优化动态菜单的渲染性能?</think># Vue3 动态菜单实现指南 在Vue3中实现动态菜单是构建权限管理系统的核心功能,下面我将详细介绍完整的实现方案,包含路由配置、状态管理和菜单渲染。 ## 核心实现步骤 ### 1. 定义动态路由组件映射 在`router/componentMap.ts`中创建组件映射对象: ```typescript // src/router/componentMap.ts export const compModules = { dashboard: () => import('@/views/Dashboard.vue'), userManagement: () => import('@/views/UserManagement.vue'), roleManagement: () => import('@/views/RoleManagement.vue'), articleList: () => import('@/views/article/ArticleList.vue'), articleEdit: () => import('@/views/article/ArticleEdit.vue'), settings: () => import('@/views/Settings.vue'), // 添加更多组件映射... }; ``` ### 2. 创建路由实例 在`router/index.ts`中配置基础路由和动态路由添加逻辑: ```typescript // src/router/index.ts import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { compModules } from './componentMap'; // 静态路由(所有用户可见) const constantRoutes: RouteRecordRaw[] = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layouts/BasicLayout.vue'), redirect: '/dashboard', children: [] // 动态路由将添加到这里 }, { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') } ]; const router = createRouter({ history: createWebHistory(), routes: constantRoutes }); // 动态添加路由的函数 export function addDynamicRoutes(routes: RouteRecordRaw[]) { routes.forEach(route => { router.addRoute('BasicLayout', route); // 添加到BasicLayout的子路由 }); } export default router; ``` ### 3. 创建Pinia状态管理 创建菜单和用户状态存储: ```typescript // src/store/menuStore.ts import { defineStore } from 'pinia'; import type { MenuItem } from '@/types'; export const useMenuStore = defineStore('menu', { state: () => ({ menuList: [] as MenuItem[], // 菜单数据 isCollapse: false // 菜单折叠状态 }), actions: { // 设置菜单数据 setMenuList(menus: MenuItem[]) { this.menuList = menus; }, // 切换菜单折叠状态 toggleCollapse() { this.isCollapse = !this.isCollapse; } } }); ``` ```typescript // src/store/userStore.ts import { defineStore } from 'pinia'; import { loginApi, getUserInfoApi, getMenuListApi } from '@/api/user'; import type { UserInfo } from '@/types'; export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: {} as UserInfo }), actions: { // 用户登录 async login(credentials: { username: string; password: string }) { const res = await loginApi(credentials); this.token = res.token; localStorage.setItem('token', res.token); }, // 获取用户信息 async getUserInfo() { const res = await getUserInfoApi(); this.userInfo = res; }, // 获取菜单列表 async getMenuList() { const res = await getMenuListApi(); const menuStore = useMenuStore(); menuStore.setMenuList(res.menus); return res.menus; }, // 退出登录 logout() { this.token = ''; this.userInfo = {} as UserInfo; localStorage.removeItem('token'); } } }); ``` ### 4. 初始化应用和权限验证 创建初始化函数和应用入口: ```typescript // src/init.ts import router from '@/router'; import { addDynamicRoutes } from '@/router'; import { useUserStore } from '@/store/user'; import { useMenuStore } from '@/store/menu'; import { transformMenuToRoutes } from '@/utils/menuUtils'; export default async function initApp() { const userStore = useUserStore(); const menuStore = useMenuStore(); // 如果有token,初始化用户和菜单 if (userStore.token) { try { // 获取用户信息 await userStore.getUserInfo(); // 获取菜单数据 const menus = await userStore.getMenuList(); // 转换菜单为路由配置 const routes = transformMenuToRoutes(menus); // 动态添加路由 addDynamicRoutes(routes); // 将菜单数据存入Pinia menuStore.setMenuList(menus); } catch (error) { console.error('初始化失败:', error); userStore.logout(); router.replace('/login'); } } } ``` ```typescript // src/utils/menuUtils.ts import { compModules } from '@/router/componentMap'; import type { RouteRecordRaw, RouteMeta } from 'vue-router'; import type { MenuItem } from '@/types'; // 转换菜单数据为路由配置 export function transformMenuToRoutes(menus: MenuItem[]): RouteRecordRaw[] { return menus.map(menu => { const route: RouteRecordRaw = { path: menu.path, name: menu.name, meta: { title: menu.meta.title, icon: menu.meta.icon, requiresAuth: menu.meta.requiresAuth || true } as RouteMeta, component: compModules[menu.component as keyof typeof compModules], children: menu.children ? transformMenuToRoutes(menu.children) : [] }; return route; }); } ``` ### 5. 菜单组件实现 创建递归菜单组件: ```vue <!-- src/components/SidebarMenu.vue --> <template> <el-menu :default-active="activeMenu" :collapse="isCollapse" :unique-opened="true" router class="sidebar-menu" > <template v-for="item in menuList" :key="item.path"> <!-- 有子菜单的项 --> <el-sub-menu v-if="item.children && item.children.length" :index="item.path"> <template #title> <el-icon v-if="item.meta.icon"> <component :is="item.meta.icon" /> </el-icon> <span>{{ item.meta.title }}</span> </template> <sidebar-menu-item :menu-list="item.children" /> </el-sub-menu> <!-- 没有子菜单的项 --> <el-menu-item v-else :index="item.path"> <el-icon v-if="item.meta.icon"> <component :is="item.meta.icon" /> </el-icon> <template #title>{{ item.meta.title }}</template> </el-menu-item> </template> </el-menu> </template> <script setup lang="ts"> import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useMenuStore } from '@/store/menu'; const route = useRoute(); const menuStore = useMenuStore(); defineProps({ menuList: { type: Array as () => MenuItem[], required: true } }); const activeMenu = computed(() => route.path); const isCollapse = computed(() => menuStore.isCollapse); </script> ``` ### 6. 路由权限守卫 添加全局路由守卫进行权限验证: ```typescript // src/router/permission.ts import router from '@/router'; import { useUserStore } from '@/store/user'; import { ElMessage } from 'element-plus'; const whiteList = ['/login']; // 白名单路径 router.beforeEach(async (to, from, next) => { const userStore = useUserStore(); // 判断是否有token if (userStore.token) { if (to.path === '/login') { // 如果已登录,跳转到首页 next('/'); } else { // 检查用户信息是否已获取 if (!userStore.userInfo.id) { try { // 获取用户信息 await userStore.getUserInfo(); // 获取菜单数据 const menus = await userStore.getMenuList(); // 转换菜单为路由配置 const routes = transformMenuToRoutes(menus); // 动态添加路由 addDynamicRoutes(routes); // 确保添加路由后跳转 next({ ...to, replace: true }); } catch (error) { // 获取用户信息失败,重新登录 userStore.logout(); next(`/login?redirect=${to.path}`); } } else { // 已获取用户信息,直接放行 next(); } } } else { // 没有token if (whiteList.includes(to.path)) { // 在白名单中,直接放行 next(); } else { // 重定向到登录页 next(`/login?redirect=${to.path}`); } } }); ``` ## 关键特性说明 1. **动态路由加载**:根据用户权限动态加载路由配置 2. **菜单状态管理**:使用Pinia集中管理菜单状态 3. **递归菜单组件**:支持任意层级的嵌套菜单 4. **路由权限守卫**:全局前置守卫控制页面访问权限 5. **菜单折叠状态**:支持菜单折叠/展开功能 6. **类型安全**:使用TypeScript确保类型安全 ## 使用示例 在应用入口初始化: ```typescript // src/main.ts import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import { createPinia } from 'pinia'; import initApp from './init'; const app = createApp(App); const pinia = createPinia(); app.use(pinia); app.use(router); // 初始化应用 initApp().then(() => { app.mount('#app'); }); ``` 在布局组件中使用菜单组件: ```vue <!-- src/layouts/BasicLayout.vue --> <template> <div class="layout-container"> <el-container> <el-aside :width="isCollapse ? '64px' : '200px'"> <div class="logo">管理系统</div> <sidebar-menu :menu-list="menuStore.menuList" /> </el-aside> <el-container> <el-header> <div class="header-left"> <el-icon @click="menuStore.toggleCollapse"> <component :is="menuStore.isCollapse ? 'Expand' : 'Fold'" /> </el-icon> </div> <div class="header-right"> <!-- 用户信息等 --> </div> </el-header> <el-main> <router-view /> </el-main> </el-container> </el-container> </div> </template> <script setup lang="ts"> import { useMenuStore } from '@/store/menu'; const menuStore = useMenuStore(); </script> ``` ## 相关优化建议 1. **菜单缓存**:使用localStorage或sessionStorage缓存菜单数据,减少API请求 2. **按钮级权限**:在菜单权限基础上扩展按钮级权限控制 3. **菜单搜索**:为大型菜单系统添加搜索功能 4. **面包屑导航**:基于当前路由生成面包屑导航 5. **多级菜单优化**:对超过三级的菜单进行优化处理 ## 总结 Vue3中实现动态菜单的关键在于路由的动态添加和菜单数据的灵活管理。通过Pinia状态管理、动态路由加载和递归菜单组件,我们可以构建出灵活、可扩展的动态菜单系统。这种实现方式不仅支持基于权限的动态菜单展示,还能适应各种复杂的菜单结构需求。 --- ### 相关问题 1. 如何实现Vue3中的动态路由权限控制? 2. Pinia和Vuex在状态管理上有哪些主要区别? 3. 如何优化大型菜单系统的渲染性能? 4. 在Vue3中如何处理路由缓存和页面刷新? 5. 如何实现按钮级别的权限控制?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值