菜单权限控制 ---> 不同权限的用户可访问的页面是不同的 可访问的页面--->路由
路由一般分为 常量路由(所有人都可以访问),动态路由(通过权限控制的路由),任意路由(当访问的路由不存在时,跳转到的页面)
本章主要介绍动态路由的两种处理方式
1. 前端对路由进行处理 2. 后端通过接口返回
这两种方式不一样的地方是在配置路由规则处不同
首先梳理一下路由权限的整体思路
1. 获取到该用户所拥有的动态路由
2. 通过route.addRoutes方法将获取到的动态路由添加到路由表中
3. 将获取到的动态路由和静态路由、任意路由组合存到vuex/pinia中,使用该数据循环渲染左侧菜单
第一步 获取到该用户所拥有的动态路由(两种方式不一样的地方)
前端处理:
一般登录成功后接口会返回该用户拥有哪些权限 例:menus: ['department', 'settings', 'permissions'] (此处list中的数据是路由的name,返回的具体内容和后端协商好就可以)
前端配置动态路由规则 在route文件夹下创建asyncRoutes.js文件
// 动态路由
export const asyncRoutes = [
// 组织架构
{
path: '/department',
component: Layout,
children: [{
path: '',
name: 'departments',
component: () => import('@/views/Department/index'),
meta: { title: '组织架构', icon: 'tree' }
}]
},
// 角色管理
{
path: '/setting',
component: Layout,
children: [{
path: '',
name: 'settings',
component: () => import('@/views/Setting/index'),
meta: { title: '角色管理', icon: 'setting' }
}]
},
// 员工管理
{
path: '/employee',
component: Layout,
children: [{
path: '',
name: 'employees',
component: () => import('@/views/Employee/index'),
meta: { title: '员工管理', icon: 'people' }
}]
},
// 权限点
{
path: '/permission',
component: Layout,
children: [{
path: '',
name: 'permissions',
component: () => import('@/views/Permission/index'),
meta: { title: '权限点管理', icon: 'lock' }
}]
},
// 工资管理
{
path: '/salary',
component: Layout,
children: [{
path: '',
name: 'salarys',
component: () => import('@/views/Salary/index'),
meta: { title: '工资管理', icon: 'money' }
}]
}
]
然后根据后端返回的list数据 做筛选,筛选出用户可以访问的动态路由(一般是在router.beforeEach中做处理)
const filterRoutes = asyncRoutes.filter(item => {
//res.data.roles.menus 就是 menus: ['department', 'settings', 'permissions']
return res.data.roles.menus.includes(item.children[0].name)
})
后端接口返回路由
上面两张图一般后端接口返回的格式(封装的获取异步路由的方法是按照第二张图的数据封装的)
import { RouteRecordRaw } from 'vue-router'
import { getRoutes } from './getRoutes'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('../views/**/*.vue')
console.log('modules', modules)
// import router from '@/router'
export const getDynamicRoute = (asyncRoute: any[]): RouteRecordRaw[] => {
const newRoute = asyncRoute.map((route) => {
const urlList = route.url.split('/')
const name = router.fullPath[router.fullPath.length - 1]
const item = {
name: name,
path: '/' + route.url,
component: '',
meta: {
icon: route.icon,
title: route.name,
},
children: [],
}
if (urlList.length === 1) {
item.component = Layout
} else {
if (urlList.length !== 2) {
item.component = modules[`../views/${route.url}.vue`]
}
}
if (route.children && route.children.length > 0) {
//这里是重点
item.children = getDynamicRoute(route.children)
}
return item
})
return newRoute
}
第二步 通过route.addRoutes方法将获取到的动态路由添加到路由表中
// 此部操作在获取到动态路由后写即可
//注意 router.addRoutes 方法是一个异步的方法,刷新页面时,不能立即生效,动态路由就没有添加到路由系统中去,所以会显示空白页面
router.addRoutes(filterRoutes)
//使用 next 再跳转一次页面,此时 addRoutes 方法就能生效了,就能访问到对应的页面了
next({
...to,
replace: true
})
第三步 将获取到的动态路由和静态路由、任意路由组合存到vuex/pinia中,使用该数据循环渲染左侧菜单(此处存到了vuex中)
store.commit('menu/setMenuList', filterRoutes) //执行vuex中的方法setMenuList //此行代码写在第二步操作下面即可
//此步是vuex中 将动态路由、静态路由、任意路由组合起来的方法
// 静态路由表 作为菜单的初始化数据
import { constantRoutes, anyRoutes } from '@/router'
export default {
namespaced: true,
state: () => ({
// 先以静态路由表作为菜单数据的初始值
menuList: [ ...constantRoutes ]
}),
mutations: {
// 修改路由表的 mutation 方法
setMenuList(state, filterRoutes) {
// filterAsyncRoutes 过滤之后的动态路由表
// 将动态路由、静态路由、任意路由组合起来
state.menuList = [...constantRoutes, ...filterRoutes, ...anyRoutes]
}
}
}
// 封住左侧菜单的组件
<template>
<!-- 该条路由规则中的 hidden,如果是 true 的话就不将改条路由规则渲染成一个导航菜单 -->
<div v-if="!item.hidden">
<!-- 仅有一个可显示的孩子 && (这个孩子不能有孩子 || 这个孩子没有可显示的孩子) && 该条路由总是需要显示的属性为 false -->
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<!-- 路由规则里有 meta 信息,才渲染成菜单,否则不渲染 -->
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<!-- 渲染没有子路由的菜单 -->
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<!-- 把 meta 信息中的 icon 和 title 传递给 item,组件 -->
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<!-- 渲染有子路由的菜单 -->
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<!-- 左侧菜单中 用来显示图标的是路由对象中meta字段里的icon 文案显示用的meta中的title -->
<!-- icon的渲染 使用名称去匹配 icons/svg/下的图标的名称-->
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<!-- 递归组件,如果子路由里面还有孩子,继续渲染它的子路由对应的菜单 -->
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
// 路由规则对象
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
// 基地址,也就是所有层的 父+子 的地址
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
// 筛选出该条路由下,所有 hidden 为 false 的子路由。也就是所有需要展示在导航菜单的子路由
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
// 拿到这个唯一的孩子
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
// 该条路由下,仅有一个需要显示的的子路由,返回 true,说明需要使用 el-menu-item 渲染成没有子菜单的菜单
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
// 没有要显示的孩子,就显示该路由自己
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
// 如果子路由的地址是外链,直接返回这个外链
if (isExternal(routePath)) {
return routePath
}
// 如果父路由的地址是外链,直接返回这个外链
if (isExternal(this.basePath)) {
return this.basePath
}
// 父子路由都不是外链,返回父路由地址 + 子路由地址
return path.resolve(this.basePath, routePath)
}
}
}
</script>
//使用左侧菜单组件
<template>
<div :class="{ 'has-logo': showLogo }">
<!-- 导航菜单上面的 logo -->
<logo v-if="showLogo" :collapse="isCollapse" />
<!-- element 滚动条 -->
<el-scrollbar wrap-class="scrollbar-wrapper">
<!-- 导航菜单组件 -->
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<!-- 菜单中的每一项 -->
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
// this.$router.options.routes 可以拿到完整的路由表(路由规则)数据
// console.log(this.$router.options.routes);
// this.$router.options.routes 只能拿到一开始就在 routes 选项中设置的路由,拿不到通过 addRoutes 方法添加的路由
// return this.$router.options.routes
return this.$store.state.menu.menuList
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
遇到的问题:
防止在同一浏览器中新登录的用户在使用浏览器回退功能看到上一次登录用户的浏览记录
解决:
在退出登录的时候需要重新创建一个新的路由实例,并使用新的路由实例覆盖老的路由实例
// reset 路由方法
export function resetRouter() {
// 得到一个全新的 router 实例对象
const newRouter = createRouter()
// 使用新的路由记录覆盖掉老的路由记录
router.matcher = newRouter.matcher // reset router
}
在退出登录时调用一下该方法即可
logout() {
// 退出登录时,清空路由记录
resetRouter()
}