之前有一些网友对我那个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);