Vue3 + Element Plus 动态菜单实现:一套代码完美适配多角色权限系统
今天分享一个基于Vue3和Element Plus的动态菜单实现。这个方案很适用于需要权限管理的后台系统,能够根据用户角色权限显示不同的菜单项。
一、什么是动态菜单?为什么需要它?
在管理后台系统中,不同角色的用户通常需要不同的功能权限。比如:
- 管理员可以访问所有功能
- 编辑者只能管理内容
- 查看者只能浏览数据
如果为每个角色单独开发一套界面,显然效率低下。动态菜单就是解决这个问题的方案——一套代码,根据不同用户角色显示不同的菜单结构。
二、实现效果预览
我们先来看看最终实现的效果:
![]()
- 角色切换:右上角可以切换用户角色(管理员/编辑者/查看者)
- 菜单过滤:根据角色自动过滤无权限的菜单项
- 侧边栏折叠:支持展开/收起侧边栏
- 面包屑导航:显示当前页面位置
老样子,完整源码在文末获取哦~
三、核心实现原理
1. 菜单数据结构设计
合理的菜单数据结构是动态菜单的基础。我们的设计如下:
const menuData = ref([
{
id: 'dashboard', // 唯一标识
name: '仪表板', // 显示名称
icon: 'DataBoard', // 图标
route: '/dashboard', // 路由路径
roles: ['admin', 'editor', 'viewer'] // 可访问的角色
},
{
id: 'content',
name: '内容管理',
icon: 'Document',
roles: ['admin', 'editor'],
children: [ // 子菜单
{
id: 'articles',
name: '文章管理',
route: '/articles',
roles: ['admin', 'editor']
}
// ... 更多子菜单
]
}
// ... 更多菜单项
]);
这种结构的特点:
- 支持多级嵌套菜单
- 每个菜单项明确指定可访问的角色
- 图标使用 Element Plus 的图标组件
2. 菜单过滤逻辑
核心功能是根据当前用户角色过滤菜单:
const filteredMenu = computed(() => {
return menuData.value
.map(item => {
// 1. 检查主菜单权限
if (!item.roles.includes(currentUser.value.role)) {
return null; // 无权限,过滤掉
}
// 2. 深拷贝菜单项(避免修改原始数据)
const menuItem = { ...item };
// 3. 如果有子菜单,过滤子菜单
if (menuItem.children) {
menuItem.children = menuItem.children.filter(
child => child.roles.includes(currentUser.value.role)
);
// 如果子菜单全被过滤掉,主菜单也不显示
if (menuItem.children.length === 0) {
return null;
}
}
return menuItem;
})
.filter(Boolean); // 过滤掉null值
});
过滤过程详解:
- 映射(map):遍历每个菜单项,返回处理后的菜单项或null
- 权限检查:检查当前用户角色是否在菜单项的角色列表中
- 子菜单过滤:对有子菜单的项,递归过滤无权限的子项
- 空子菜单处理:如果所有子项都被过滤,父项也不显示
- 最终过滤:用filter(Boolean)移除所有null值
计算属性(computed)的优势:
- 自动响应依赖变化(当用户角色变化时自动重新计算)
- 缓存结果,避免重复计算
3. 用户角色管理
用户信息和角色切换的实现:
// 当前用户信息
const currentUser = ref({
name: '管理员',
role: 'admin',
avatar: 'https://example.com/avatar.png'
});
// 处理角色切换
const handleRoleChange = (role) => {
currentUser.value.role = role;
// 角色切换后更新当前激活的菜单
if (role === 'viewer') {
// 查看者只能访问仪表板
activeMenu.value = '/dashboard';
currentPageTitle.value = '仪表板';
} else {
// 其他角色显示第一个可访问的菜单
const firstMenu = findFirstAccessibleMenu();
if (firstMenu) {
activeMenu.value = firstMenu.route;
currentPageTitle.value = firstMenu.name;
}
}
};
四、界面布局与组件使用
1. 整体布局结构
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar" :class="{ collapsed: isCollapse }">
<!-- Logo区域 -->
<div class="logo-area">...</div>
<!-- 菜单区域 -->
<el-menu>...</el-menu>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">...</div>
<!-- 页面内容 -->
<div class="content">...</div>
<!-- 页脚 -->
<div class="footer">...</div>
</div>
</div>
这种布局是管理后台的经典设计,具有清晰的视觉层次。
2. Element Plus 菜单组件使用
<el-menu
:default-active="activeMenu" <!-- 当前激活的菜单 -->
class="el-menu-vertical"
background-color="#001529" <!-- 背景色 -->
text-color="#bfcbd9" <!-- 文字颜色 -->
active-text-color="#409EFF" <!-- 激活项文字颜色 -->
:collapse="isCollapse" <!-- 是否折叠 -->
:collapse-transition="false" <!-- 关闭折叠动画 -->
:unique-opened="true" <!-- 只保持一个子菜单展开 -->
>
<!-- 菜单项渲染 -->
<template v-for="item in filteredMenu" :key="item.id">
<!-- 有子菜单的情况 -->
<el-sub-menu v-if="item.children" :index="item.id">
<!-- 标题区域 -->
<template #title>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.name }}</span>
</template>
<!-- 子菜单项 -->
<el-menu-item v-for="child in item.children"
:key="child.id"
:index="child.route"
@click="selectMenu(child)">
{{ child.name }}
</el-menu-item>
</el-sub-menu>
<!-- 没有子菜单的情况 -->
<el-menu-item v-else :index="item.route" @click="selectMenu(item)">
...
</el-menu-item>
</template>
</el-menu>
关键点说明:
-
动态组件:
<component :is="item.icon">实现动态图标渲染 -
条件渲染:使用
v-if和v-else区分子菜单和单菜单项 -
循环渲染:
v-for遍历过滤后的菜单数据 -
唯一key:为每个菜单项设置唯一的
:key="item.id"提高性能
五、样式设计技巧
1. 侧边栏折叠动画
.sidebar {
width: 240px;
background-color: #001529;
transition: width 0.3s; /* 宽度变化动画 */
overflow: hidden;
}
.sidebar.collapsed {
width: 64px;
}
.logo-area .logo-text {
margin-left: 10px;
transition: opacity 0.3s; /* 文字淡入淡出 */
}
.sidebar.collapsed .logo-text {
opacity: 0; /* 折叠时隐藏文字 */
}
2. 布局技巧
.app-container {
display: flex;
min-height: 100vh; /* 全屏高度 */
}
.main-content {
flex: 1; /* 占据剩余空间 */
display: flex;
flex-direction: column;
overflow: hidden; /* 防止内容溢出 */
}
.content {
flex: 1; /* 内容区占据主要空间 */
padding: 20px;
overflow-y: auto; /* 内容过多时滚动 */
}
使用 Flex 布局可以轻松实现经典的侧边栏+主内容区布局。
六、实际应用扩展建议
在实际项目中,你还可以进一步扩展这个基础实现:
1. 与路由集成
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
// 菜单点击处理
const selectMenu = (item) => {
// 路由跳转
router.push(item.route);
};
// 根据当前路由设置激活菜单
watch(route, (newRoute) => {
activeMenu.value = newRoute.path;
// 根据路由查找对应的页面标题
currentPageTitle.value = findTitleByRoute(newRoute.path);
});
2. 后端动态菜单
在实际项目中,菜单数据通常来自后端:
// 从API获取菜单数据
const fetchMenuData = async () => {
try {
const response = await axios.get('/api/menus', {
params: { role: currentUser.value.role }
});
menuData.value = response.data;
} catch (error) {
console.error('获取菜单数据失败:', error);
}
};
3. 权限控制增强
除了菜单过滤,还可以添加更细粒度的权限控制:
// 权限指令
app.directive('permission', {
mounted(el, binding) {
const { value: requiredRoles } = binding;
const userRole = currentUser.value.role;
if (!requiredRoles.includes(userRole)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
// 在模板中使用
<button v-permission="['admin', 'editor']">编辑内容</button>
总结
通过这个 Vue 3 + Element Plus 的动态菜单实现,我们学到了:
- 设计合理的菜单数据结构是动态菜单的基础
- 使用计算属性实现菜单过滤,自动响应角色变化
- 利用 Element Plus 组件快速构建美观的界面
- Flex 布局技巧实现响应式侧边栏
- 扩展思路,如路由集成、后端动态菜单等
这个实现方案具有很好的可扩展性,你可以根据实际需求进行调整和增强。
完整源码GitHub地址:github.com/1344160559-…
你可以直接复制到HTML文件中运行体验。尝试切换不同的用户角色,观察菜单的变化,加深对动态菜单工作原理的理解。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+MySQL+Vue实现文件共享系统》