用户角色不同 看到菜单不同 目前用户密码默认都是6个1

image.png
super_admin 角色默认全部菜单

image.png

image.png
image.png

image.png

4-1 修改src/permission.ts

路由导航里权限验证

先判断是否 有token 如果有 判断目前有没有角色,没有就根据token去获取用户信息包含角色信息 然后根据用户角色获取权限菜单 然后在根据权限菜单 筛选asyncRoutes 注册相应的权限路由 并渲染返回的菜单
src/permission.ts

  1. import router from '@/router'
  2. import nProgress from 'nprogress'
  3. import 'nprogress/nprogress.css' // progress bar style
  4. import { getToken } from './utils/auth'
  5. import store from '@/store'
  6. import { ElMessage } from 'element-plus'
  7. nProgress.configure({ showSpinner: false })
  8. const whiteList = ['/login'] // 白名单
  9. router.beforeEach(async (to) => {
  10. nProgress.start()
  11. const hasToken = getToken()
  12. if (hasToken) { // 有token代表已登录
  13. if (to.path === '/login') {
  14. nProgress.done()
  15. return {
  16. path: '/',
  17. replace: true
  18. }
  19. } else {
  20. try {
  21. const hasRoles = store.getters.roles && store.getters.roles.length > 0
  22. if (hasRoles) {
  23. nProgress.done()
  24. return true
  25. }
  26. // 无用户信息和角色信息 就请求获取
  27. const roles = await store.dispatch('user/getUserInfo')
  28. // 该用户未分配角色 进行异常提示
  29. if (!roles || roles.length === 0) {
  30. throw new Error('该用户未分配角色')
  31. }
  32. // 获取权限路由
  33. const accessRoutes = await store.dispatch('permission/generateRoutes')
  34. // 动态注册路由
  35. accessRoutes.forEach(router.addRoute)
  36. // 触发重定向
  37. return to.fullPath
  38. } catch (error) { // 登录失败处理
  39. // 移除token重新登录
  40. await store.dispatch('user/resetToken')
  41. ElMessage.error('登录失败:' + (error.message || 'Has Error'))
  42. nProgress.done()
  43. return `/login?redirect=${to.path}`
  44. }
  45. }
  46. } else {
  47. if (whiteList.includes(to.path)) {
  48. nProgress.done()
  49. return true
  50. }
  51. nProgress.done()
  52. return {
  53. path: '/login',
  54. query: {
  55. redirect: to.path,
  56. ...to.query
  57. }
  58. }
  59. }
  60. })
  61. router.afterEach(() => {
  62. nProgress.done()
  63. })

4-2 permissions store

src/store/modules/permission.ts

  1. import { Module, MutationTree, ActionTree } from 'vuex'
  2. import { RouteRecordRaw } from 'vue-router'
  3. import store, { IRootState } from '../index'
  4. import { asyncRoutes } from '../../router/index'
  5. import { MenuData } from './menu'
  6. import path from 'path'
  7. // 生成路由路径数组
  8. const generateRoutePaths = (menus: Array<MenuData>): string[] => {
  9. return menus.map(menu => menu.path)
  10. }
  11. // 白名单
  12. const whiteList = ['/:pathMatch(.*)*']
  13. // 生成可访问路由表
  14. const generateRoutes = (routes: Array<RouteRecordRaw>, routePaths: string[], basePath = '/') => {
  15. const routerData: Array<RouteRecordRaw> = []
  16. routes.forEach(route => {
  17. const routePath = path.resolve(basePath, route.path)
  18. if (route.children) { // 先看子路由 是否有匹配上的路由
  19. route.children = generateRoutes(route.children, routePaths, routePath)
  20. }
  21. // 如果当前路由子路由 数量大于0有匹配上 或 paths中包含当面路由path 就需要把当前父路由添加上
  22. if (routePaths.includes(routePath) || (route.children && route.children.length >= 1) || whiteList.includes(routePath)) {
  23. routerData.push(route)
  24. }
  25. })
  26. return routerData
  27. }
  28. const filterAsyncRoutes = (menus: Array<MenuData>, routes: Array<RouteRecordRaw>) => {
  29. // 生成要匹配的路由path数组
  30. const routePaths = generateRoutePaths(menus)
  31. // 生成匹配path的路由表
  32. const routerList = generateRoutes(routes, routePaths)
  33. return routerList
  34. }
  35. // 定义state类型
  36. export interface IPermissionState {
  37. routes: Array<RouteRecordRaw>;
  38. accessRoutes: Array<RouteRecordRaw>;
  39. }
  40. // mutations类型
  41. type IMutations = MutationTree<IPermissionState>
  42. // actions类型
  43. type IActions = ActionTree<IPermissionState, IRootState>
  44. // 定义state
  45. const state: IPermissionState = {
  46. routes: [],
  47. accessRoutes: []
  48. }
  49. // 定义mutation
  50. const mutations: IMutations = {
  51. SET_ROUTES(state, data: Array<RouteRecordRaw>) {
  52. state.routes = data
  53. },
  54. SET_ACCESS_ROUTES(state, data: Array<RouteRecordRaw>) {
  55. state.accessRoutes = data
  56. }
  57. }
  58. // 定义actions
  59. const actions: IActions = {
  60. generateRoutes({ dispatch }, type?: number) { // 1 针对菜单排序更新
  61. return new Promise((resolve, reject) => {
  62. let accessedRoutes: Array<RouteRecordRaw> = []
  63. if (store.getters.roleNames.includes('super_admin')) { // 超级管理员角色
  64. accessedRoutes = asyncRoutes
  65. dispatch('menu/getAllMenuListByAdmin', null, { root: true })
  66. resolve(accessedRoutes)
  67. } else { // 根据角色过滤菜单
  68. const roles = store.getters.roleIds
  69. dispatch('menu/getAccessByRoles', roles, { root: true }).then(menus => {
  70. if (type !== 1) { // 菜单重新排序 不需要再过次滤路由
  71. accessedRoutes = filterAsyncRoutes(menus, asyncRoutes)
  72. }
  73. resolve(accessedRoutes)
  74. }).catch(reject)
  75. }
  76. })
  77. }
  78. }
  79. // 定义user module
  80. const permission: Module<IPermissionState, IRootState> = {
  81. namespaced: true,
  82. state,
  83. mutations,
  84. actions
  85. }
  86. export default permission

4-5 修改路由表

路由表之前 数组 合并导出的路由 现在不需要了

image.png
/src/router/index.ts

  1. import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
  2. import Layout from '@/layout/index.vue'
  3. // 看作是异步获取路由
  4. export const asyncRoutes: Array<RouteRecordRaw> = [
  5. {
  6. path: '/documentation',
  7. component: Layout, // 布局组件作为一级路由
  8. redirect: '/documentation/index',
  9. name: 'DocumentationLayout',
  10. children: [
  11. {
  12. path: 'index',
  13. name: 'Documentation',
  14. component: () => import(/* webpackChunkName: "documentation" */ '@/views/documentation/index.vue'),
  15. meta: {
  16. title: 'Documentation',
  17. icon: 'documentation',
  18. hidden: false, // 菜单栏不显示
  19. // 路由是否缓存 没有这个属性或false都会缓存 true不缓存
  20. noCache: true
  21. }
  22. }
  23. ]
  24. },
  25. {
  26. path: '/async',
  27. component: Layout,
  28. redirect: '/async/index',
  29. name: 'AsyncLayout',
  30. children: [
  31. {
  32. path: 'index',
  33. name: 'Async',
  34. component: () => import(/* webpackChunkName: "async" */ '@/views/async.vue'),
  35. meta: {
  36. title: '动态路由',
  37. icon: 'guide'
  38. // 当guide路由激活时高亮选中的是 documentation/index菜单
  39. // activeMenu: '/documentation/index'
  40. }
  41. }
  42. ]
  43. },
  44. {
  45. path: '/guide',
  46. component: Layout,
  47. redirect: '/guide/index',
  48. name: 'GuideLayout',
  49. meta: {
  50. title: 'GuideLay',
  51. icon: 'guide'
  52. },
  53. children: [
  54. {
  55. path: 'index',
  56. name: 'Guide',
  57. component: () => import(/* webpackChunkName: "guide" */ '@/views/guide/index.vue'),
  58. meta: {
  59. title: 'Guide',
  60. icon: 'guide'
  61. // 当guide路由激活时高亮选中的是 documentation/index菜单
  62. // activeMenu: '/documentation/index'
  63. }
  64. },
  65. {
  66. path: 'guide2',
  67. name: 'Guide2',
  68. component: () => import(/* webpackChunkName: "guide" */ '@/views/guide/index.vue'),
  69. meta: {
  70. title: 'Guide2',
  71. icon: 'guide'
  72. // 当guide路由激活时高亮选中的是 documentation/index菜单
  73. // activeMenu: '/documentation/index'
  74. }
  75. },
  76. {
  77. path: 'guide3',
  78. name: 'Guide3',
  79. component: () => import(/* webpackChunkName: "guide" */ '@/views/guide/index.vue'),
  80. meta: {
  81. title: 'Guide3',
  82. icon: 'guide'
  83. // 当guide路由激活时高亮选中的是 documentation/index菜单
  84. // activeMenu: '/documentation/index'
  85. }
  86. }
  87. ]
  88. },
  89. {
  90. path: '/system',
  91. component: Layout,
  92. redirect: '/system/user',
  93. name: 'SystemLayout',
  94. meta: {
  95. title: 'System',
  96. icon: 'lock',
  97. alwaysShow: true // 根路由始终显示 哪怕只有一个子路由
  98. },
  99. children: [
  100. {
  101. path: 'menu',
  102. name: 'Menu Management',
  103. component: () => import(/* webpackChunkName: "menu" */ '@/views/system/menu/index.vue'),
  104. meta: {
  105. title: 'Menu Management',
  106. hidden: false,
  107. breadcrumb: false
  108. }
  109. },
  110. {
  111. path: 'role',
  112. name: 'Role Management',
  113. component: () => import(/* webpackChunkName: "role" */ '@/views/system/role/index.vue'),
  114. meta: {
  115. title: 'Role Management',
  116. hidden: false
  117. }
  118. },
  119. {
  120. path: 'user',
  121. name: 'User Management',
  122. component: () => import(/* webpackChunkName: "user" */ '@/views/system/user/index.vue'),
  123. meta: {
  124. title: 'User Management'
  125. }
  126. }
  127. ]
  128. },
  129. { // 外链路由
  130. path: '/external-link',
  131. component: Layout,
  132. children: [
  133. {
  134. path: 'https://www.baidu.com/',
  135. redirect: '/',
  136. meta: {
  137. title: 'External Link',
  138. icon: 'link'
  139. }
  140. }
  141. ]
  142. },
  143. { // 404一定放在要在最后面
  144. path: '/:pathMatch(.*)*',
  145. redirect: '/404',
  146. meta: {
  147. hidden: true
  148. }
  149. }
  150. ]
  151. export const constantRoutes: Array<RouteRecordRaw> = [
  152. {
  153. path: '/',
  154. component: Layout,
  155. redirect: '/dashboard',
  156. name: 'DashboardLayout',
  157. children: [
  158. {
  159. path: 'dashboard',
  160. name: 'Dashboard',
  161. component: () => import(/* webpackChunkName: "dashboard" */ '@/views/dashboard/index.vue'),
  162. meta: {
  163. title: 'Dashboard',
  164. // icon: 'dashboard'
  165. icon: 'el-icon-platform-eleme',
  166. affix: true // 固定显示在tagsView中
  167. }
  168. }
  169. ]
  170. },
  171. {
  172. path: '/redirect',
  173. component: Layout,
  174. meta: {
  175. hidden: true
  176. },
  177. name: 'Redirect',
  178. children: [
  179. { // 带参数的动态路由正则匹配
  180. // https://next.router.vuejs.org/zh/guide/essentials/route-matching-syntax.html#%E5%8F%AF%E9%87%8D%E5%A4%8D%E7%9A%84%E5%8F%82%E6%95%B0
  181. path: '/redirect/:path(.*)', // 要匹配多级路由 应该加*号
  182. component: () => import('@/views/redirect/index.vue')
  183. }
  184. ]
  185. },
  186. {
  187. path: '/login',
  188. name: 'Login',
  189. component: () => import('@/views/login/index.vue')
  190. },
  191. {
  192. path: '/profile',
  193. component: Layout,
  194. redirect: '/profile/index',
  195. name: 'ProfileLayout',
  196. children: [
  197. {
  198. path: 'index',
  199. name: 'Profile',
  200. component: () => import('@/views/profile/index.vue'),
  201. meta: {
  202. hidden: true,
  203. title: '个人中心'
  204. }
  205. }
  206. ]
  207. },
  208. {
  209. path: '/401',
  210. component: Layout,
  211. name: '401Layout',
  212. children: [
  213. {
  214. path: '',
  215. component: () => import('@/views/error-page/401.vue'),
  216. meta: {
  217. title: '401',
  218. icon: '404',
  219. hidden: true
  220. }
  221. }
  222. ]
  223. },
  224. {
  225. path: '/404',
  226. component: () => import('@/views/error-page/404.vue'),
  227. meta: {
  228. hidden: true // 404 hidden掉
  229. }
  230. }
  231. ]
  232. // export const routes = [
  233. // ...constantRoutes,
  234. // ...asyncRoutes
  235. // ]
  236. export const routes = constantRoutes
  237. const router = createRouter({
  238. history: createWebHashHistory(),
  239. routes
  240. })
  241. export default router

4-4 修改侧边栏

从权限store里获取菜单

image.png
image.png

src/layout/components/Sidebar/index.vue

  1. <template>
  2. <div class="sidebar-wrapper">
  3. <logo v-if="showLogo" :collapse="isCollapse" />
  4. <scroll-panel>
  5. <el-menu
  6. class="sidebar-container-menu"
  7. :class="{
  8. 'sidebar-show-logo': showLogo
  9. }"
  10. mode="vertical"
  11. :default-active="activeMenu"
  12. :background-color="scssVariables.menuBg"
  13. :text-color="scssVariables.menuText"
  14. :active-text-color="themeColor"
  15. :collapse="isCollapse"
  16. :collapse-transition="true"
  17. >
  18. <sidebar-item
  19. v-for="route in menuRoutes"
  20. :key="route.path"
  21. :item="route"
  22. :base-path="route.path"
  23. />
  24. </el-menu>
  25. </scroll-panel>
  26. </div>
  27. </template>
  28. <script lang="ts">
  29. import { defineComponent, computed } from 'vue'
  30. import { useRoute } from 'vue-router'
  31. import variables from '@/styles/variables.scss'
  32. import SidebarItem from './SidebarItem.vue'
  33. import { useStore } from '@/store'
  34. import Logo from './Logo.vue'
  35. import ScrollPanel from '@/components/ScrollPanel.vue'
  36. export default defineComponent({
  37. name: 'Sidebar',
  38. components: {
  39. Logo,
  40. SidebarItem,
  41. ScrollPanel
  42. },
  43. setup() {
  44. const route = useRoute()
  45. const store = useStore()
  46. // 根据路由路径 对应 当前激活的菜单
  47. const activeMenu = computed(() => {
  48. const { path, meta } = route
  49. // 可根据meta.activeMenu指定 当前路由激活时 让哪个菜单高亮选中
  50. if (meta.activeMenu) {
  51. return meta.activeMenu
  52. }
  53. return path
  54. })
  55. // scss变量
  56. const scssVariables = computed(() => variables)
  57. // 展开收起状态 稍后放store 当前是展开就让它收起
  58. const isCollapse = computed(() => !store.getters.sidebar.opened)
  59. // 获取权限菜单
  60. const menuList = computed(() => store.state.menu.authMenuTreeData)
  61. // 渲染路由
  62. const menuRoutes = computed(() => [...menuList.value])
  63. // 获取主题色
  64. const themeColor = computed(() => store.getters.themeColor)
  65. // 是否显示logo
  66. const showLogo = computed(() => store.state.settings.sidebarLogo)
  67. return {
  68. // ...toRefs(variables), // 不用toRefs原因 缺点variables里面变量属性来源不明确
  69. scssVariables,
  70. isCollapse,
  71. activeMenu,
  72. menuRoutes,
  73. themeColor,
  74. showLogo
  75. }
  76. }
  77. })
  78. </script>
  79. <style lang="scss" scoped>
  80. .sidebar-wrapper {
  81. .sidebar-container-menu {
  82. height: 100vh;
  83. &.sidebar-show-logo { // 显示logo时
  84. // 100vh-50px
  85. height: calc(100vh - 50px);
  86. }
  87. }
  88. }
  89. </style>

image.png