Skip to content

动态路由

  • 动态参数路由
    • /stu/:id
    • /stu/1、/stu/2
  • 动态的添加/删除路由表里面的路由
    • 角色 A:1、2、3、4、5
    • 角色 B:1、2、4

基础知识

addRoute

这里的 router 就是通过 createRouter 方法创建的路由实例。

  • router.addRoute( ):动态的添加路由,只注册一个新的路由,如果要跳转到新路由需要手动 push 或者 replace.

removeRoute

  • router.removeRoute(name):动态的移除路由,除了此方法移除路由,还有几种方式

    • 通过添加一个名称冲突的路由。如果添加与现有路由名称相同的路由,会先删除旧路由,再添加新路由:

      js
      router.addRoute({ path: "/about", name: "about", component: About });
      // 这将会删除之前已经添加的路由,因为他们具有相同的 name
      router.addRoute({ path: "/other", name: "about", component: Other });
    • 通过调用 router.addRoute( ) 返回的回调函数,调用该函数后可以删除添加的路由。当路由没有名称时,这很有用。

      js
      const removeRoute = router.addRoute(routeRecord);
      removeRoute(); // 删除路由如果存在的话

如果要添加嵌套的路由,可以将路由的 name 作为第一个参数传递给 router.addRoute( )

js
router.addRoute({ name: "admin", path: "/admin", component: Admin });
router.addRoute("admin", { path: "settings", component: AdminSettings });

这等价于:

js
router.addRoute({
  name: "admin",
  path: "/admin",
  component: Admin,
  children: [{ path: "settings", component: AdminSettings }],
});

另外还有两个常用 API:

  • router.hasRoute(name):检查路由是否存在。
  • router.getRoutes( ):获取一个包含所有路由记录的数组。

实战案例:后台菜单权限

需求:

实现一个后台管理系统,该系统根据用户登录的不同角色,显示不同的导航栏。

如果处于登录状态,则不能在地址栏输入/login来进行跳转。

权限分为三种:

  • 管理员:能够访问所有模块(教学、教师、课程、学生)
  • 教师:能够访问教学、课程、学生模块
  • 学生:能够访问课程模块

思路

  • 路由表中分出来常量路由和异步路由
  • 添加一个路由映射表,分别是每个角色对应哪些路由(类似后端返回路由表的方式)
  • 当用户登录的时候,本地存储 userInfo 和登录状态,从路由表中找出对应的路由,通过addRoute()来动态添加路由。
  • 通过当前角色对应的路由,侧边栏渲染出来。

处理细节:

  • 刷新页面会丢失路由信息
    • (因为刷新页面会重新执行 main.js 文件,所以在 main.js 中重新设置一下路由表)
  • 路由会有缓存,导致角色切换后侧边栏不更新
    • 在设置路由前,将所有的路由清空即可。
  • 用户已经登录,通过地址栏访问 login 应当跳转之前所在的页面
  • 用户未登录却想去要权限的页面,跳转到登录页
js
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import { routesMap } from "./roles";

const baseRoutes = [
  { path: "/login", component: () => import("../views/Login.vue") },
  {
    path: "/",
    name: "Dashboard",
    component: () => import("../views/Dashboard.vue"),
    children: [],
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes: baseRoutes,
});

// 获取 dashboard 路由
const dashboardRoute = router.getRoutes().find((route) => route.path === "/");

/**
 * 清空已有的路由
 */
function clearRoutes() {
  const routes = router.getRoutes();
  routes.forEach((route) => {
    // 如果路由的 name 不为空,并且不是 dashboard 路由,则移除
    if (route.name && route.name !== dashboardRoute.name) {
      router.removeRoute(route.name);
    }
  });
}

/**
 * 根据角色动态的添加路由
 * @param {*} role string (admin、teacher、student)
 */
export function setRoutesbyRole(role) {
  // 1. 先清空已有的路由
  clearRoutes();
  // 2. 根据角色将对应的路由取出来
  const roleRoutes = routesMap[role] || [];
  // 3. 动态的给 dashboard 添加子路由
  roleRoutes.forEach((route) => {
    router.addRoute(dashboardRoute.name, route);
  });
}

// 全局路由守卫
router.beforeEach((to, from, next) => {
  // 先获取用户的登录状态
  const isLogin = localStorage.getItem("isLogin");
  if (to.path === "/login" && isLogin) {
    // 用户已经登陆了,但是他又想去登陆页,直接跳转到之前所在的页面
    const activeIndexRoute = localStorage.getItem("activeIndex");
    next(activeIndexRoute);
  } else if (to.path !== "/login" && !isLogin) {
    // 用户未登录,但是你又要去受保护的路由,跳转到登陆页
    next("/login");
  } else {
    next();
  }
});

export default router;
js
// 角色和路由之间的关系映射表
export const routesMap = {
  admin: [
    {
      path: "/student",
      name: "Student",
      component: () => import("../views/Student.vue"),
      meta: { title: "学生模块", icon: "User" },
    },
    {
      path: "/teacher",
      name: "Teacher",
      component: () => import("../views/Teacher.vue"),
      meta: { title: "教师模块", icon: "Suitcase" },
    },
    {
      path: "/teaching",
      name: "Teaching",
      component: () => import("../views/Teaching.vue"),
      meta: { title: "教学模块", icon: "DataBoard" },
    },
    {
      path: "/course",
      name: "Course",
      component: () => import("../views/Course.vue"),
      meta: { title: "课程模块", icon: "Collection" },
    },
  ],
  teacher: [
    {
      path: "/student",
      name: "Student",
      component: () => import("../views/Student.vue"),
      meta: { title: "学生模块", icon: "User" },
    },
    {
      path: "/teaching",
      name: "Teaching",
      component: () => import("../views/Teaching.vue"),
      meta: { title: "教学模块", icon: "DataBoard" },
    },
    {
      path: "/course",
      name: "Course",
      component: () => import("../views/Course.vue"),
      meta: { title: "课程模块", icon: "Collection" },
    },
  ],
  student: [
    {
      path: "/course",
      name: "Course",
      component: () => import("../views/Course.vue"),
      meta: { title: "课程模块", icon: "Collection" },
    },
  ],
};
js
let userInfo = null;

/**设置用户信息 */
export function setUserInfo(user) {
  userInfo = user;
  localStorage.setItem("userInfo", JSON.stringify(user));
}

/**获取用户信息 */
export function getUserInfo() {
  if (localStorage.getItem("userInfo")) {
    userInfo = JSON.parse(localStorage.getItem("userInfo"));
  }
  return userInfo;
}

/**清除用户信息 */
export function clearUserInfo() {
  localStorage.clear();
}
vue
<template>
  <div class="login-container">
    <div class="container">
      <h1 class="title">智能云端学生平台</h1>
      <el-form :model="loginForm">
        <el-form-item prop="username">
          <el-input
            v-model="loginForm.username"
            placeholder="用户名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="密码"
          ></el-input>
        </el-form-item>
        <el-button type="primary" @click="handleLogin">登录</el-button>
      </el-form>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import { setUserInfo } from "@/store/user";
import { setRoutes } from "@/router/index.js";
import rolesMap from "@/router/rolesMap.js";

const router = useRouter();

// 和表单做数据双向绑定
const loginForm = ref({
  username: "",
  password: "",
});

// 登录对应的方法
const handleLogin = () => {
  // 验证用户名密码
  let role = "";
  if (
    loginForm.value.username === "admin" &&
    loginForm.value.password === "admin"
  ) {
    role = "admin";
  } else if (
    loginForm.value.username === "teacher" &&
    loginForm.value.password === "teacher"
  ) {
    role = "teacher";
  } else if (
    loginForm.value.username === "student" &&
    loginForm.value.password === "student"
  ) {
    role = "student";
  } else {
    alert("用户名密码错误");
    return;
  }
  //设置用户信息
  setUserInfo({
    name: loginForm.value.username,
    role,
  });
  // 设置对应的路由
  setRoutes(role);

  // 找到第一个路由
  const routes = rolesMap[role] || [];
  console.log("routes", routes);
  const firstRoute = routes.length > 0 ? routes[0].path : "/";
  console.log("firstRoute", firstRoute);
  localStorage.setItem("activeRoute", firstRoute);
  localStorage.setItem("logined", true);
  router.push(firstRoute);
};
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f5f5;
}

.title {
  text-align: center;
  font-weight: 200;
}

.el-form {
  width: 300px;
  padding: 20px;
  background-color: white;
  border-radius: 10px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  text-align: center;
}
</style>
vue
<template>
  <el-container>
    <!-- 管理系统头部 -->
    <el-header>
      <!-- 左侧logo -->
      <el-col :span="23">
        <img class="logo" src="../assets/logo.png" />
        <span class="sysTitle">智能云端学生平台</span>
      </el-col>
      <!-- 右侧用户头像 -->
      <el-col :span="1">
        <!-- 头像下拉菜单 -->
        <el-dropdown>
          <span class="el-dropdown-link">
            <span class="el-dropdown-link userinfo-inner">
              <img
                src="https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"
              />
            </span>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>
                <span>用户信息</span>
              </el-dropdown-item>
              <el-dropdown-item>
                <span @click="loginoutHandle">退出登陆</span>
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-col>
    </el-header>
    <el-container>
      <!-- 左侧导航栏 -->
      <el-aside width="200px" class="el-aside">
        <el-menu :default-active="activeIndex" @select="handleSelect">
          <!-- 不能像之前一样写死,而是应该动态根据数据来生成 -->
          <!-- <el-menu-item key="/student" index="/student">
            <el-icon><User /></el-icon>
            <span>学生模块</span>
          </el-menu-item>
          <el-menu-item key="/teacher" index="/teacher">
            <el-icon><Suitcase /></el-icon>
            <span>教师模块</span>
          </el-menu-item>
          <el-menu-item key="/teaching" index="/teaching">
            <el-icon><DataBoard /></el-icon>
            <span>教学模块</span>
          </el-menu-item>
          <el-menu-item key="/course" index="/course">
            <el-icon><Collection /></el-icon>
            <span>课程模块</span>
          </el-menu-item> -->

          <el-menu-item
            v-for="route in filterdRoutes"
            :key="route.path"
            :index="route.path"
          >
            <el-icon><component :is="route.meta.icon" /></el-icon>
            <span>{{ route.meta.title }}</span>
          </el-menu-item>
        </el-menu>
      </el-aside>

      <!-- 右侧内容区域 -->
      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import { clearUserInfo } from "@/store/user";

const router = useRouter();
// 记录当前选中的菜单
const activeIndex = ref(localStorage.getItem("activeRoute") || "/");

const filterdRoutes = computed(() => {
  // 过滤出所有带有 meta.title 的路由
  // 这里主要演示针对获取到的路由做一个二次处理
  const routes = router.getRoutes();
  return routes.filter((item) => {
    return item.meta && item.meta.title;
  });
});

const handleSelect = (key) => {
  activeIndex.value = key;
  router.push(`${key}`); // 路由跳转
  localStorage.setItem("activeRoute", key);
};

const loginoutHandle = () => {
  // 退出登陆
  clearUserInfo();
  localStorage.setItem("activeRoute", null);
  localStorage.setItem("logined", false);
  router.push("/login");
};
</script>

<style scoped>
.el-container {
  height: 100vh;
}

.el-header {
  background: rgb(53, 68, 87);
  color: #c0ccda;
  display: flex;
  line-height: 60px;
  font-size: 24px;
  padding: 0 30px 0 0;
}
.el-aside {
  background: rgb(53, 68, 87);
  color: #fff;
}
.logo {
  width: 40px;
  float: left;
  margin: 10px 15px;
}
.sysTitle {
  font-size: 20px;
  font-weight: 100;
}
.userinfo-inner {
  cursor: pointer;
}
.userinfo-inner img {
  width: 40px;
  height: 40px;
  border-radius: 20px;
  margin: 10px 0px 10px 10px;
  float: right;
}
.el-aside {
  /* background: rgb(77, 94, 112); */
  color: #b3bcc5;
  line-height: 200px;
}

.el-main {
  background: #f1f2f7;
  color: #333;
  height: 100%;
  overflow: auto;
  padding-bottom: 100px;
}
.el-menu {
  background: rgb(53, 68, 87);
  border-right: none;
}
.el-menu-item {
  color: #c0c0c0;
}
.el-menu-item.is-active {
  /* color: #fff; */
  background-color: rgb(68, 88, 113);
}
.el-menu-item:hover {
  background-color: rgb(68, 88, 113);
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
  line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
  line-height: 320px;
}
.fade-enter-active,
.fade-leave-active {
  transition: all 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
js
import { createApp } from "vue";
import App from "./App.vue";
import router, { setRoutes } from "./router";
// 引入组件库
import ElementPlus from "element-plus";
// 引入组件库相关样式
import "element-plus/dist/index.css";

import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import { getUserInfo } from "./store/user";

const app = createApp(App);

// 引入图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}

//修复刷新路由丢失bug
const role = getUserInfo()?.role;
if (role) setRoutes(role);

app.use(router).use(ElementPlus).mount("#app");

-EOF-

MIT License