后台管理系统通常都会有权限模块,用来控制用户能访问哪些页面和哪些数据接口。大多数管理系统的页面都长这样。 前端
左边为菜单,分为两级,右边为图表显示区域,有增删改查的按钮。vue
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_auth_rule -- ---------------------------- DROP TABLE IF EXISTS `t_auth_rule`; CREATE TABLE `t_auth_rule` ( `id_pk` bigint(20) NOT NULL AUTO_INCREMENT, `auth_id` varchar(128) NOT NULL COMMENT '权限Id', `pauth_id` varchar(128) DEFAULT NULL COMMENT '父级Id', `auth_name` varchar(255) NOT NULL COMMENT '权限名称', `auth_icon` varchar(255) NOT NULL COMMENT '权限图标', `auth_type` smallint(6) NOT NULL COMMENT '权限类型,BIT表示其属性\r\n 0x00表示可显示的菜单权限节点;\r\n 0x01表示普通节点', `auth_condition` text COMMENT '条件', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `is_menu` smallint(255) DEFAULT '0' COMMENT '是否为菜单,0表示非,1表示是', `weight` int(11) NOT NULL DEFAULT '0' COMMENT '权重', `rule` varchar(256) DEFAULT NULL COMMENT '规则路径主要对应菜单或方法的路径名称', `cr_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间', `up_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id_pk`), UNIQUE KEY `AK_auth_id` (`auth_id`) ) ENGINE=InnoDB AUTO_INCREMENT=264 DEFAULT CHARSET=utf8 COMMENT='权限规则表,记录权限相关的信息,权限以父子关系存在,菜单是权限的一种。'; SET FOREIGN_KEY_CHECKS = 1; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_role_auth -- ---------------------------- DROP TABLE IF EXISTS `t_role_auth`; CREATE TABLE `t_role_auth` ( `id_pk` bigint(20) NOT NULL AUTO_INCREMENT, `role_id_fk` varchar(32) DEFAULT NULL COMMENT '角色id', `auth_id_fk` varchar(128) DEFAULT NULL COMMENT '权限id', `aa` varchar(255) DEFAULT NULL, PRIMARY KEY (`id_pk`) ) ENGINE=InnoDB AUTO_INCREMENT=77 DEFAULT CHARSET=utf8 COMMENT='角色与权限的关系表'; SET FOREIGN_KEY_CHECKS = 1;
稍微解释一下表结构,t_auth_rule 表用来存储对应的权限菜单,通常来讲,菜单分为一级和二级菜单,rule字段对应前端的路由规则;而按钮为第三级,rule对应的是接口url地址。
t_role_auth 表为角色权限关联表,一个角色拥有哪些权限是经过这张表查出来的。固然还有一个role表,还有一个帐号表,帐号表里有一个role的外键。
这样就是一个帐号 --> 角色 --> 权限的关系。vue-router
// 本地写好路由列表(须要进行动态匹配的) export const routerList: Array<RouteConfig> = [ // 首页 { path: '/', name: '_home', redirect: '/home', component: Layout, meta: { hideInMenu: true, notCache: true }, children: [ { path: 'home', name: 'home', meta: { hideInMenu: true, title: '首页', notCache: true, icon: 'md-home' }, component: () => import('@/views/overview-operations/data-center/DataCenter.vue') } ] } ] // 负责将后台返回的 菜单列表 转成vue-router所须要的 router list export function toRouterComponent(menuList: Array<any>) { if (!menuList.length) return [] let routerArr = []; for (let j = 0; j < menuList.length; j++) { let obj; let firstIndex = routerList.findIndex(i => i.path === menuList[j].url); // 一级菜单 if (firstIndex !== -1) { let children: Array<RouteConfig> = [] obj = { path: routerList[firstIndex].path, component: routerList[firstIndex].component, redirect: routerList[firstIndex].redirect, name: routerList[firstIndex].name, meta: routerList[firstIndex].meta, children }; // 若是有子菜单 if (menuList[j].children && menuList[j].children.length) { for (let k = 0; k < menuList[j].children.length; k++) { const _children = routerList[firstIndex].children! let secondIndex = _children.findIndex(i => { let fullpath = ''; if (routerList[firstIndex].path === '/') { fullpath = `${routerList[firstIndex].path}${i.path}`; } else { fullpath = `${routerList[firstIndex].path}/${i.path}`; } return fullpath === menuList[j].children[k].url; }); if (secondIndex !== -1) { obj.children.push(_children[secondIndex]); } } } } if (obj) { routerArr.push(obj); } } return routerArr; } // 根据菜单权限,获取路由数组 // 本地只保存后台返回的菜单,在页面刷新的时候从本地拿到菜单从新调用toRouterComponent生成 路由数组 export function getRouterList() { if (!storage.get('username')) { storage.set('menuTree', ''); // 清空菜单权限数据 } let menuList = storage.get('menuTree') ? storage.get('menuTree') : []; const routerArr = toRouterComponent(menuList); return routerArr; }
menuList,菜单数组(或对象), 由后台返回; routerList为前端定义的路由表;遍历routerList,若是routerList的path在menuMap里能找到的话,就表示该路由存在。最后生成一个过滤后的路由表,用vue提供的addRoutes方法动态添加到路由中,并把过滤后的路由表存到本地。api
在页面刷新的时候,从本地获取路由表,添加到路由表中,代码以下,constRouterArr为基础路由表,好比登陆,404等 数组
注意这一步有个问题,因为我写的storage库用了JSON.stringify,把路由表中的component(实际为一个函数)丢失了,因此在从本地获取路由的时候,还要从新生成一个新的路由表,从新把component加上去,即把上面的addrouters从新执行一遍缓存
if (res.data.auth_rule_map) { let obj = {} Object.keys(res.data.auth_rule_map).forEach(i => { // 将全部的按钮放到一个obj里 key 为接口地址 if (res.data.auth_rule_map[i].is_menu === 0) { // 若是是按钮 obj[res.data.auth_rule_map[i].rule] = 1 } }) storage.set("btnList", obj); storage.set("menuTree", res.data.auth_rule_map); }
auth_rule_map为接口返回权限map,把按钮的权限过滤出来存到本地。
将map添加到每一个路由组件的data里,(这里有一个问题,怎么判断一个组件是不是路由组件),目前想到的是经过组件name来判断,把全部的路由组件放到一个数组里作判断。 ide
在组件内部的按钮上加上v-if,若是this.uri__里的uri在uriMap里存在就显示。
也能够经过方法来判断,以下面的__isBtnShow,不只能够控制按钮的显示隐藏,还能够控制其样式,好比颜色等,更加灵活,推荐使用方法来控制函数
uri = { ADD_MEMBER: '/api/add_member' } export default function install (Vue) { const uriMap = storage.get('btnList') //uriMap['/admin/api/auth_rule/update_auth_rule.action'] = 1 Vue.mixin({ created() { const arr = ['MemberManage', 'PayManage', '...'] if (arr.indexOf(this.$options.name) !== -1) { this.dataUri__ = uriMap this.uri__ = uri } }, data() { return { dataUri__: {} } }, methods: { __isBtnShow(uri) { return uriMap[uri] ? 'display: inline-block' : 'display: none' }, } }) } <Button v-if="dataUri__[uri__.ADD_MEMBER]">添加会员</Button> // 经过方法来控制,更加灵活 <Button :style="__isBtnShow(uri__.ADD_MEMBER)">添加会员</Button>
**登出后要清空缓存,routerArr,btnList 等。
因为以前登陆,调用addRouter把权限上个帐号的路由表加进去了,因此登出后要location.reload()一次,从新实例化路由表,去掉动态添加的路由,只保留基础路由。
location.reload()体验不是太好,可是vue-router没有提供动态删除路由的api,好比 deleteRouter。**this
能想到的解决方法是存一个loginIndex 来表示登陆帐号的个数,好比第一次登陆的时候存一个loginIndex=0, 后面存数据的时候都把这个参数带上;后面登多个帐号的时候个loginIndex++,这样localStorage的key就是一个动态的(这样仍是不行)
最简单的方法是存到localStorage里,只有登出才会清空缓存,只能登一个帐号。url