前言
动态路由权限控制是项目常用的功能,这里介绍一种方式,通过将后端权限码与本地路由的JSON配置ID进行匹配,能够有效地实现用户权限的控制。不同角色的操作项是根据权限码匹配到的路由进行区分的,这样可以确保每个用户只能看到与自己所属角色相关的操作项。随后,通过特定方法将其转换为路由格式,并通过addRoute方法动态添加路由,从而实现了灵活而精准的用户权限路由控制。
功能分析
(1)路由配置项:由parentRouter父级路由和childrenRouter子级路由组成,关联方式为子级路由的parentId和父级路由的id匹配,后续权限路由匹配完毕后,通过formatTree方法生成嵌套路由的格式
(2)基于配置项的动态路由处理:将动态路由的配置项化,这样可以更加灵活地控制路由的生成。通过配置项,你可以定义路由的路径、组件、参数等信息,方便地管理和维护路由配置。
(3)使用标识代替组件引入:使用标识字符串来代替组件的引入。例如,将布局组件标识为’layout’。这样可以简化配置项的设置,并且可以方便地进行组件的替换或扩展
(4)与后端权限码绑定的JSON路由配置:可以在JSON路由配置项中添加parentId、id,绑定后端权限码信息。这样可以方便与后端返回的权限码进行匹配,从而生成符合条件的动态路由对象,实现基于权限的动态路由生成。
(5)权限联系:父级路由的权限按照子级路由的权限来处理,当一个有权限的子级路由都没有时,父级路由剔除
代码+详细解释
(1)此处举例动态路由映射文件配置
// 父级路由
export const parentRouter: RouterProp[] = [
// 系统管理
{
path: "/system",
meta: { title: "系统管理", icon: "xt", alwaysShow: true },
component: "Layout",
id: "4",
parentId: "0",
},
];
// 子集路由
export const childrenRouter: RouterProp[] = [
// -----------------------系统管理子菜单-------------------------------
{
path: "role",
name: "Role",
meta: {
title: "角色管理",
breadcrumb: false,
icon: "tsrzsb",
},
component: "system/role/index",
id: "09000000",
parentId: "4",
},
{
path: "menu",
name: "Menu",
meta: {
title: "菜单管理",
breadcrumb: false,
icon: "tsrzsb",
},
component: "system/menu/index",
id: "09000001",
parentId: "4",
},
{
path: "pushFailureLog",
name: "PushFailureLog",
meta: {
title: "推送失败日志",
breadcrumb: false,
icon: "tsrzsb",
},
component: "system/pushFailureLog/index",
id: "09000002",
parentId: "4",
},
];
(2)递归生成路由对象方法
// 递归函数格式化树状菜单
export function formatTree(data: any, id = 'permId', pid = 'parentId', child = 'children', root = '0') {
const tree = [] as any
if (data && data.length > 0) {
data.forEach((item: any) => {
// 获取顶层菜单,parent_id === 0
if (item[pid] === root) {
const children = formatTree(data, id, pid, child, item[id])
if (children && children.length > 0) {
item[child] = children
}
tree.push(item)
}
})
}
return tree
}
(3)新建pinia store文件,通过后台返回的路由权限码,映射本地路由JSON配置文件,并通过mapComponent方法生成常规路由对象格式
import { defineStore } from "pinia";
import Layout from "@/layout/index.vue";
import { formatTree } from "@/utils/tree";
import { parentRouter, childrenRouter } from "@/router/config";
import { getCookie } from "@/utils";
import { Stores } from "types/stores";
const _import = (file: string) => () => import("@/views/" + file + ".vue");
/**
* 映射本地组件
* @returns
*/
export function mapComponent(data: any) {
const tree_data = data.filter((item: any) => {
// component
item.component = item.component === "layout" ? Layout : _import(`${item.component}`);
// children
if (item.children && Object.prototype.toString.call(item.children) === "[object Array]" && item.children.length >= 0) {
mapComponent(item.children);
}
return item;
});
return tree_data;
}
export const routerStore = defineStore("permission", {
state: (): Stores.router => ({
asyncRouter: [],
}),
actions: {
/**
* 获取路由
*/
getRouterAction(): Promise<RouterItem[]> {
return new Promise(async (resolve, reject) => {
// 权限code集合
let powerCode = getCookie("permIds")?.split(",") || [];
// 不存在路由
if (!powerCode?.length) {
reject();
return false;
}
const asyncRouterArr: RouterItem[] = [];
powerCode.forEach((id) => {
const hasRouter = childrenRouter.filter((item: any) => item.id.split(",").includes(id));
hasRouter.length && asyncRouterArr.push(hasRouter[0]);
});
// 去重
powerCode = [...new Set(powerCode)];
// 格式化树形
const routerTree = formatTree([...parentRouter, ...asyncRouterArr], "id");
/**
* 过滤子级
*/
const filterRouter = routerTree.filter((item: RouterItem) => {
return item?.children?.length;
});
const routers = mapComponent(filterRouter);
this.asyncRouter = routers;
resolve(routers);
});
},
},
});
(4)在路由守卫中,添加并更新动态路由
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
// 判断是否有token
const token = getCookie(TOKEN);
const routeStore = routerStore();
if (!token) {
next("/login");
} else {
// 获取静态路由
console.log("router.options", router.options);
if (routeStore.asyncRouter.length === 0) {
try {
// 获取动态路由
const asyncRouter = await routeStore.getRouterAction();
// 获取静态路由
const default_router_data = router.options.routes;
// 更新静态路由
router.options.routes = default_router_data.concat(asyncRouter);
// 激活动态路由
asyncRouter.forEach((item: RouterItem) => {
router.addRoute(item);
});
// 确认进入下一个路由
next({ ...to, replace: true });
} catch {
next();
}
} else {
if (to.name === "Login") {
if (!from.name) {
next("/admin");
} else {
router.back();
}
} else {
next();
}
}
}
});