Vue3 + Element Plus 动态菜单实现:一套代码完美适配多角色权限系统

今天分享一个基于Vue3Element Plus的动态菜单实现。这个方案很适用于需要权限管理的后台系统,能够根据用户角色权限显示不同的菜单项。

一、什么是动态菜单?为什么需要它?

在管理后台系统中,不同角色的用户通常需要不同的功能权限。比如:

  • 管理员可以访问所有功能
  • 编辑者只能管理内容
  • 查看者只能浏览数据

如果为每个角色单独开发一套界面,显然效率低下。动态菜单就是解决这个问题的方案------一套代码,根据不同用户角色显示不同的菜单结构

二、实现效果预览

我们先来看看最终实现的效果:

  1. 角色切换:右上角可以切换用户角色(管理员/编辑者/查看者)
  2. 菜单过滤:根据角色自动过滤无权限的菜单项
  3. 侧边栏折叠:支持展开/收起侧边栏
  4. 面包屑导航:显示当前页面位置

老样子,完整源码在文末获取哦~

三、核心实现原理

1. 菜单数据结构设计

合理的菜单数据结构是动态菜单的基础。我们的设计如下:

javascript 复制代码
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. 菜单过滤逻辑

核心功能是根据当前用户角色过滤菜单:

javascript 复制代码
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值
});

过滤过程详解

  1. 映射(map):遍历每个菜单项,返回处理后的菜单项或null
  2. 权限检查:检查当前用户角色是否在菜单项的角色列表中
  3. 子菜单过滤:对有子菜单的项,递归过滤无权限的子项
  4. 空子菜单处理:如果所有子项都被过滤,父项也不显示
  5. 最终过滤:用filter(Boolean)移除所有null值

计算属性(computed)的优势

  • 自动响应依赖变化(当用户角色变化时自动重新计算)
  • 缓存结果,避免重复计算

3. 用户角色管理

用户信息和角色切换的实现:

javascript 复制代码
// 当前用户信息
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. 整体布局结构

html 复制代码
<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 菜单组件使用

html 复制代码
<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>

关键点说明

  1. 动态组件<component :is="item.icon"> 实现动态图标渲染
  2. 条件渲染 :使用 v-ifv-else 区分子菜单和单菜单项
  3. 循环渲染v-for 遍历过滤后的菜单数据
  4. 唯一key :为每个菜单项设置唯一的 :key="item.id" 提高性能

五、样式设计技巧

1. 侧边栏折叠动画

css 复制代码
.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. 布局技巧

css 复制代码
.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. 与路由集成

javascript 复制代码
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. 后端动态菜单

在实际项目中,菜单数据通常来自后端:

javascript 复制代码
// 从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. 权限控制增强

除了菜单过滤,还可以添加更细粒度的权限控制:

javascript 复制代码
// 权限指令
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 的动态菜单实现,我们学到了:

  1. 设计合理的菜单数据结构是动态菜单的基础
  2. 使用计算属性实现菜单过滤,自动响应角色变化
  3. 利用 Element Plus 组件快速构建美观的界面
  4. Flex 布局技巧实现响应式侧边栏
  5. 扩展思路,如路由集成、后端动态菜单等

这个实现方案具有很好的可扩展性,你可以根据实际需求进行调整和增强。

完整源码GitHub地址github.com/1344160559-...

你可以直接复制到HTML文件中运行体验。尝试切换不同的用户角色,观察菜单的变化,加深对动态菜单工作原理的理解。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot+MySQL+Vue实现文件共享系统》

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

《SpringBoot 动态菜单权限系统设计的企业级解决方案》

《Vue3和Vue2的核心区别?很多开发者都没完全搞懂的10个细节》

相关推荐
n***84071 小时前
Springboot-配置文件中敏感信息的加密:三种加密保护方法比较
android·前端·后端
姜太公钓鲸2331 小时前
Bootstrap是什么?作用是什么?使用场景是什么?如何使用?
前端·bootstrap·html
Aerelin1 小时前
爬虫playwright中的等待机制
前端·爬虫·python
慧慧吖@1 小时前
关于在本地去模拟生产环境检测页面内容注意事项
前端·javascript·vue.js
码农很忙2 小时前
用SpreadJS实现分权限管理:前端技术栈的精准控制实践
前端
黄团团2 小时前
Vue2整合Electron开发桌面级应用以及打包发布(提供Gitee源码)
前端·javascript·vue.js·elementui·electron
勇气要爆发2 小时前
问:LocalStorage、Vuex、Pinia的区别和本质
前端
Aerelin2 小时前
iframe讲解(爬虫playwright的特殊应用)
前端·爬虫·python·html
Drift_Dream2 小时前
IntersectionObserver:现代Web开发的交叉观察者
前端