一、将前端中的全量路由分为常量(无权限公共)和异步(有权限异步加载)
之前我将所有的路由都写在了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 = ref<RouteConfig[]>([]) const routerStore = useRouterStore() // 遍历树形结构,收集所有路径,便于过滤 const treeToSet = (resource: RouteConfig[]) => { const set = new Set<string>() listToSet(resource, '', set) return set } // 递归函数,开始遍历路由树 const listToSet = (routes: RouteConfig[], parentPath: string, set: Set<string>) => { 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)] // 后端返回的所有权限路由(Array<string>),包括子路由 // 建立一个前端中有权限路由的全量副本 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)重置路由:一定要把通配符加在最后,不然就会一直定向404!
const dynamicRouteNames = new Set<string>() // 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-if="menuItem.children && menuItem.children.length && isHidden(menuItem) "> <a-sub-menu :key="menuItem.route || String(menuItem.id)"> <template #title> <span class="menu-title-wrapper"> <SvgIcon v-if="menuItem.meta?.icon" :name="menuItem.meta?.icon" size="18px"></SvgIcon> <span v-if="!collapsed">{{ menuItem.name }}</span> </span> </template> <MenuItem v-for="child in menuItem.children" :key="child.route || child.id" :menu-item="child"></MenuItem> </a-sub-menu> </template> <a-menu-item v-else-if="isHidden(menuItem)" :key="menuItem.route" @click="handleMenuClick(menuItem.route)"> {{ menuItem.name }} </a-menu-item> </template>function isHidden(menuItem: RouteConfig) { if (menuItem.meta?.hidden) { return false } else { return true } }这样就可以过滤掉hidden为true的路由了
2.更换路由title
3.不同用户权限路由适配
我之前设置的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