
一、将前端中的全量路由分为常量无权限公共和异步有权限异步加载之前我将所有的路由都写在了index.ts中的routes中现在我新建了一个routers.ts文件将路由单独分为无权限和有权限两个部分再引入index.ts中1.路由分类routers.ts// 登陆时的vue组件 import LoginView from ../views/login/index.vue // type { RouteRecordRaw }TypeScript类型用于类型检查type关键字表示这是纯类型打包时会被移除 // RouteRecordRaw是Vue Router源码中定义的一个TypeScript类型/接口规定了每个路由配置必须包含哪些字段、可选哪些字段、每个字段是什么类型 import type { RouteRecordRaw } from vue-router // 加上 type 关键字 // 动态导入路由懒加载 const Layout () import(../views/layout/index.vue) // 无权限公共路由 const constantRoutes: RouteRecordRaw[] [ { // 登录页用同步加载因为是必须的 path: /login, name: Login, component: LoginView, meta: { requiresAuth: false, hidden: true, } }, { // 其他页面懒加载 path: /, name: Home, redirect: /sysManage, meta: { requiresAuth: true, hidden: true, } }, { // 404 path: /404, name: 404, component:() import(/views/error/404.vue), meta: { title: 404 - 页面未找到, hidden: true, requiresAuth: false // 不需要登录 } } ] // 需要权限判断的异步加载路由 const asyncRouters: RouteRecordRaw[] [ { path: /sysManage, name: sysManage, component: Layout, meta: { title: 系统管理 }, redirect: /sysManage/userManage, children: [ { path: /sysManage/userManage, name: userManage, component: () import(/views/sys/UserManage/index.vue), meta: { title: 用户管理 } }, { path: /sysManage/menuManage, name: menuManage, component: () import(/views/sys/menuManage/index.vue), meta: { title: 菜单管理 } } ] } ] export { constantRoutes, asyncRouters }2.初始创建实例传入常量的无权限公共路由index.ts中创建路由实例的时候就先传入常量的无权限公共路由import {constantRoutes, asyncRouters} from ./routers // 创建路由实例 const router createRouter({ history: createWebHistory(), // 使用history模式 routes: constantRoutes // 传入路由表 })二、路由守卫beforeEach中处理动态路由重要1拿到后端有权限的路由信息其实在登陆的时候就已经保存在了localStorage中// 拿到后端resource const resourceStr localStorage.getItem(resource) || [] const resource JSON.parse(resourceStr)2根据拿到的后端权限路由与前端的全量路由取交集// 得到最终过滤后的路由 const accessRoutes permissionStore.generateRoutes(resource)generateRoutes是我创建的一个路由权限store仓库中的方法permission.ts这个文件的作用就是放置有权限的路由信息路由过滤routes为最终所有有权限的路由集合最后得到的routes要存入import { defineStore } from pinia; import { ref } from vue import { asyncRouters, constantRoutes } from /router/routers import { useRouterStore } from ./routerList import type { RouteConfig } from /api/types export const usePermissionStore defineStore(usePermission, () { const routes refRouteConfig[]([]) const routerStore useRouterStore() // 遍历树形结构收集所有路径便于过滤 const treeToSet (resource: RouteConfig[]) { const set new Setstring() listToSet(resource, , set) return set } // 递归函数开始遍历路由树 const listToSet (routes: RouteConfig[], parentPath: string, set: Setstring) { routes.forEach(r { // 获取当前路径 const path r.path || r.route // 先用path再用route if (!path) return // 构建完整路径(如果path以/开头则说明是绝对路径可以直接使用如果不是则需要拼接parentPath) const fullPath path.startsWith(/) ? path : ${parentPath}/${path} set.add(fullPath) // 添加当前路径 // 处理当前路径下的children,如果children里有子路由则接着递归 if (r.children r.children.length) { listToSet(r.children, fullPath, set) } }) } // 深拷贝 const deepClone T(obj: T): T { // 基础类型处理 if (obj null || typeof obj ! object ) return obj // 数组处理,对每个元素递归调用 deepClone,最后返回一个新数组 if (Array.isArray(obj)) return obj.map(item deepClone(item)) as T // 对象处理 const cloned: any {} for (const key in obj) { // 检查属性是否是自己的因为深拷贝拷贝的是自己本身的属性所以需要先检查 if (obj.hasOwnProperty(key)) { cloned[key] deepClone(obj[key] as any) } } return cloned as T } // 检查path是否存在于routeArray中 const hasPermission (routeArray: string[], path: string) { const match routeArray.find(r { // r.startsWith(path /) 检查path是否为r父路由 // 意思是如果path存在在routeArray里或者path是routeArray中某一项的父路由那么就代表有权限 return r path || r.startsWith(path /) }) return !!match } // 路由过滤遍历全量路由副本如果routeArray中包含就说明是需要展示的路由 const filterAsyncRoutes (routes: RouteConfig[], parentPath: string, routeArray: string[]): RouteConfig[] { const res: RouteConfig[] [] // 遍历routes for (const r of routes) { const path r.path || r.route const fullPath path?.startsWith(/) ? path : ${parentPath}/${path} // 给路由对象添加属性 r.fullPath fullPath r.pPath parentPath // 权限判断 if (hasPermission(routeArray, fullPath)) { if (r.children r.children.length) { r.children filterAsyncRoutes(r.children, fullPath, routeArray) } res.push(r) } } return res } // 递归查找子节点,在treeData中查找keyName属性中和key一样的值 const findItemKey (treeData: any[], key: string, keyName: string): any | null { for (const item of treeData) { // 检查当前节点是否匹配 if (item[keyName] key) { return item } // 如果有子节点接着递归查找 if (item.children item.children.length) { const found findItemKey(item.children, key, keyName) if (found) { return found } } } return null } // 给新路由排序 const setOrder (newRoutes: RouteConfig[], oldRoutes: RouteConfig[]) { // 遍历新路由,从旧路由中查找并复制排序值 newRoutes.forEach(nr { const match findItemKey(oldRoutes, nr.path, route) if (match) { nr.orderStr match.orderStr } if (nr.children?.length) { setOrder(nr.children, oldRoutes) } }) // 排序 newRoutes.sort((a, b) { return (a.orderStr ?? 1) - (b.orderStr ?? 1) }) } const generateRoutes (resource: RouteConfig[]) { const routeArray [...treeToSet(resource)] // 后端返回的所有权限路由Arraystring包括子路由 // 建立一个前端中有权限路由的全量副本 const asyncRoutes_copy deepClone(asyncRouters) // 过滤 const accessedRoutes filterAsyncRoutes(asyncRoutes_copy, , routeArray) if (accessedRoutes.length) { // 排序 setOrder(accessedRoutes, resource) // 重新设置重定向 accessedRoutes.forEach(e { e.redirect e.children?.[0].path ?? }) // 找到根路径 / 的路由将它的 redirect 设置为第一个动态路由的路径 const root constantRoutes.find(e e.path /) if (root) { root.redirect accessedRoutes[0].path } } // 合并常量路由和动态路由 let permission_routes accessedRoutes.concat(constantRoutes) // 处理应用路由以/app-开头的,预留微前端 // const apps resource.filter(r r.route?.startsWith(/app-)) // apps.forEach(r { // if (r.route) { // r.path r.route // delete r.route // } // }) // permission_routes permission_routes.concat(apps) routes.value permission_routes // 保存到pinia routerStore.saveRouter(permission_routes) return accessedRoutes } return { routes, generateRoutes, findItemKey } })4重置路由一定要把通配符加在最后不然就会一直定向404const dynamicRouteNames new Setstring() // 404 通配符必须在所有动态路由之后加 const catchAllRoute: RouteRecordRaw { path: /:oathMatch(.*)*, redirect: /404 } export function resetRouter(accessRoutes?: RouteRecordRaw[]) { // 移除所有动态路由 dynamicRouteNames.forEach(name { if (router.hasRoute(name)) { router.removeRoute(name) } }) dynamicRouteNames.clear() // 添加动态路由排在通配符之前 if (accessRoutes accessRoutes.length) { accessRoutes.forEach(route { if (!route.name) { route.name route.path } router.addRoute(route) dynamicRouteNames.add(route.name as string) }) } // 最后添加通配符 router.addRoute(catchAllRoute) dynamicRouteNames.add(pathMatch) }三、左侧菜单路由的修改优化1. hidden控制路由显隐其实这个时候所有的有权限的路由就已经挂载上了但是一些不希望出现在菜单中的路由也出现了这个时候就需要在路由中添加一个hidden去控制显隐在左侧菜单渲染的时候过滤掉hidden为true的路由template template v-ifmenuItem.children menuItem.children.length isHidden(menuItem) a-sub-menu :keymenuItem.route || String(menuItem.id) template #title span classmenu-title-wrapper SvgIcon v-ifmenuItem.meta?.icon :namemenuItem.meta?.icon size18px/SvgIcon span v-if!collapsed{{ menuItem.name }}/span /span /template MenuItem v-forchild in menuItem.children :keychild.route || child.id :menu-itemchild/MenuItem /a-sub-menu /template a-menu-item v-else-ifisHidden(menuItem) :keymenuItem.route clickhandleMenuClick(menuItem.route) {{ menuItem.name }} /a-menu-item /templatefunction isHidden(menuItem: RouteConfig) { if (menuItem.meta?.hidden) { return false } else { return true } }这样就可以过滤掉hidden为true的路由了2.更换路由title3.不同用户权限路由适配我之前设置的admin路由有菜单管理和用户管理user只有用户管理但现在两个账号切换时路由都是第一次登陆时挂载的路由。原因是登陆后没有重置路由所以在退出账号的时候要添加移除路由的操作并且在登陆的时候重置路由退出登录function LoginOut() { Modal.confirm({ title: 温馨提示, content: 确认退出吗, okText: 确认, cancelText: 取消, centered: true, getContainer: document.body, // 强制挂载到body上 async onOk() { await logout() userStore.clearUserInfo() userRouter.clearRouter() userPermission.clearRoutes() router.replace(/login) }, onCancel() {} }) }登录const handleLogin async () { // 这里写登录逻辑比如调用接口 console.log(登录信息, loginForm.username,loginForm. password) // 检查表单实例是否存在 if (!loginFormRef.value) return try { await loginFormRef.value.validate() loading.value true const res await login({ username: loginForm.username, password: loginForm.password }) console.log(res, res) // 保存token localStorage.setItem(token, res.token) // 保存用户信息到store userStore.setUserInfo({ userId: res.userId, username: res.username, email: res.email, nickname: res.nickname, avatar: res.avatar ?? , roles: res.roles, }) // 保存路由到store routerStore.saveRouter(res.resourceList as any) const accessRoutes await permissionStore.generateRoutes(res.resourceList as any) resetRouter(accessRoutes) message.success(登陆成功) // 跳转到重定向页面或首页 router.push(/) } catch (e: any) { // 表单验证失败或登陆失败 if (e?.errorFields) { //表单验证错误不提示 return } message.error(e?.message || e?.msg || 登陆失败) } finally { loading.value false } }注意 后端接口和前端的路由一定要提前设计好字段不然对接的时候会很难受。github地址GitHub - 199906-del/zy-admin · GitHub在线预览zy-admin