vue3后台系统动态路由实现

news/2025/1/11 21:13:30 标签: vue.js, javascript, vue3, 动态路由

动态路由的流程:用户登录之后拿到用户信息和token,再去请求后端给的动态路由表,前端处理路由格式为vue路由格式。

1)拿到用户信息里面的角色之后再去请求路由表,返回的路由为tree格式

后端返回路由如下:

前端处理:

共识:动态路由在路由守卫 beforeEach 里面进行处理,每次跳转路由都会走这里。

1.src下新建permission.js文件,main.js中引入

javascript">// main.js
import './permission'

2.permission.js里面重要的一点是:要确保路由已经被添加进去才跳转,否则页面会404或白屏

javascript">import router from "./router";
import { ElMessage } from "element-plus";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/auth";
import usePermissionStore from "@/store/modules/permission";
NProgress.configure({ showSpinner: false });

const whiteList = ["/login", "/register"];

const isWhiteList = (path) => {
  return whiteList.some((pattern) => isPathMatch(pattern, path));
};

router.beforeEach((to, from, next) => {
  NProgress.start();
  
  if (getToken()) {
    /* has token*/
    if (to.path === "/login") {
      next({ path: "/" });
      NProgress.done();
    } else if (isWhiteList(to.path)) {
      next();
    } else {
      // 如果已经请求过路由表,直接进入
      const hasRefresh = usePermissionStore().hasRefresh
      if (!hasRefresh) {
        next()
      }else{
        try {
          // getRoutes 方法用来获取动态路由
          usePermissionStore().getRoutes().then(routes => {           
            const hasRoute = router.hasRoute(to.name)
            routes.forEach(route => {
                router.addRoute(route) // 动态添加可访问路由表
            })
            if (!hasRoute) {
              // 如果该路由不存在,可能是动态注册的路由,它还没准备好,需要再重定向一次到该路由
              next({ ...to, replace: true }) // 确保addRoutes已完成
            } else {
              next()
            }
          }).catch((err)=>{
            next(`/login?redirect=${to.path}`)
          })
        } catch (error) {
          ElMessage.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    // 没有token
    if (isWhiteList(to.path)) {
      // 在免登录白名单,直接进入
      next();
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done();
    }
  }
});

router.afterEach(() => {
  NProgress.done();
});

3.store/modules/permission.js

javascript">async getRoutes() {
      this.hasRefresh = false;
      const roleId = JSON.parse(localStorage.getItem("user")).roldId;
      return new Promise((resolve, reject)=>{
        if (roleId) {
          getRouters({ roleId: roleId }).then((res) => {
            let routes = [];
            routes = generaRoutes(routes, res.data);
            console.log('routes',routes);
            this.setRoutes(routes)
            this.setSidebarRouters(routes)
            resolve(routes);
          });
        } else {
          this.$router.push(`/login`);
        }
      })  
    }

//添加动态路由
setRoutes(routes) {
   this.addRoutes = routes;
   this.routes = constantRoutes.concat(routes);
},

// 设置侧边栏路由
setSidebarRouters(routes) {
  this.sidebarRouters = routes;
}
javascript">// 匹配views里面所有的.vue文件
const modules = import.meta.glob("./../../views/**/*.vue");

//将后端给的路由处理成vue路由格式,这个方法不是固定的,根据后端返回的数据做处理
//这段代码是若依框架里的,原来的代码不支持三级路由,我改了下
function generaRoutes(routes, data, parentPath = "") {
  data.forEach((item) => {
    if (item.isAccredit == true) {
      if (
        item.category.toLowerCase() == "moudle" ||
        item.category.toLowerCase() == "menu"
      ) {
       
        const fullPath = parentPath ? `${parentPath}/${item.path}` : item.path;
        const menu = {
          path:
            item.category.toLowerCase() == "moudle"
              ? "/" + item.path
              : item.path,
          name: item.path,
          component:
            item.category.toLowerCase() == "moudle"
              ? Layout
              : loadView(`${fullPath}/index`),
          hidden: false,
          children: [],
          meta: {
            icon: item.icon,
            title: item.name,
          },
        };
        if (item.children) {
          generaRoutes(menu.children, item.children, fullPath);
        }
        routes.push(menu);
      }
    }
  });
  return routes;
}

export const loadView = (view) => {
  let res;
  for (const path in modules) {
    const dir = path.split("views/")[1].split(".vue")[0];
    // 将路径转换为数组以便逐级匹配
    const pathArray = dir.split('/');
    const viewArray = view.split('/');

    if (pathArray.length === viewArray.length && pathArray.every((part, index) => part === viewArray[index])) {
      res = () => modules[path]();
      break; // 找到匹配项后退出循环
    }
  }
  return res;
};

2)登录接口里后端返回路由表,返回的路由格式为对象数组,不为tree格式

这种情况下需要将后端返回的路由处理成tree格式后,再处理成vue的路由格式,我是分两步处理的。(有来技术框架基础上改的)

后端返回路由如下:这个数据格式比较简陋,但没关系,只要能拿到url或path就没问题

1.登录逻辑里面将数据处理成tree格式,store/modules/user.ts

  const menuList = useStorage<TreeNode[]>("menuList", [] as TreeNode[]);
 
function login(loginData: LoginData) {
    return new Promise<void>((resolve, reject) => {
      AuthAPI.login(loginData)
        .then((data) => {
          const { accessToken, info, menus, welcome } = data;
          setToken("Bearer" + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
          menuList.value = transRouteTree(menus);
          // 生成路由和侧边栏
          usePermissionStoreHook().generateRoutes(menuList.value);
          resolve();
        })
        .catch((error) => {
          reject(error);
        });
    });
  }

  // 将后端返回的路由转为tree结构
  function transRouteTree(data: RouteNode[]): TreeNode[] {
    if (!data || !Array.isArray(data)) {
      return [];
    }
    const map: { [id: number]: TreeNode } = {};
    const roots: TreeNode[] = [];
    data.forEach((node) => {
      if (!node || typeof node !== "object") {
        return [];
      }
      map[node.id] = {
        path: node.url ? node.url : "/",
        component: node.url ? node.url + "/index" : "Layout",
        name: node.url,
        meta: {
          title: node.menuName,
          icon: "system",
          hidden: false,
          alwaysShow: false,
          params: null,
        },
        children: [],
      };

      if (node.parentId === 0) {
        roots.push(map[node.id]);
      } else {
        if (map[node.parentId]) {
          map[node.parentId].children.push(map[node.id]);
        }
      }
    });
    return roots;
  }

2.src下的permission.ts 

router.beforeEach(async (to, from, next) => {
    NProgress.start();

    const isLogin = !!getToken(); // 判断是否登录
    if (isLogin) {
      if (to.path === "/login") {
        // 已登录,访问登录页,跳转到首页
        next({ path: "/" });
      } else {
        const permissionStore = usePermissionStore();
        // 判断路由是否加载完成
        if (permissionStore.isRoutesLoaded) {
          console.log(to, "to000");

          if (to.matched.length === 0) {
            // 路由未匹配,跳转到404
            next("/404");
          } else {
            // 动态设置页面标题
            const title = (to.params.title as string) || (to.query.title as string);
            if (title) {
              to.meta.title = title;
            }
            next();
          }
        } else {
          try {
            // 生成动态路由
            const list = userStore.menuList || [];
            await permissionStore.generateRoutes(list);
            next({ ...to, replace: true });
          } catch (error) {
            // 路由加载失败,重置 token 并重定向到登录页
            await useUserStore().clearUserData();
            redirectToLogin(to, next);
            NProgress.done();
          }
        }
      }
    } else {
      // 未登录,判断是否在白名单中
      if (whiteList.includes(to.path)) {
        next();
      } else {
        // 不在白名单,重定向到登录页
        redirectToLogin(to, next);
        NProgress.done();
      }
    }
  });

  // 后置守卫,保证每次路由跳转结束时关闭进度条
  router.afterEach(() => {
    NProgress.done();
  });

// 重定向到登录页
function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
  const params = new URLSearchParams(to.query as Record<string, string>);
  const queryString = params.toString();
  const redirect = queryString ? `${to.path}?${queryString}` : to.path;
  next(`/login?redirect=${encodeURIComponent(redirect)}`);
}

3.store/modules/permission.ts

  /**
   * 生成动态路由
   */
  function generateRoutes(data: RouteVO[]) {
    return new Promise<RouteRecordRaw[]>((resolve) => {
      const dynamicRoutes = transformRoutes(data);
      routes.value = constantRoutes.concat(dynamicRoutes); // 侧边栏
      dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route));
      isRoutesLoaded.value = true;
      resolve(dynamicRoutes);
    });
  }

/**
 * 转换路由数据为组件
 */
const transformRoutes = (routes: RouteVO[]) => {
  const asyncRoutes: RouteRecordRaw[] = [];
  routes.forEach((route) => {
    const tmpRoute = { ...route } as RouteRecordRaw;

    // 顶级目录,替换为 Layout 组件
    if (tmpRoute.component?.toString() == "Layout") {
      tmpRoute.component = Layout;
    } else {
      // 其他菜单,根据组件路径动态加载组件
      const component = modules[`../../views${tmpRoute.component}.vue`];

      if (component) {
        tmpRoute.component = component;
      } else {
        tmpRoute.component = modules["../../views/error-page/404.vue"];
      }
    }

    if (tmpRoute.children) {
      tmpRoute.children = transformRoutes(route.children);
    }

    asyncRoutes.push(tmpRoute);
  });

  return asyncRoutes;
};

http://www.niftyadmin.cn/n/5820191.html

相关文章

开源库:jcon-cpp

说明 jcon-cpp 是一个用于 C 的 JSON-RPC 库&#xff0c;它允许开发者通过 JSON-RPC 协议进行进程间通信&#xff08;IPC&#xff09;。JSON-RPC 是一种轻量级的远程过程调用协议&#xff0c;基于 JSON 格式数据进行通信。基于MIT协议&#xff0c;最新代码基于Qt6实现。可通过…

PHP语言的学习路线

PHP语言的学习路线 PHP&#xff08;Hypertext Preprocessor&#xff09;是一种广泛使用的开源服务器端脚本语言&#xff0c;尤其适用于Web开发。由于其易学易用、功能强大&#xff0c;PHP成为了许多动态网站和Web应用程序开发的首选语言。随着Web3.0和云计算的兴起&#xff0c…

B-spline 控制点生成

B-spline 控制点生成 一、概述 本知识文档详细介绍了如何将离散的轨迹点转换为 B-spline 的控制点的方法&#xff0c;包括其背后的数学原理和相应的代码实现。B-spline 曲线在多个领域&#xff0c;如计算机图形学、机器人路径规划、动画制作等&#xff0c;有着广泛的应用&…

统计学习方法(第二版) 第五章 决策树

本章主要讨论决策树模型&#xff0c;以及生成、剪枝和分类的方法。 剪枝方法的补充 统计学习方法(第二版) 第五章 补充-CSDN博客 目录 基础数学知识&#xff1a; 1.自信息量 2.互信息 3.平均自信息量和熵 4.熵函数的性质 5.联合熵和条件熵 6.平均互信息量&#xff08;…

怎么对PDF插入图片并设置可见程度-免费PDF编辑工具分享

一、PDF文件插入图片操作的需求背景 在我们日常对PDF文件的各种处理中&#xff0c;我们有时需要对手中的PDF插入一些图片&#xff0c;以期达到更好的页面视觉效果&#xff0c;包括增强视觉效果、丰富信息呈现、个性化定制、提高专业度、简化阅读流程、符合法规要求以及适应不同…

E10鸿蒙App

入口&#xff1a;EntryAbility -> onWindowStageCreate windowStage.loadContent jsApi&#xff1a;JSNameConstants App主页面&#xff1a;MainPage 消息主页面&#xff1a;MessageCenterPage 会话列表&#xff1a;SessionListViewIMHeaderViewAppImageKnifeComponent…

易于上手难于精通---关于游戏性的一点思考

1、小鸟、狙击、一闪&#xff0c;都是通过精准时机来逼迫玩家练习&#xff0c; 而弹道、出招时机等玩意&#xff0c;不是那么容易掌握的&#xff0c;需要反复的观察、反应与行动&#xff0c; 这也正是游戏性的体现&#xff0c; 玩家能感觉到一些朦胧的东西&#xff0c;但又不…

01 springboot集成mybatis后密码正确但数据库连接失败

01 springboot集成mybatis后密码正确但数据库连接失败 问题描述&#xff1a; 1.datasource配置&#xff1a; //application.yaml spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mp?useUnicodetrue&characterEncodingUTF-8&autoReconnecttrue&serverTime…