动态路由
- 动态参数路由
- /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):动态的移除路由,除了此方法移除路由,还有几种方式
通过添加一个名称冲突的路由。如果添加与现有路由名称相同的路由,会先删除旧路由,再添加新路由:
jsrouter.addRoute({ path: "/about", name: "about", component: About }); // 这将会删除之前已经添加的路由,因为他们具有相同的 name router.addRoute({ path: "/other", name: "about", component: Other });
通过调用 router.addRoute( ) 返回的回调函数,调用该函数后可以删除添加的路由。当路由没有名称时,这很有用。
jsconst 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-