<think>我们之前已经讨论过Vue2和ElementUI实现侧边导航栏与路由结合的基本方案。现在用户要求提供示例代码,并参考了站内引用[1]中提到的动态侧边导航栏实现,特别是涉及vuex和动态路由权限控制的部分。结合用户的需求和之前的回答,我们将提供更详细的实现方案,包括以下关键点:1.使用VueRouter的路由配置,并通过meta字段添加菜单信息2.创建可递归处理多级菜单的侧边栏组件(使用ElementUI的菜单组件)3.通过vuex管理菜单状态(如折叠状态)4.实现权限控制:根据用户权限动态生成可访问的路由菜单具体步骤:###1.路由配置(router/index.js)在路由配置中,我们使用meta字段来存储菜单的标题、图标和权限等信息。同时,我们将路由分为两种:常量路由(所有用户可见)和动态路由(根据权限动态添加)。```javascriptimportVuefrom'vue'importRouterfrom'vue-router'Vue.use(Router)//常量路由(所有用户可见)exportconstconstantRoutes=[{path:'/login',component:()=>import('@/views/login'),hidden:true//不在侧边栏显示},{path:'/',component:()=>import('@/layout/Layout'),redirect:'/dashboard',children:[{path:'dashboard',name:'Dashboard',component:()=>import('@/views/dashboard'),meta:{title:'控制台',icon:'dashboard',affix:true}}]}]//动态路由(需要根据权限动态添加)exportconstasyncRoutes=[{path:'/user',component:()=>import('@/layout/Layout'),redirect:'/user/list',name:'User',meta:{title:'用户管理',icon:'user',roles:['admin','editor']//允许访问的角色},children:[{path:'list',name:'UserList',component:()=>import('@/views/user/list'),meta:{title:'用户列表'}},{path:'detail/:id',name:'UserDetail',component:()=>import('@/views/user/detail'),hidden:true,//不在侧边栏显示meta:{title:'用户详情'}}]},//其他动态路由...]//创建路由实例constcreateRouter=()=>newRouter({mode:'history',routes:constantRoutes})constrouter=createRouter()//重置路由的函数(用于动态添加路由后刷新路由)exportfunctionresetRouter(){constnewRouter=createRouter()router.matcher=newRouter.matcher//重置路由匹配器}exportdefaultrouter```###2.侧边栏菜单组件(layout/components/Sidebar.vue)这个组件将根据路由生成菜单,并支持递归多级菜单。```vue<template><div><el-scrollbarwrap-class="scrollbar-wrapper"><el-menu:default-active="activeMenu":collapse="isCollapse":unique-opened="true"background-color="#304156"text-color="#bfcbd9"active-text-color="#409EFF"mode="vertical"router><sidebar-itemv-for="routeinpermission_routes":key="route.path":item="route":base-path="route.path"/></el-menu></el-scrollbar></div></template><script>import{mapGetters}from'vuex'importSidebarItemfrom'./SidebarItem'exportdefault{components:{SidebarItem},computed:{...mapGetters(['permission_routes','sidebar']),activeMenu(){constroute=this.$routeconst{meta,path}=route//如果设置了高亮的路径(如编辑页面需要高亮列表页),则返回该路径if(meta.activeMenu){returnmeta.activeMenu}returnpath},isCollapse(){return!this.sidebar.opened}}}</script>```###3.侧边栏菜单项组件(layout/components/SidebarItem.vue)这是一个递归组件,用于处理多级菜单。```vue<template><divv-if="!item.hidden"><!--只有一个子路由且该子路由没有children--><templatev-if="hasOneShowingChild(item.children,item)&&(!onlyOneChild.children||onlyOneChild.noShowingChildren)"><app-linkv-if="onlyOneChild.meta":to="resolvePath(onlyOneChild.path)"><el-menu-item:index="resolvePath(onlyOneChild.path)"><item:icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)":title="onlyOneChild.meta.title"/></el-menu-item></app-link></template><!--有多个子路由--><el-submenuv-else:index="resolvePath(item.path)"popper-append-to-body><templateslot="title"><itemv-if="item.meta":icon="item.meta.icon":title="item.meta.title"/></template><sidebar-itemv-for="childinitem.children":key="child.path":item="child":base-path="resolvePath(child.path)"/></el-submenu></div></template><script>importpathfrom'path'import{isExternal}from'@/utils/validate'importItemfrom'./Item'importAppLinkfrom'./Link'exportdefault{name:'SidebarItem',components:{Item,AppLink},props:{//路由对象item:{type:Object,required:true},//基础路径basePath:{type:String,default:''}},data(){//用于存储只有一个子路由的情况this.onlyOneChild=nullreturn{}},methods:{hasOneShowingChild(children=[],parent){constshowingChildren=children.filter(item=>{if(item.hidden){returnfalse}else{//临时设置(只有一个子路由时使用)this.onlyOneChild=itemreturntrue}})//当只有一个子路由时,显示该子路由if(showingChildren.length===1){returntrue}//没有子路由时,显示父路由if(showingChildren.length===0){this.onlyOneChild={...parent,path:'',noShowingChildren:true}returntrue}returnfalse},resolvePath(routePath){if(isExternal(routePath)){returnroutePath}returnpath.resolve(this.basePath,routePath)}}}</script>```###4.辅助组件(layout/components/Item.vue和Link.vue)-Item.vue:用于渲染菜单项的图标和标题```vue<template><div><iv-if="icon":class="icon"></i><spanv-if="title">{{title}}</span></div></template><script>exportdefault{name:'MenuItem',props:{icon:{type:String,default:''},title:{type:String,default:''}}}</script>```-Link.vue:处理外部链接和内部路由```vue<template><av-if="isExternal":href="to"target="_blank"rel="noopener"><slot></slot></a><router-linkv-else:to="to"><slot></slot></router-link></template><script>import{isExternalasexternal}from'@/utils/validate'exportdefault{name:'AppLink',props:{to:{type:String,required:true}},computed:{isExternal(){returnexternal(this.to)}}}</script>```###5.Vuex状态管理(store/modules/permission.js)用于存储根据权限过滤后的路由(即侧边栏菜单要显示的路由)```javascriptimport{asyncRoutes,constantRoutes}from'@/router'//判断用户是否有该路由的权限functionhasPermission(roles,route){if(route.meta&&route.meta.roles){returnroles.some(role=>route.meta.roles.includes(role))}else{returntrue}}//根据角色过滤路由exportfunctionfilterAsyncRoutes(routes,roles){constres=[]routes.forEach(route=>{consttmp={...route}if(hasPermission(roles,tmp)){if(tmp.children){tmp.children=filterAsyncRoutes(tmp.children,roles)}res.push(tmp)}})returnres}conststate={routes:[],//完整路由(常量路由+动态路由)addRoutes:[]//动态添加的路由}constmutations={SET_ROUTES:(state,routes)=>{state.addRoutes=routesstate.routes=constantRoutes.concat(routes)//将动态路由和常量路由合并}}constactions={generateRoutes({commit},roles){returnnewPromise(resolve=>{letaccessedRoutes//如果角色包含admin,则拥有所有权限if(roles.includes('admin')){accessedRoutes=asyncRoutes||[]}else{//根据角色过滤动态路由accessedRoutes=filterAsyncRoutes(asyncRoutes,roles)}//提交mutation,将过滤后的动态路由添加到vuexcommit('SET_ROUTES',accessedRoutes)resolve(accessedRoutes)})}}exportdefault{namespaced:true,state,mutations,actions}```###6.权限控制流程(在登录后获取用户角色并动态添加路由)在用户登录成功后,获取用户角色,然后根据角色生成可访问的路由,并动态添加到路由器中。```javascript//在登录成功的回调中importrouterfrom'@/router'importstorefrom'@/store'//获取用户角色(假设从后台获取到角色数组)constroles=['admin']//示例角色//生成路由store.dispatch('permission/generateRoutes',roles).then(accessedRoutes=>{//动态添加路由accessedRoutes.forEach(route=>{router.addRoute(route)})})```###7.布局组件(layout/Layout.vue)整合侧边栏和主内容区```vue<template><el-containerclass="app-wrapper"><!--侧边栏--><el-aside:width="sidebar.opened?'210px':'64px'"><sidebar/></el-aside><el-container><!--顶部导航栏--><el-header><navbar/></el-header><!--内容区域--><el-main><app-main/></el-main></el-container></el-container></template><script>import{Navbar,Sidebar,AppMain}from'./components'exportdefault{name:'Layout',components:{Navbar,Sidebar,AppMain},computed:{sidebar(){returnthis.$store.state.app.sidebar}}}</script>```###总结以上实现方案结合了Vue2、ElementUI和VueRouter,实现了动态侧边导航栏与路由的集成。关键点包括:-使用路由的meta字段存储菜单信息-递归组件处理多级菜单-通过vuex管理菜单状态和权限路由-动态路由加载这种方案可以灵活地根据用户权限动态生成菜单,并且支持多级嵌套菜单。§§相关问题§§1.Vue2中如何根据用户角色动态生成侧边栏菜单?2.在ElementUI中如何实现侧边栏菜单的折叠与展开?3.如何处理动态添加路由后刷新页面路由丢失的问题?4.如何实现侧边栏菜单的高亮(包括子菜单激活)?5.如何优化递归组件在大型菜单中的性能?</think>下面我将为你提供一个完整的Vue2 + ElementUI侧边导航栏与路由集成的实现方案,包含示例代码和最佳实践:
### 一、完整实现方案
```vue
<!-- src/layout/Layout.vue -->
<template>
<el-container style="height: 100vh;">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '200px'">
<div class="logo-container">
<h3 v-show="!isCollapse">管理系统</h3>
</div>
<Sidebar :routes="permissionRoutes" :isCollapse="isCollapse" />
</el-aside>
<!-- 主内容区 -->
<el-container>
<el-header>
<div class="header-left">
<el-button @click="toggleCollapse" icon="el-icon-s-fold" />
<Breadcrumb />
</div>
<UserDropdown />
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script>
import Sidebar from './components/Sidebar'
import Breadcrumb from './components/Breadcrumb'
import UserDropdown from './components/UserDropdown'
import { mapGetters } from 'vuex'
export default {
components: { Sidebar, Breadcrumb, UserDropdown },
data() {
return {
isCollapse: false
}
},
computed: {
...mapGetters(['permissionRoutes']) // 从Vuex获取权限过滤后的路由
},
methods: {
toggleCollapse() {
this.isCollapse = !this.isCollapse
// 保存折叠状态到本地存储
localStorage.setItem('sidebarCollapse', JSON.stringify(this.isCollapse))
}
},
mounted() {
// 从本地存储加载折叠状态
const savedState = localStorage.getItem('sidebarCollapse')
if (savedState) this.isCollapse = JSON.parse(savedState)
}
}
</script>
```
```vue
<!-- src/layout/components/Sidebar.vue -->
<template>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:collapse-transition="false"
router
unique-opened
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409eff"
>
<template v-for="route in routes">
<MenuItem
:key="route.path"
:item="route"
:base-path="route.path"
:is-collapse="isCollapse"
/>
</template>
</el-menu>
</template>
<script>
import MenuItem from './MenuItem'
export default {
components: { MenuItem },
props: {
routes: Array,
isCollapse: Boolean
},
computed: {
activeMenu() {
const route = this.$route
const { meta, path } = route
return meta.activeMenu || path
}
}
}
</script>
```
```vue
<!-- src/layout/components/MenuItem.vue -->
<template>
<div v-if="!item.hidden">
<!-- 无子路由 -->
<el-menu-item
v-if="!item.children || item.children.length === 0"
:index="resolvePath(basePath, item.path)"
>
<i :class="item.meta.icon"></i>
<span slot="title">{{ item.meta.title }}</span>
</el-menu-item>
<!-- 有子路由 -->
<el-submenu v-else :index="item.path">
<template slot="title">
<i :class="item.meta.icon"></i>
<span v-show="!isCollapse">{{ item.meta.title }}</span>
</template>
<MenuItem
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(basePath, item.path)"
:is-collapse="isCollapse"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
export default {
name: 'MenuItem',
props: {
item: {
type: Object,
required: true
},
basePath: {
type: String,
default: ''
},
isCollapse: Boolean
},
methods: {
resolvePath(basePath, routePath) {
return path.resolve(basePath, routePath)
}
}
}
</script>
```
### 二、路由配置示例
```javascript
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
redirect: '/dashboard',
hidden: true
},
{
path: '/dashboard',
component: () => import('@/layout'),
meta: { title: '控制台', icon: 'el-icon-s-data' }
}
]
export const asyncRoutes = [
{
path: '/user',
component: () => import('@/layout'),
redirect: '/user/list',
name: 'User',
meta: {
title: '用户管理',
icon: 'el-icon-user',
roles: ['admin'] // 权限控制
},
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/views/user/list'),
meta: { title: '用户列表' }
},
{
path: 'create',
name: 'UserCreate',
component: () => import('@/views/user/create'),
meta: { title: '创建用户' }
}
]
},
// 其他动态路由...
]
const createRouter = () => new Router({
mode: 'history',
routes: constantRoutes
})
const router = createRouter()
// 重置路由函数(用于动态添加路由)
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
```
### 三、权限控制实现
```javascript
// src/store/modules/permission.js
import { asyncRoutes, constantRoutes } from '@/router'
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
```
### 四、路由守卫配置
```javascript
// src/permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
// 白名单
const whiteList = ['/login']
router.beforeEach(async (to, from, next) => {
// 显示页面加载进度条
NProgress.start()
// 确定用户是否已登录
const hasToken = store.getters.token
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
// 检查用户是否已获取权限信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 根据角色生成可访问的路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加路由
router.addRoutes(accessRoutes)
// 使用replace确保导航成功
next({ ...to, replace: true })
} catch (error) {
// 移除token并跳转到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 用户未登录 */
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// 完成进度条
NProgress.done()
})
```
### 五、面包屑组件
```vue
<!-- src/layout/components/Breadcrumb.vue -->
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in levelList"
:key="item.path"
>
<span v-if="index === levelList.length - 1" class="no-redirect">
{{ item.meta.title }}
</span>
<router-link v-else :to="item.path">
{{ item.meta.title }}
</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
data() {
return {
levelList: []
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// 过滤掉没有标题的路由
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
this.levelList = matched
}
}
}
</script>
```
### 最佳实践总结
1. **路由驱动菜单**
菜单数据直接从路由配置生成,保证菜单与路由同步更新[^1]
2. **权限控制**
使用路由守卫 + Vuex 实现动态路由加载和菜单过滤
3. **递归组件**
使用递归组件处理无限层级菜单,支持任意深度嵌套
4. **状态持久化**
将侧边栏折叠状态保存到 localStorage
5. **性能优化**
- 路由懒加载:`component: () => import('@/views/dashboard')`
- 按需加载 ElementUI 组件
- 使用 keep-alive 缓存页面状态
6. **用户体验**
- 面包屑导航显示当前路径
- 菜单折叠时自动显示 Tooltip
- 路由切换时显示加载进度条
> 项目结构建议:
> ```
> src/
> ├── layout/ # 布局组件
> │ ├── Layout.vue # 主布局
> │ └── components/ # 布局子组件
> │ ├── Sidebar.vue
> │ ├── MenuItem.vue
> │ ├── Breadcrumb.vue
> │ └── ...
> ├── router/ # 路由配置
> │ └── index.js
> ├── store/ # Vuex 状态管理
> │ └── modules/
> │ ├── permission.js # 权限模块
> │ └── ...
> ├── views/ # 页面组件
> ├── permission.js # 路由守卫
> └── main.js # 入口文件
> ```