vue2-模拟vue-element-admin手写路由按钮权限

1,035 阅读4分钟

screenshots.gif

vue

注意

扁平化路由'/travel/china',会没有travel父级节点的,就不会展示TravelPage组件

 // 嵌套路由
 const routes = [
   {
     path: '/travel', component: TravelPage,
     children: [
       { path: '/travel/america', component: TravelAmericaPage },
     ]
   },
   {
     path: '/travel/china', component: AboutPage
   }
 ];

image-20250118173440301.png

路由接口

后端采用node-express,这里配置的接口包括用户信息,路由信息

 const express = require('express');
 const router = express.Router();
 ​
 /* GET users listing. */
 router.get('/', (req, res, next) => {
   console.log(req.cookies);
   const data = req.cookies.USER === 'admin'
     ? {
         name: 'admin',
         role: 'admin',
         routes: [{
           path: '/',
           children: [
             {
               path: 'demo',
               name: 'demo',
 ​
               meta: {
                 sidebar: true,
                 menuName: 'demo'
               },
               component: 'demo/index.vue',
               children: [
                 {
                   path: 'access',
                   name: 'access',
 ​
                   component: 'demo/Access/index.vue',
                   meta: {
                     sidebar: true,
                     menuName: '权限',
                     button: {
                       'btn:createUser': 2, // 显示
                       'btn:editUser': 2, // 显示
                       'module:module1': 2// 显示
                     },
                     roles: ['admin', 'liming']
                   }
                 }
               ]
             }
           ]
         }]
       }
     : req.cookies.USER === 'liming' ? {
       name: 'liming',
       role: 'second',
       routes: [{
         path: '/',
         children: [
           {
             path: 'demo',
             name: 'demo',
 ​
             meta: {
               sidebar: true,
               menuName: 'demo'
             },
             component: 'demo/index.vue',
             children: [
               {
                 path: 'access',
                 name: 'access',
 ​
                 component: 'demo/Access/index.vue',
                 meta: {
                   sidebar: true,
                   menuName: '权限',
                   button: {
                     'btn:createUser': 1, // 禁用
                     'btn:editUser': 0, // 隐藏
                     'module:module1': 2// 显示
                   },
                   roles: ['admin', 'liming']
                 }
               }
             ]
           }
         ]
       }]
     }
       : {
           name: 'third',
           role: 'third',
           routes: []
         }
   res.send({
     code: 200,
     message: 'success',
     data: {
       ...data
     }
   });
 });
 ​
 module.exports = router;
 ​

路由权限

  • 静态路由(路由白名单):固定的路由。如 login 页面

  • 动态路由/权限路由

    1. 要求少可以通过用户角色筛选路由表
    2. 要求多可以直接返回路由表
store
  • state

       state: {
         commonMenu: [], // 菜单栏
         whiteRoutes, // 静态路由
         asyncRoutes: [], // 动态路由
         flatAsyncRoutes: [], // 扁平化动态路由
         routes: whiteRoutes, // 静态路由+动态路由
         registerRouteFresh: true // 动态路由注册状态,账号切换,路由需要更新
       }
    
  • mutations

       mutations: {
         SET_ROUTES: (state, routes) => {
           console.log('SET_ROUTES:', routes)
           state.routes = routes
         },
         // SET_PERMISSION会在登录成功的时候触发,更新路由表
         SET_PERMISSION: (state, data) => {
           state[data.type] = data.data
         }
       },
    
     //login.vue
     //登录成功后,改变动态路由注册状态
     store.commit('SET_PERMISSION', { type: 'registerRouteFresh', data: true });
    
  • actions

    这里配置的接口包括用户信息,路由信息

       actions: {
         async generateRoutes({ commit, state }) {
           return new Promise((resolve) => {
             (async () => {
               const { data: { role, name, routes: asyncRoutes } } = await userInfoApi()
               commit('change_role', {
                 role
               })// 修改角色
               commit('SET_USER_INFO', {
                 type: 'name',
                 data: name
               })// 修改用户信息
               console.log(state.whiteRoutes, 'state.whiteRoutes')
               state.routes = $lm.lodash.cloneDeep(state.whiteRoutes) // 初始化routes为静态路由,同时深拷贝防止影响静态路由
               filterAsyncRoutes(whiteRoutes, asyncRoutes, state.flatAsyncRoutes, state.routes, '')
               state.asyncRoutes = asyncRoutes
               state.commonMenu = state.routes[0].children // 配置菜单栏
               console.log('routes----', state.routes, state.whiteRoutes, asyncRoutes, commonMenu)
               resolve(state.routes)
             })()
           })
         }
       }
    
     //网上说后端给的动态路由组件地址不能包含@/views/
     export const loadView = (view) => {
       return () => import(`@/views/${view}`)
     }
     /**
      * 合并静态路和动态路由,并构建最终的路由列表。
      * @param {*} whiteRoutes 静态路由
      * @param {*} asyncRoutes 动态路由
      * @param {*} flatAsyncRoutes 扁平化动态路由
      * @param {*} routes 静态路由+动态路由
      * @param {*} path 扁平化动态路由父级路径
      * @returns
      */
     export function filterAsyncRoutes(whiteRoutes, asyncRoutes, flatAsyncRoutes, routes, path) {
       asyncRoutes.forEach((element, index) => {
         let pos = -1 // 静态和动态路由匹配索引
         const whiteRoutesItem = whiteRoutes.find((item, index) => {
           if (item.path === element.path) {
             pos = index
           }
           return item.path === element.path
         })
         if (pos === -1) {
           // TODO: 优化element还可能有children
           const componentPath = element.component
           const urlPath = `${path}/${element.path}`.replace('//', '') // 处理路径
           routes.push({ ...element, component: loadView(componentPath) })
           flatAsyncRoutes.push({ ...element, path: urlPath, component: loadView(componentPath) })
         } else if (whiteRoutesItem && whiteRoutesItem.children && element.children) {
           filterAsyncRoutes(whiteRoutesItem.children, element.children, flatAsyncRoutes, routes[pos].children, `${path}/${element.path}`)
         }
       })
       return routes
     }
    
router
  • 加载基础路由

     import Vue from 'vue';
     import VueRouter, { RouteConfig } from 'vue-router';
     import $lm from '@lm/shared/lib/src/utils';
     import store from '@/store';
     Vue.use(VueRouter); // 安装路由功能
     const isProd = process.env.NODE_ENV === 'production';
     // 基础路由
     const whiteRoutes: Array<RouteConfig> = store.getters.whiteRoutes;
     console.log(whiteRoutes, 'router:whiteRoutes');
     const createRouter = () =>
       new VueRouter({
         mode: 'history',
         base: window.__POWERED_BY_QIANKUN__
           ? '/qiankun/vue2-pc/' // 配置子应用的路由根路径
           : isProd
           ? '/vue2-pc/' // 单一项目下的访问路径
           : '/',
         routes: whiteRoutes,
       });
     const router: any = createRouter();
    
  • 解决编程式路由往同一地址跳转时会报错的情况

     // 解决编程式路由往同一地址跳转时会报错的情况
     const originalPush = VueRouter.prototype.push;
     const originalReplace = VueRouter.prototype.replace;
     ​
     // push
     // @ts-ignore
     VueRouter.prototype.push = function push(location: any, onResolve: any, onReject: any) {
       if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject);
       // @ts-ignore
       return originalPush.call(this, location).catch((err: any) => console.log(err));
     };
     ​
     // replace
     // @ts-ignore
     VueRouter.prototype.replace = function push(location: any, onResolve: any, onReject: any) {
       if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject);
       // @ts-ignore
       return originalReplace.call(this, location).catch((err: any) => err);
     };
    
  • 登录

    • 点击登录执行事件:更改 动态路由注册状态

      login(this.ruleForm).then(
        res => {
          Vue.ls.set('token', res.token);
          //路由表状态刷新
          this.$store.commit('SET_PERMISSION', { type: 'registerRouteFresh', data: true });
          this.$router.push('/');
        },
        err => {
          console.log(err);
          this.$message.error(err.message);
        }
      );
      
    • 登录后先清空动态路由(账号不同,路由权限不同),再执行 generateRoutes 获取动态路由

       /**
        * 全局全局前置守卫
        * to : 将要进入的目标路由对象
        * from : 即将离开的目标路由对象
        */
       router.beforeEach(async (to: any, from: any, next: any) => {
         console.log('router.beforeEach:', to, from, router.getRoutes(), store.getters.registerRouteFresh);
         // 设置当前页的title;
         document.title = to.meta.title || 'vue2-pc';
         if (to.path !== '/login' && !localStorage.getItem('token')) {
           // 如果没有登录,跳转到登录页
           next('/login');
         } else if (store.getters.registerRouteFresh && to.path !== '/login') {
           // 如果to找不到对应的路由那么他会再执行一次beforeEach((to, from, next))直到找到对应的路由,
           store.commit('SET_PERMISSION', { type: 'registerRouteFresh', data: false });
           // 获取路由
           const routes = await store.dispatch('generateRoutes');
           console.log('routes', routes);
           resetRouter(); // 重置路由信息
           routes.forEach((item: RouteConfig) => {
             router.addRoute(item);
           });
           // 获取路由配置
           console.log('getRoutes', router.getRoutes());
           // 解决登录或者刷新后路由找不到的问题:
           // 虽然to找不到对应的路由那么他会再执行一次beforeEach,但是登录或者刷新前路由表没有动态路由信息,那么to.name还是找不到对应的路由,最后会跳转到404页面
           // next({ ...to, replace: true }); // 解决刷新后路由失效的问题
           next(to.path); // 需要指向确切的地址
         } else {
           next();
         }
       });
      

image-20250119181140052.png

菜单权限

因为动态路由直接从后端给,所以不需要判断,从store里面获取commonMenu,并循环渲染出来就行

 <template>
   <el-menu
     @select="selectSiderBar"
     router
     :default-active="currentRouteInfo.path"
     class="app-menu"
     background-color="#292C45"
     text-color="#fff"
     active-text-color="#ffd04b"
   >
     <template v-for="item in routeList">
       <el-menu-item
         v-if="item.meta.sidebar && (!item.children || item.children.length === 0)"
         :key="item.path"
         :index="`/${item.path}`"
       >
         <i class="el-icon-menu"></i>
         <span slot="title">{{ item.meta.menuName }}</span>
       </el-menu-item>
       <el-submenu v-if="item.children && item.children.length !== 0" :key="item.path" :index="item.path">
         <template slot="title">
           <i class="el-icon-menu"></i>
           <span>{{ item.meta.menuName }}</span>
         </template>
         <el-menu-item-group v-if="item.children">
           <el-menu-item
             v-for="child in item.children"
             :key="`${item.path}/${child.path}`"
             :index="`/${item.path}/${child.path}`"
           >
             {{ child.meta.menuName }}
           </el-menu-item>
         </el-menu-item-group>
       </el-submenu>
     </template>
   </el-menu>
 </template>
 ​
 <script lang="ts">
 import { Component, Vue, PropSync, Watch } from 'vue-property-decorator';
 ​
 @Component({
   watch: {
     $route(to: any, from: any) {
       console.log(to,'to')
       window.sessionStorage.setItem('currentRouteInfo', JSON.stringify({ path: to.path, meta: to.meta }));
       this.$store.commit('setRouteInfo');
       this.$store.commit('setTagNav', { path: to.path, meta: to.meta });
     },
   },
   computed: {
     routeList() {
       return this.$store.getters.commonMenu;
     },
   },
 })
 export default class Sidebar extends Vue {
   @PropSync('collapse') isCollapse!: boolean;
   private readonly name = 'Sidebar';
 ​
   get currentRouteInfo() {
     return this.$store.getters.currentRouteInfo;
   }
 ​
   selectSiderBar() {
     console.log();
   }
 }
 </script>
 ​
 <style scoped>
 .el-menu {
   width: 220px;
   border-right: initial;
   height: calc(100vh - 60px);
   /* overflow-y: scroll; */
 }
 .el-menu-item.is-active {
   background: #3f4b99;
 }
 .el-aside {
   overflow: initial;
 }
 </style>
 ​

按钮权限

通过路由中 meta 的按钮权限控制

 meta: {
   button: {
   'btn:createUser': 0, //隐藏
   'btn:editUser': 1, //禁用
   'module:module1': 2 //启用
   },
 },

权限按钮

     <a-button :disabled="false" v-permission="{ type: 'btn:createUser', route: $route }"> 新建用户 </a-button>
     <a-button :disabled="false" v-permission="{ type: 'btn:editUser', route: $route }"> 编辑用户 </a-button>

v-permission指令

 import store from '@/store';
 ​
 function checkPermission(el, binding) {
   const { type, route } = binding.value;
   //权限按钮
   const mockButton = store.getters && store.getters.mockButton;
   if (route.meta.button[type] === 0) {
     el.disabled = true;
     el.setAttribute('disabled', true);
   } else if (route.meta.button[type] === 1) {
     el.style.display = 'none';
   } else {
     el.style.display = 'block';
     el.disabled = false;
   }
   throw new Error(`need roles! Like v-permission="['admin','editor']"`)
 }
 ​
 export default {
   inserted(el, binding) {
     checkPermission(el, binding);
   },
   update(el, binding) {
     checkPermission(el, binding);
   },
 };