Vue-element-admin实现菜单根据用户权限动态加载
之前有一些网友对我那个 IT 部门信息管理系统(http://caijt.com/it)的前端感兴趣,我已经开源到 github(https://github.com/Caijt/itsys-ui)
上面有两个分支,master 是对应 php 后端的,itsys-net 是对应 asp.net core 后端的。
这里我简单介绍下我这个系统前端代码,当时我几乎完全参考 vue-element-admin 的,不过没用它的代码,但写法几乎都参考了他的教程,不过有一点不同的是,我的路由跟菜单是动态生成的,是后台根据当前登录的用户,查询用户的角色,再查询角色所具有的菜单列表,返回到前端,然后在前端生成 Routers 树数据,再用 router.addRouters 方法挂载到 router 上,当然 vue-element-admin 的作者也有考虑到这个问题,看下图。
那我来介绍下我的系统是怎么实现这种动态权限的需求的,当然我的代码不完全跟 vue-element-admin 一样,只是提供一种思路。
先介绍下我数据表(sys_menu)的结构,如下图所示,很明显,我的数据表(sys_menu)是一个树型结构,主要的字段有 id,title(决定你前端菜单的标题),path(很重要,前端会根据这个值去寻找 views 下面的 vue 文件,所以前端创建的文件夹名称必须跟这个值一致),parent_id(父级菜单 id),order(菜单的排序顺序),还有一个 parent_ids 字段, 这个只是我在其它查询中的一个辅助字段,在这里没什么作用。
后端获取菜单数据,以下 sql 不涉及角色菜单的判断
select id,title,path,parent_id from sys_menu order by `order`
前端得到这样的对象数组,如下图所示
let menuList = res.data.menuList //这是后端的菜单数据 let menuRouters = [] //定义一个空数组,这个是用来装真正路由数据的//下面就要根据后端的菜单数据组装树型路由数据
//先取出根节点,没有父 id 的就是根节点
menuList.forEach((m, i) => {
if (m.parent_id == null) {
m.fullPath = '/' + m.path
let module = {
path: '/' + m.path,
component: layout,
meta: { id: m.id, title: m.title, fullPath: '/' + m.path },
children: [
{
path: '',
component:() => import('@/views/' + m.path + '/index')
meta: {
menuHide: true,
title: m.title
}
}
]
}
menuRouters.push(module)
}
})
//定义一个递归方法
function convertTree(routers) {
routers.forEach(r => {
menuList.forEach((m, i) => {
if (m.parent_id && m.parent_id == r.meta.id) {
if (!r.children) r.children = []
m.fullPath = r.meta.fullPath + '/' + m.path
let menu = {
path: m.path,
component: () => import('@/views'+r.meta.fullPath+'/'+m.path),
meta: { id: m.id, title: m.title, fullPath: r.meta.fullPath + '/' + m.path }
}
r.children.push(menu)
}
})
if (r.children) convertTree(r.children)
})
}
convertTree(menuRouters) //用递归填充
router.addRoutes(menuRouters) //挂载到 router
路由挂载好后,我同时把 menuRouters 赋值给 vuex($store.state.user.routers),方便我在 menu 组件中调用
还有我的系统跟 vue-element-admin 菜单显示也有所不同,我是按模块显示菜单的,如下图所示,vue-element-admin 是菜单统一显示左边栏上
那么处理起来很容易,在我的系统里,根节点就是模块,根节点的子菜单,就是该模块下的菜单列表
顶部模块跟左侧菜单组件都是用 element-ui 的 NavMenu 组件,只是模块的是用一个horizontal 水平的,菜单是 vertical 垂直的
// 以下是顶部模块菜单的代码
<el-menu class="_layout-header" router mode="horizontal" :default-active="modulePath" background-color="#304156" text-color="#fff" active-text-color="#409EFF" style="border:none" ref="elHeader" > <el-menu-item v-for="m in $store.state.user.routers.slice(0,maxShowHeaderMenu)" :index="m.path" :key="m.meta.id" >{{m.meta.title}}</el-menu-item> <el-submenu index="/more" v-if="$store.state.user.routers.length>maxShowHeaderMenu"> <template slot="title">更多</template> <el-menu-item v-for="m in $store.state.user.routers.slice(maxShowHeaderMenu)" :index="m.path" :key="m.meta.id" >{{m.meta.title}}</el-menu-item> </el-submenu> <el-submenu index="/my" style="float:right;"> <template slot="title">{{$store.state.user.name}}</template> <el-menu-item index @click="logout">注销</el-menu-item> </el-submenu> </el-menu>
//maxShowHeaderMenu 值是根据当前页面大小跟模块数量计算出最多可以显示多少个模块菜单,其余模块菜单会放进一个更多的折叠按钮下,是为了防止模块太多,导致顶部模块菜单超出,导致样式变形
//this.maxShowHeaderMenu = Math.floor(document.body.clentWidth / 100) -3;
以下是左侧菜单的组件,可实现哪个模块就显示哪些菜单
// 左侧菜单 <el-scrollbar class="_scroll"> <el-menu class="_layout-nav" :default-active="$route.path" :default-openeds="openedMenus" router ref="menu" style="border:none" @select="select" > <el-menu-tree v-for="menu in $store.state.user.routers" v-show="menu.path==modulePath" :menus="menu.children||[]" :key="menu.path" ></el-menu-tree> </el-menu> </el-scrollbar>
// 上面代码里封装的一个 el-menu-tree 组件 <template> <div> <template v-for="m in filterMenus"> <el-menu-item v-if="typeof(m.children)=='undefined'|| m.children.length==0" :key="m.meta.id" :index="m.meta.fullPath" >{{m.meta.title}}</el-menu-item> <el-submenu v-else :index="m.meta.fullPath" :key="m.meta.id"> <template slot="title">{{m.meta.title}}</template> <el-menu-tree :menus="m.children"></el-menu-tree> </el-submenu> </template> </div> </template> <script> export default { name: "elMenuTree", props: { menus: {type: Array} }, computed: {filterMenus() { return this.menus.filter(item => !item.meta.menuHide);} } }; </script>
对了,还有一个面包屑
这个很容易,主要是根据 $route.matched 数组
<el-breadcrumb separator="/" style="margin-bottom: 20px"> <el-breadcrumb-item v-for="m in breadItems" :key="m.meta.id" :to="m.path" @click.native="itemClick" >{{m.meta.title}}</el-breadcrumb-item> </el-breadcrumb>
//breadItems = this.$route.matched.filter(item=>!item.meta.menuHide);