diff --git a/src/api/common/login.ts b/src/api/common/login.ts index e5748eb..65cc051 100644 --- a/src/api/common/login.ts +++ b/src/api/common/login.ts @@ -1,4 +1,5 @@ import { http } from "@/utils/http"; +import { RouteRecordRaw } from "vue-router"; export type CaptchaDTO = { /** 验证码的base64图片 */ @@ -97,12 +98,67 @@ export const getLoginUserInfo = () => { return http.request>("get", "/getLoginUserInfo"); }; -type Result = { - success: boolean; - data: Array; +export interface RouteMeta { + id: string; + title: string; + icon?: string; + showLink?: boolean; + showParent?: boolean; + auths?: string[]; + rank?: number; + frameSrc?: string; + isFrameSrcInternal?: boolean; +} + +export type RouteItem = RouteRecordRaw & { + name?: string; + path: string; + meta: RouteMeta; + children?: RouteItem[]; }; -/** 获取动态菜单 */ -export const getAsyncRoutes = () => { - return http.request("get", "/getRouters"); +type AsyncRoutesResponse = { + code: number; + msg: string; + data: RouteItem[]; +}; + +/** + * 为后端返回的路由添加唯一id,后面我们在构建菜单树的层级结构时需要用到 + * 这里我们假设 name + path 是唯一的,若日后有 name + path 不唯一的情况, + * 则需要修改此处的逻辑 + */ +const addUniqueId = (routes: RouteItem[]): RouteItem[] => { + return routes.map(route => { + const id = `${route.name || ""}${route.path}`; + + if (route.children && route.children.length > 0) { + route.children = addUniqueId(route.children); + } + + return { + ...route, + meta: { + ...route.meta, + id + } + }; + }); +}; + +function withId(result: AsyncRoutesResponse) { + if (result.data) { + result.data = addUniqueId(result.data); + } + + return result; +} + +/** + * 获取动态菜单 + * TODO:对于开发环境下此处可以对路由数据做一些校验,比如说 name 是否重复,name+path 是否重复等等 + */ +export const getAsyncRoutes = async () => { + const result = await http.request("get", "/getRouters"); + return withId(result); }; diff --git a/src/router/utils.ts b/src/router/utils.ts index 06645f2..26e254d 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -27,7 +27,7 @@ const IFrame = () => import("@/layout/frameView.vue"); const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}"); // 动态路由 -import { getAsyncRoutes } from "@/api/common/login"; +import { getAsyncRoutes, RouteItem } from "@/api/common/login"; import { TokenDTO } from "@/api/common/login"; function handRank(routeInfo: any) { @@ -152,34 +152,32 @@ function addPathMatch() { } /** 处理动态路由(后端返回的路由) */ -function handleAsyncRoutes(routeList) { - if (routeList.length === 0) { - usePermissionStoreHook().handleWholeMenus(routeList); - } else { - formatFlatteningRoutes(addAsyncRoutes(routeList)).map( - (v: RouteRecordRaw) => { - // 防止重复添加路由 - if ( - router.options.routes[0].children.findIndex( - value => value.path === v.path - ) !== -1 - ) { - return; - } else { - // 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转 - router.options.routes[0].children.push(v); - // 最终路由进行升序 - ascending(router.options.routes[0].children); - if (!router.hasRoute(v?.name)) router.addRoute(v); - const flattenRouters: any = router - .getRoutes() - .find(n => n.path === "/"); - router.addRoute(flattenRouters); - } +function handleAsyncRoutes(routeList: RouteItem[]) { + if (routeList.length) { + const normalizedRoutes = normalizeBackendRoutes(routeList); + const flattenedRoutes = formatFlatteningRoutes(normalizedRoutes); + + flattenedRoutes.forEach((v: RouteRecordRaw) => { + // 防止重复添加路由 + if ( + router.options.routes[0].children.findIndex( + value => value.path === v.path + ) !== -1 + ) { + return; } - ); - usePermissionStoreHook().handleWholeMenus(routeList); + + // 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转 + router.options.routes[0].children.push(v); + // 最终路由进行升序 + ascending(router.options.routes[0].children); + if (!router.hasRoute(v?.name)) router.addRoute(v); + const flattenRouters: any = router.getRoutes().find(n => n.path === "/"); + router.addRoute(flattenRouters); + }); } + + usePermissionStoreHook().handleWholeMenus(routeList); addPathMatch(); } @@ -218,7 +216,7 @@ function initRouter() { * @param routesList 传入路由 * @returns 返回处理后的一维路由 */ -function formatFlatteningRoutes(routesList: RouteRecordRaw[]) { +function formatFlatteningRoutes(routesList: RouteItem[]) { if (routesList.length === 0) return routesList; let hierarchyList = buildHierarchyTree(routesList); for (let i = 0; i < hierarchyList.length; i++) { @@ -293,12 +291,15 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) { } /** 过滤后端传来的动态路由 重新生成规范路由 */ -function addAsyncRoutes(arrRoutes: Array) { - if (!arrRoutes || !arrRoutes.length) return; +function normalizeBackendRoutes(backendRoutes: Array) { + if (!backendRoutes || !backendRoutes.length) return; + const modulesRoutesKeys = Object.keys(modulesRoutes); - arrRoutes.forEach((v: RouteRecordRaw) => { - // 将backstage属性加入meta,标识此路由为后端返回路由 + + backendRoutes.forEach(v => { + // 标识此路由为后端返回路由 v.meta.backstage = true; + // 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值 if (v?.children && v.children.length && !v.redirect) v.redirect = v.children[0].path; @@ -316,14 +317,15 @@ function addAsyncRoutes(arrRoutes: Array) { v.component = modulesRoutes[modulesRoutesKeys[index]]; } if (v?.children && v.children.length) { - addAsyncRoutes(v.children); + normalizeBackendRoutes(v.children); } }); - return arrRoutes; + + return backendRoutes; } /** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */ -function getHistoryMode(routerHistory): RouterHistory { +function getHistoryMode(routerHistory: string): RouterHistory { // len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1 const historyMode = routerHistory.split(","); const leftMode = historyMode[0]; @@ -379,7 +381,7 @@ export { addPathMatch, isOneOfArray, getHistoryMode, - addAsyncRoutes, + normalizeBackendRoutes, getParentPaths, findRouteByPath, handleAliveRoute, diff --git a/src/utils/tree.ts b/src/utils/tree.ts index e0ae9ad..d596bc5 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -1,3 +1,5 @@ +import { RouteItem } from "@/api/common/login"; + /** * @description 提取菜单树中的每一项uniqueId * @param tree 树 @@ -47,29 +49,41 @@ export const deleteChildren = (tree: any[], pathList = []): any => { return tree; }; +// 定义扩展属性类型 +export interface HierarchyNodeExtra { + id?: string; + parentId?: string | null; + pathList?: string[]; +} + /** * @description 创建层级关系 * @param tree 树 * @param pathList 每一项的id组成的数组 * @returns 创建层级关系后的树 */ -export const buildHierarchyTree = (tree: any[], pathList = []): any => { +export function buildHierarchyTree( + tree: T[], + pathList: string[] = [] +): T[] { if (!Array.isArray(tree)) { console.warn("tree must be an array"); return []; } if (!tree || tree.length === 0) return []; - for (const [key, node] of tree.entries()) { - node.id = key; - node.parentId = pathList.length ? pathList[pathList.length - 1] : null; + + for (const node of tree) { + node.id = node.meta.id; + node.parentId = pathList.at(-1) ?? null; node.pathList = [...pathList, node.id]; - const hasChildren = node.children && node.children.length > 0; - if (hasChildren) { + + if (node?.children?.length) { buildHierarchyTree(node.children, node.pathList); } } + return tree; -}; +} /** * @description 广度优先遍历,根据唯一uniqueId找当前节点信息