• 前端实现菜单&按钮级权限


    核心思想就是通过登录请求此用户对应的权限菜单,然后跳转首页,触发全局前置导航守卫,在全局导航守卫中通过 addRoute 添加动态路由进去。addRoute有一个需要注意的地方,就是我们添加完动态路由后,地址栏上立即访问添加的动态路由,它不会跳转,需要我们手动触发下,push或者replace都可以进行触发。但是用在全局前置导航守卫中,写法又不太一样,可以参照官网说明

    动态路由 | Vue Router (vuejs.org)

    这是我自己练习的项目文件分布

    我这里把主要文件的代码都贴出来

    首先是 pinia 文件,这里面主要存储了 token 与 动态路由 数据。我做了一个持久化存储

    stores 里面的 counter.ts

    1. import { ref } from 'vue'
    2. import { defineStore } from 'pinia'
    3. export const useUserStore = defineStore('user', () => {
    4. // token
    5. const token = ref('')
    6. // 动态路由
    7. const dynamicRoutes = ref([])
    8. // 设置token
    9. const setToken = (t: string) => { token.value = t }
    10. // 设置动态路由
    11. const setDynamicRoutes = (r: any) => { dynamicRoutes.value = r }
    12. // 清空token
    13. const clearToken = () => { token.value = '' }
    14. // 清空动态路由
    15. const clearDynamicRoutes = () => { dynamicRoutes.value = [] }
    16. return {
    17. token,
    18. dynamicRoutes,
    19. setToken,
    20. setDynamicRoutes,
    21. clearToken,
    22. clearDynamicRoutes
    23. }
    24. }, {
    25. persist: {
    26. enabled: true // true 表示开启持久化保存
    27. }
    28. })

    路由信息  router 文件夹里面的 index.ts 

    1. import { createRouter, createWebHistory } from 'vue-router'
    2. import HomeView from '../views/HomeView.vue'
    3. const router = createRouter({
    4. history: createWebHistory(import.meta.env.BASE_URL),
    5. routes: [
    6. {
    7. path: '/',
    8. name: 'home',
    9. component: HomeView,
    10. // 重定向的首页
    11. redirect: '/test'
    12. },
    13. {
    14. path: '/login',
    15. name: 'login',
    16. component: () => import('../views/Login.vue')
    17. }
    18. ]
    19. })
    20. export default router

    登录页面:views 文件夹里面的 Login.vue

    1. <script setup lang="ts">
    2. import { useUserStore } from '@/stores/counter'
    3. import { useRouter } from 'vue-router'
    4. const userStore = useUserStore()
    5. const router = useRouter()
    6. const submit = () => {
    7. // 模拟登录
    8. setTimeout(() => {
    9. // 存储token和动态路由
    10. userStore.setToken('Bearen Xxx')
    11. userStore.setDynamicRoutes([
    12. {
    13. path: '/about',
    14. name: 'about',
    15. children: [
    16. { path: '/about/music', name: 'music', component: 'Music' },
    17. { path: '/about/movie', name: 'movie', component: 'Movie' },
    18. {
    19. path: '/about/parent',
    20. name: 'parent',
    21. children: [
    22. { path: '/about/parent/child', name: 'parent', component: 'Parents' }
    23. ]
    24. },
    25. ]
    26. },
    27. { path: '/test', name: 'test', component: 'Test' },
    28. ])
    29. // 跳转路由。触发全局前置导航守卫
    30. router.replace('/')
    31. })
    32. }
    33. script>

    点击登录页面的登录按钮后,会跳转到  '/'  ,路由变化了,就会触发全局前置导航守卫,全局前置导航守卫我写在了 mian.ts 文件中

    自定义指令可以忽略,那是我做按钮级权限用的(可以看我上一篇文章)。

    需要注意的地方就是 hasAddAliveRoutes 这个变量,记录是否已经添加过动态路由了,如果添加过了,就赋值为true。在去其他路由的时候,就不会重新添加动态路由了。还有一个作用就是,刷新的时候,hasAddAliveRoutes会重新变为false,会重新添加一下动态路由,防止刷新路由丢失

    1. import { createApp } from 'vue'
    2. import { createPinia } from 'pinia'
    3. import App from './App.vue'
    4. import router from './router'
    5. import ElementPlus from 'element-plus'
    6. import 'element-plus/dist/index.css'
    7. import piniaPersist from 'pinia-plugin-persist'
    8. import { useUserStore } from './stores/counter'
    9. const app = createApp(App)
    10. const pinia = createPinia()
    11. app.use(pinia)
    12. app.use(router)
    13. app.use(ElementPlus)
    14. pinia.use(piniaPersist)
    15. // 处理动态路由,下面的全局前置导航守卫会用到
    16. const getNewRoutes = (routes: any) => {
    17. let res = routes.map((i: any) => {
    18. return {
    19. ...i,
    20. // 动态添加component,有就添加,没有就不添加 (带有子级的路由是没有component的)
    21. ...(i.component && { component: () => import(`./views/${i.component}.vue`) }),
    22. // 再把内层的处理一下
    23. ...(i.children && { children: getNewRoutes(i.children) }),
    24. }
    25. })
    26. return res
    27. }
    28. // 是否添加过动态路由 这里的标识作用:刷新的时候,会变为false,然后就会重新添加动态路由,防止路由丢市的
    29. let hasAddAliveRoutes = false
    30. router.beforeEach((to, from, next) => {
    31. if (to.path === '/login') {
    32. next()
    33. } else {
    34. let token = useUserStore().token
    35. if (token) { // 存在token
    36. // 判断是否有动态路由添加了
    37. if (!hasAddAliveRoutes) { // 没有添加,则添加动态路由并进行触发
    38. // 对pinia中的动态路由进行处理,component字段只是一个文件名称,不是我们想要的动态引入,所以需要修改
    39. let arr = getNewRoutes(useUserStore().dynamicRoutes)
    40. console.log('处理之后的路由', arr)
    41. // 开始添加动态路由
    42. for (let i = 0; i < arr.length; i++) {
    43. router.addRoute('home', arr[i])
    44. }
    45. // 修改添加动态路由的状态
    46. hasAddAliveRoutes = true
    47. // 触发添加的动态路由
    48. next(to.fullPath)
    49. } else { // 已经添加了,则直接通过
    50. next()
    51. }
    52. } else { // 不存在token
    53. next('/login')
    54. }
    55. }
    56. })
    57. // 假装此用户在tset页面只有 改 和 查 的按钮权限
    58. let buttonAuth = [
    59. { path: '/test', btn: ['check', 'change'] }
    60. ]
    61. // 自定义指令: 控制按钮级权限
    62. app.directive('permission', {
    63. mounted(el, binding) {
    64. // console.log(el) // 元素
    65. // console.log(binding.value) // 值
    66. // console.log(binding.arg) // 路由
    67. // 遍历按钮数组,根绝当前的路由找到这一项的按钮权限
    68. let btnAuth = buttonAuth.find(item => item.path === binding.arg)
    69. if (btnAuth) { // 找到了
    70. // 不包含此按钮权限就移除按钮
    71. !btnAuth.btn.includes(binding.value) && el.parentNode.removeChild(el)
    72. }
    73. }
    74. })
    75. app.mount('#app')

    然后就会跳往首页了,也就是 HomeView.vue 页面,这个文件里面用到了递归组件MenuTree.vue 

    这个递归组件就是用来递归菜单的,多少级菜单都能进行展示

    1. <script lang="ts" setup>
    2. import { Setting } from '@element-plus/icons-vue'
    3. import MenuTree from './MenuTree.vue'
    4. import { useUserStore } from '@/stores/counter'
    5. import { ref } from 'vue'
    6. import { useRouter } from 'vue-router'
    7. const userStore = useUserStore()
    8. const router = useRouter()
    9. // 路由
    10. const routes = ref(userStore.dynamicRoutes)
    11. // 是否折叠
    12. const isCollapse = ref(false)
    13. // 宽度
    14. const aside = ref('200px')
    15. // 菜单展示方式
    16. const mode = ref('vertical')
    17. // 点击折叠菜单
    18. const collapse = () => {
    19. isCollapse.value = !isCollapse.value
    20. aside.value = isCollapse.value ? '60px' : '200px'
    21. }
    22. // 点击改变布局
    23. const changeMode = () => {
    24. mode.value = mode.value === 'vertical' ? 'horizontal' : 'vertical'
    25. }
    26. // 点击退出
    27. const reback = () => {
    28. userStore.clearToken()
    29. userStore.clearDynamicRoutes()
    30. router.replace('/login')
    31. }
    32. script>
    33. <style scoped>
    34. .layout-container-demo .el-header {
    35. position: relative;
    36. background-color: var(--el-color-primary-light-7);
    37. color: var(--el-text-color-primary);
    38. }
    39. .layout-container-demo .el-aside {
    40. color: var(--el-text-color-primary);
    41. background: var(--el-color-primary-light-8);
    42. /* 新加的过度效果 */
    43. transition: width 0.15s;
    44. -webkit-transition: width 0.15s;
    45. -moz-transition: width 0.15s;
    46. -webkit-transition: width 0.15s;
    47. -o-transition: width 0.15s;
    48. }
    49. .layout-container-demo .el-menu {
    50. border-right: none;
    51. }
    52. .layout-container-demo .el-main {
    53. padding: 0;
    54. }
    55. .layout-container-demo .toolbar {
    56. display: inline-flex;
    57. align-items: center;
    58. justify-content: center;
    59. height: 100%;
    60. right: 20px;
    61. }
    62. style>

    递归组件MenuTree.vue