[{
"id": 1,
"parentId": 0,
"resourceName": "运营管理中心",
"resourceInfo": "运营管理中心页面资源",
"resourceType": 0,
"resourceUrl": "/xxxx",
"resourceCode": "/xxxx",
"appName": "运营管理中心"
},{
"id": 2,
"parentId": 1,
"resourceName": "设备管理",
"resourceInfo": "设备管理页面资源",
"resourceType": 0,
"resourceUrl": "/xxxx",
"resourceCode": "/xxxx",
"appName": "运营管理中心"
},{
"id": 3,
"parentId": 2,
"resourceName": "设备管理-新增",
"resourceInfo": "设备管理-按钮操作新增设备",
"resourceType": 1,
"resourceUrl": "/xxxx",
"resourceCode": "/xxxx",
"appName": "运营管理中心"
},....]
返回的是一种简单JSON数据,以前用过jQuery树型插件zTree的同学或许很熟悉。但这种简单JSON数据结构目前并不能直接在Vue的ElementUI的el-tree组件中使用,需要转化为标准JSON数据结构,这里提供了一个转化方法,供参考:
// 转换el-tree数据格式:children级联
export const convertTreeData = (data: any[], config: any) => {
let id = config.id || 'id';
let pid = config.pid || 'pid';
let children = config.children || 'children';
let idMap: any = {
};
let jsonTree: any = [];
data.forEach((v: any) => {
idMap[v[id]] = v;
});
data.forEach((v: any) => {
let parent = idMap[v[pid]];
if (parent) {
!parent[children] && (parent[children] = []);
parent[children].push(v);
} else {
jsonTree.push(v);
}
});
return jsonTree;
};
// 调用方法:将后端返回的简单json树responseTreeData转换为el-tree识别的标准树数据结构elTreeData
const elTreeData = convertTreeData(responseTreeData, {
id: 'id',
pid: 'parentId',
children: 'children'
});
Note that:convertTreeData()这个方法里的any,尽量换成自己项目里需要的类型接口interface,使用大量any会让TypeScript的价值大打折扣。
关于页面菜单的权限控制这里有两种方案:
【方案一】展示全部的页面菜单项,使用导航守卫拦截提示没有权限的部分。
【方案二】仅展示有权限的页面菜单项(此方案在方案一的基础上进行,导航守卫拦截是不可缺少的)。
实现方式概述,这里是导航守卫的拦截片段,其核心思路就是:拿到路由的to.path后,与store中存储的当前用户所拥有的该app中的权限比对判断。
// 全局前置导航
router.beforeEach(beforeEachCallback);
拦截的代码片段beforeEachCallback:
// 验证权限
const getRoleCheckNextStep: GetRoleCheckNextStep = (toPath) => {
// 以这些 url 卡头的路径不需要验证权限
const isNoCheckPath = [
CommonUrls.Login,
CommonUrls.Home,
CommonUrls.NotFound
].some((path) => toPath.toLowerCase().startsWith(path));
if (isNoCheckPath) {
return NextSteps.Next;
}
// 拦截无权限的页面菜单部分
// grantedResouse为当前用户所拥有的该app中的权限,数据结构为后端RD提供的简单树JSON
const grantedResouse = JSON.parse(store.getters.getGrantedResouse());
const hasAuth = grantedResouse.some((item: any) => toPath.toLowerCase().endsWith(item.resourceUrl));
if (!hasAuth) {
return NextSteps.Stay;
} else {
return NextSteps.Next;
}
};
const roleNextStep = getRoleCheckNextStep(to.path);
if (roleNextStep === NextSteps.Stay) {
Message.error('您没有该模块的访问权限,如有需要请与管理员联系');
next(false);
return;
}
next();
若平台中的某个系统只需要展示用户有权限的部分,eg:菜单栏里只显示有权限的部分。这时就需要采用【方案二】,具体要根据PM的mrd来敲定。
FE的研发内容,相对好处理,做递归筛查出有权限的data,然后再渲染菜单即可:
// store中当前用户对应系统的权限资源
@Getter getGrantedResouse!: () => string;
/**
* 递归过滤配置的静态菜单选项,返回符合用户角色权限的菜单
* @param menus 配置静态的菜单
*/
filterUserAuthMenu(menus: any[]) {
const grantedResouse = JSON.parse(this.getGrantedResouse());
const accessedMenus = menus.filter((menu) => {
if (this.hasPermission(menu, grantedResouse)) {
if (menu.children && menu.children.length) {
menu.children = this.filterUserAuthMenu(menu.children);
}
return true;
} else {
return false;
}
});
return accessedMenus;
}
/**
* 判断是否有权限
* @param menu 当前菜单
* @param grantedResouse 当前中心的权限
* */
hasPermission(menu: MenuConfig, grantedResouse: any[]) {
if (menu.title) {
return grantedResouse.some((item: any) => {
return menu.title === item.resourceName;
});
}
}
async mounted() {
// 只展示用户拥有权限的菜单:使用filteredMenus替代配置的静态菜单menus
// 暂不使用过滤后的菜单:避免使用递归循环从而减少性能的损耗
this.filteredMenus = await this.filterUserAuthMenu(this.menus);
}
Note that:考虑到平台内各个系统的体量、前端性能等因素,FE应尽可能避免使用递归循环。本项目未采用【方案二】。
若需要对系统内增删改等按钮操作做权限控制,这里采用了自定义指令的方式来控制:
// 注册一个全局自定义指令 `v-allow` 根据权限控制操作按钮的隐显
Vue.directive('allow', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function(el: any, binding, vnode) {
const grantedResouse = JSON.parse(store.getters.getGrantedResouse());
const hasAuth = grantedResouse.some((item: any) => {
return binding.value.name === item.resourceName;
});
if (!hasAuth) {
el.parentNode.removeChild(el);
}
}
});
<el-button v-allow="{name: '摄像头管理-新增'}">新增el-button>
Note that:这里从项目后续可能的扩展性考虑,在v-allow指令里传入了一个对象,便于后续版本迭代增加新的key\value,来做不同的控制。
这种由资源-角色-用户动态配置的权限管理方案,有较高的灵活性。以前也做过一种较为简单死板的权限管理控制:页面有固定的使用角色,用户有固定的角色,这种情况下,只需要FE在自己的router里配置固定的权限拥有着然后遍历即可,配置 meta.roles 如下,eg:
{
path: '/accountData',
name: 'accountData',
meta: {
title: '账户管理', icon: 'fa fa-user-plus', roles: ['admin','editor']},
component: () => import('@/views/UserManage/AccountData.vue')
}
然后根据当前用户的固定角色,与router里固定的meta.roles进行逻辑操作即可完成权限控制:
// router.beforeEach()中,权限判断
if (hasPermission(key, to)) {
next();
} else {
next('/404'); // 没有权限进入
}
/**
* 判断是否有权限
* @param roles 当前角色
* @param route 当前路由对象
* */
function hasPermission(roles: string, route: any) {
if (route.meta && route.meta.roles) {
// 如果meta.roles是否包含角色的key值,如果包含那么就是有权限,否则无权限
return route.meta.roles.some((role: string) => {
return role.indexOf(roles) >= 0;
});
} else {
// 默认不设置有权限
return true;
}
}
在补充说明中的这种权限处理方案有较大的局限性,适用于小型系统。现在回头再看,还是对于前端来说,还是jQuery时代做权限管理带劲,前端直接拿到资源树渲染菜单就行了,因为view路由都在后端的Controller里拦截。