前端之vue 根据菜单自动生成路由(动态配置前端路由)

avatar
作者
猴君
阅读量:4

在需要权限控制的页面,往往存在根据用户来显示菜单的情况,单独根据用户类型判断显然不是很好,如果后面用户类型发生变化,项目修改维护可能就会比较麻烦,所以比较好的做法是根据后端返回的菜单动态生成页面路由,以达到完全权限控制的目的,并且若权限发生变化,仅需该配置数据即可

1.创建项目

首先用vue-cli3创建好项目

2.新建文件

创建好项目后,新建我们需要的文件。结构如图

在这里插入图片描述

下载相关依赖包 :element-ui(菜单样式用) 和 axios(获取菜单用)

npm i element-ui axios --save 

3.到main.js中

import Vue from 'vue' import App from './App.vue' import router from './router' import elementUi from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(elementUi)  Vue.config.productionTip = false  new Vue({   router,   render: h => h(App) }).$mount('#app') 

4.先把菜单组件写好,到menu.vue中

这里使用element-ui的el-menu组件

<template>   <div class="menulist-inner">     <el-menu             default-active="/project"             :default-openeds="openedMenu"             background-color="rgba(44,55,71,1)"             text-color="#A7BAC6"             active-text-color="#FFFFFF"             @select="select"             unique-opened             @open="open">       <el-submenu :index="menu.path" v-for="(menu,index) in menus" :key="index">         <template #title>           <div class="menuTitlBox">             <div class="rowBtween menuTitle">               <div class="rowStart">                 <div class="iconBox" style="margin-right:10px;">                   <i v-if="/icon/.test(menu.icon)" class="iconfont" :class="menu.icon" style="                   color:#A7BAC6;font-size:16px;"></i>                                    </div>                 <span class="font16" :class="{whiteText:(!menu.children || !menu.children.length) && new RegExp(menu.path).test($route.path)}">{{menu.label}}</span>               </div>             </div>             <div class="firstMenuBk" v-if="(!menu.children || !menu.children.length) && new RegExp(menu.path).test($route.path)"></div>           </div>         </template>         <div class="menuItemBox rowEnd"              v-for="(cMenu,cIndex) in menu.children"              :key="cIndex"              :class="{activeMenu:RegExp(cMenu.path).test($route.path)}"              @click="select(cMenu)"         >           <span class="rowStart font14">{{cMenu.label}}</span>         </div>       </el-submenu>     </el-menu>   </div> </template>  <script>     import {mapState}from 'vuex'     export default {         name: "Menu",         data() {             return{                 menuMinHeight:0,//                 openedMenu:[],//展开的菜单              }         },         computed:{             ...mapState(['menus'])         },         created(){             let pathArr=this.$route.path.split('/')             this.openedMenu=[`/${pathArr[1]}`]         },         async mounted() {          },         methods: {             //中菜单             select(cMenu){                 // console.log(cMenu)                 let {routeName}=cMenu                 this.$router.push({                     name:routeName                 })             },             //菜单展开             open(index,indexPath){                 // console.log(indexPath)                 this.openedMenu=indexPath                 let menu=this.menus.filter(item=>item.path===index)                 // console.log(menu)                 let routeMenuInfo={}                 let time=0                 if(!menu[0] || !menu[0].children || !menu[0].children.length){                     routeMenuInfo=menu[0]                 }else{                     routeMenuInfo=menu[0].children[0]                     time=500                 }                 let {path,}=routeMenuInfo                 // console.log(path)                 setTimeout(()=>{                     this.$router.push(path)                 },time)             },          }     } </script> <style lang="scss" scoped>   .menulist-inner{     min-height:calc(100vh - 50px);     .menuItemBox{       height:36px;       &:hover{         background:rgba(62,70,120,.4);         &>span{           color: #fff;         }       }       span{         color:#ffffff;         flex-wrap:nowrap;         width:150px;         cursor:pointer;       }     }     .activeMenu{       background:rgba(62,70,120,1);     }   }   .menuTitlBox{     .menuTitle{       position: relative;       z-index: 10;     }     .firstMenuBk{       position: absolute;       left:0;       top:50%;       width:200px;       height:56px;       background:#3e4678;       -webkit-transform: translateY(-50%);       -moz-transform: translateY(-50%);       -ms-transform: translateY(-50%);       -o-transform: translateY(-50%);       transform: translateY(-50%);     }     .whiteText{       color:#ffffff;     }   }  </style> <style>   .el-submenu{     position: relative;   }   .el-menu{     border:none !important;   }   .el-submenu__icon-arrow{     width:0;     height:0;     color:transparent;   }   .el-submenu__icon-arrow:before{     content:'' !important;   }   .el-submenu__icon-arrow:before{     content:'' !important;   }   .el-submenu__title{     background:rgba(44,55,71,1) !important;     margin-bottom:5px;     height:46px !important;     line-height: 46px !important;   }    .el-menu.el-menu--inline{     background-color: rgba(0,0,0,.2) !important;   }    .menulist-inner .menuItemBox.rowEnd span{     color: #A7BAC6;   }   .menulist-inner .menuItemBox.rowEnd.activeMenu span{     color: #fff;   } </style> 

5.注册全局组件

将menu组件注册为全局组件,方便需要的地方直接引入
到global-components.js文件中

/*  全局组件引入注册   */  import Menu from "../global-components/menu" //菜单  export default {     install(Vue) {         Vue.component('Menu',Menu)     } }; 

6.到router文件夹写好路由模块

6.1 base-router.js中写好我们需要的固定的路由
/**  * 页面上固定路由  */ import Layout from '../views/layout/layout' import Login from '../views/login/login' import Loading from '../views/loading/loading' const routes = [     {         path: '/',         redirect: '/loading',     },     {         path: '/layout',         component:Layout,         redirect: '/loading',     },     {         path: '/login',         name: 'Login',         component: Login     },     {         path: '/loading',         name: 'Loading',         component: Loading     }, ] export default routes 
6.2 lm-router.js中写动态配置路由的方法
import {setUserRoutesData} from "../utils/global-methods";  const RouterPlugin = function() {     this.$router = null     this.$store = null } RouterPlugin.install = function(router, store) {     this.$router = router     this.$store = store     this.$router.$lmRouter = {         // 全局配置         safe: this,         // 动态路由         formatRoutes:function (routes) {             let routers=setUserRoutesData(routes)             this.safe.$router.addRoutes(routers)             return routers         }     } } export default RouterPlugin 
6.3 index.js中写路由入口
import Vue from 'vue' import VueRouter from 'vue-router' import baseRoutes from './base-router' import store from '../store' import LmRouter from './lm-router'   Vue.use(VueRouter)  const createRouter = () => {     return new VueRouter({         scrollBehavior (to, from, savedPosition) {             if (savedPosition) {                 return savedPosition             } else {                 return {                     x: 0,                     y: to.meta.savedPosition || 0                 }             }         },         routes: [...baseRoutes],         mode:'history',//     }) } let router = createRouter() /** resetRouter函数用于重置路由,每一次动态配置路由之前要先重置路由 **/ export function resetRouter() {     const newRouter = createRouter()     router.matcher = newRouter.matcher // reset router     LmRouter.install(router, store) } /** 当用户刷新页面时,路由数据会丢失,如果已经登录,需要重新渲染路由。渲染的路由数据在登录时存在浏览器本地 **/ if(sessionStorage.getItem('hasLogin')){     let sessionMenus=localStorage.getItem('menus')     sessionMenus=sessionMenus ? JSON.parse(sessionMenus) : []     let sessionRoutes=localStorage.getItem('userRoutes')     LmRouter.install(router, store)     sessionRoutes=sessionRoutes ? JSON.parse(sessionRoutes) : []     router.$lmRouter.formatRoutes(sessionRoutes, true)     store.dispatch('setUserRoutes',sessionRoutes)     store.dispatch('setMenus',sessionMenus) }  router.beforeEach((to, from, next) => {     let hasLogin = sessionStorage.getItem('hasLogin')     // console.log(to, from)     if(to.name===from.name){         return     }     console.log(hasLogin)     if(to.name==='Login'){         sessionStorage.clear()         localStorage.clear()         store.state.userRoutes=[]         store.state.menus=[]         next({replace:true})         resetRouter()         return     }     if (!hasLogin) {         next({path: '/login',replace:true})         return     }     next()  }) /**  * 解决element-ui点击同一个菜单报错  * @type {VueRouter.push|*}  */ const originalPush = VueRouter.prototype.push VueRouter.prototype.push = function push(location) {     return originalPush.call(this, location).catch(err => err) } export default router 

7.容器页和加载页

大部分有二级菜单的页面,父级页面通常只需要路由跳转功能,代码一致,为了精简代码,我们新建通用的路由容器页面layout.vue,二级菜单路由页面经layout页面跳转

为避免用户在浏览器直接输入、粘贴路径时,或者直接访问页面根路径时找不到路由(因为用户可能已经登录了,但这是没有已经登录界面),我们新建loading.vue,当用户粘贴路径,或者访问根路径时,在loading.vue中渲染路由

7.1 layout.vue
<!--Layout--> <template>     <div>         <header class="header">头部</header>         <div class="main">             <div class="aside">                 <Menu/>             </div>             <div class="mainRight columnStart">                 <router-view></router-view>             </div>         </div>     </div> </template>  <script>     export default {         name: 'Layout',     } </script> 
7.2 loading.vue
<!--加载页面--> <template>     <div class="loadingBox">         <div class="loadingContentbox columnCenter">             <i class="el-icon-loading gray999"></i>             <div class="gray999 font16">页面加载中...</div>         </div>      </div> </template>  <script>      import {reqUserRoutes} from "../../api/common";     import {setUserRoutesData,getAndFilterMenus,} from "../../utils/global-methods";      export default {         name: 'Loading',         data() {             return {}         },         computed: {},         created(){             let hasLogin = sessionStorage.getItem('hasLogin')             if(!hasLogin){                 this.$router.replace('/login')             }else{                 let menus=localStorage.getItem('menus')                 let userRoutes=localStorage.getItem('userRoutes')                 userRoutes=userRoutes ? JSON.parse(userRoutes) : []                 menus=menus ? JSON.parse(menus) : []                 if(!menus.length || !userRoutes.length){                     this.getMenuRoutes()                     return                 }                 let userInfo=getUserInfoFromLocalStorage()                 this.$store.dispatch('setUserInfo',userInfo)                 let {permissions=[]}=userInfo                 this.$store.dispatch('setPermissions',permissions)                 this.$router.replace((userRoutes[0] && userRoutes[0].path) ? userRoutes[0].path : '/404')             }         },         methods: {             //获取菜单和路由             async getMenuRoutes(){                 let userRoutes=await reqUserRoutes()                 console.log(userRoutes)                 let menus=getAndFilterMenus(JSON.parse(JSON.stringify(userRoutes)))                 if(!menus.length){                     this.$router.replace('/login')                     return                 }                 localStorage.setItem('userRoutes',JSON.stringify(userRoutes))                 localStorage.setItem('menus',JSON.stringify(menus))                 userRoutes=setUserRoutesData([...userRoutes])                 this.$store.dispatch('setUserRoutes',userRoutes)                 this.$store.dispatch('setMenus',menus)                 this.$router.addRoutes([...userRoutes])                 this.$router.replace((userRoutes[0] && userRoutes[0].path) ? userRoutes[0].path : '/404')             }         },      } </script>  <style scoped lang="scss">     .loadingBox{         .loadingContentbox{             position: absolute;             left:50%;             top:50%;             -webkit-transform: translate(-50%,-50%);             -moz-transform: translate(-50%,-50%);             -ms-transform: translate(-50%,-50%);             -o-transform: translate(-50%,-50%);             transform: translate(-50%,-50%);             .el-icon-loading{                 font-size: 40px;                 margin-bottom:10px;             }         }     } </style> 

8.写筛选菜单和路由的方法

utils,global-methods.js中写菜单筛选和路由数据方法
首先我们假设后端返回的数据为如下结构(仅列出关键字段)

[   {     label:'人员管理',     path:'/person',     isLeftMenu:1,     routeName:'Person',     isContainer:1,     children:[      {         label:'人员列表',         path:'/person/person-list',         routeName:'PersonList',          isLeftMenu:1,          component:'/person/person-list',     },      {         label:'新增人员',         path:'/person/person-add',         routeName:'PersonAdd',          component:'/person/person-add',          isLeftMenu:0      }     ]   },     {       label:'订单管理',       path:'/order',       isLeftMenu:1,       routeName:'Order',        component:'/order/order',       isContainer:0,     } ] 

其中 label 为菜单显示标题,path 是路由路径,isLeftMenu用于区分是否为菜单,这里isLeftMenu=1表示是菜单,routeName路由名,isContainer用于区分路由是否经过容器组件layout,这里isContainer=1表示使用layout容器,component用于指定组件引入的路径
根据上面的数据结构,我们新建好对应得vue文件
结构如下图:

在这里插入图片描述

// 公共函数模块,用import引用 // 根据日期时间值获取字符串各是日期 import Layout from '../views/layout/layout'  //获取并筛选菜单 export const getAndFilterMenus=(menus)=> {     // console.log(menus)     menus=hanldeChildAppRoute(menus)     for(let i=0;i<menus.length;i++){         delete menus[i].component         //只有leLeftMeu=1的是菜单         if(!parseInt(menus[i].isLeftMenu)){             menus.splice(i,1)             i--         }         if(menus[i] && menus[i].children){             getAndFilterMenus(menus[i].children)         }     }     return menus } //处理用户路由数据 export const setUserRoutesData=(routes,isChild)=>{     // console.log(routes)     !isChild && (routes=hanldeChildAppRoute(routes))     for(let i=0;i<routes.length;i++){         let {component,isContainer,routeName,keepAlive}=routes[i]         // console.log(routes[i])         //如果指定使用路由容器组件layout,即isContainer=1,那么路由的component值就是Layout组件         if(parseInt(isContainer)){             routes[i].component=Layout         }else{         //不使用容器组件的,根据component用Import动态引入             routes[i].component= () => import( `../views${component}`)         }         routes[i].name=routeName         /**         如果有子路由,那就用递归继续生成路由         **/         if((routes[i].children instanceof Array) && routes[i].children.length){             setUserRoutesData(routes[i].children,isChild)         }     }     return routes } 

9. 登录成功后生成路由

最后看下登录页,我们将在登录成功后获取菜单数据,然后动态生成页面路由,并且通过Vue路由的addRoutes方法将路由添加到VueRouter中

<template>     <div class="login-wrap">         <div class="loginContent">             <div class="rowBtween loginFormBox">                 <el-form :model="loginForm" :rules="rules" ref="loginForm" class="loginForm">                     <el-row class="loginTitleBox">                         <span class="loginTitle font20">登录/Login</span>                     </el-row>                     <el-row>                         <lm-form-item-col :span="24" v-model.number="loginForm.username" prefix-icon="el-icon-user" placeholder="请输入用户名" prop="username" width="300"/>                     </el-row>                     <el-row>                         <lm-form-item-col :span="24" type="password" v-model="loginForm.password" prefix-icon="el-icon-lock" placeholder="请输入登录密码" prop="password" width="300"/>                     </el-row>                     <div class="rowCenter loginBtnBox" >                         <div class="loginBtn rowCenter font16" @click="submitForm">                             <span>登录</span>                             <div v-if="showLoading">                                 <span>中</span>                                 <i class="el-icon-loading"></i>                             </div>                         </div>                     </div>                 </el-form>             </div>         </div>     </div> </template> <script>     import {mapState} from 'vuex'     import axios from 'axios'     import {reqUserRoutes} from "../../api/common";     import {setUserRoutesData,getAndFilterMenus,} from "../../utils/global-methods";     import {resetRouter} from '../../router'     import baseRoutes from '../../router/base-router'     export default {         data() {             return {                 loginForm: {},                 rules: {                     username: [ { required: true, message: '请输入用户名', trigger: 'blur' } ],                     password: [ { required: true, message: '请输入密码', trigger: 'blur' }, ],                 },//规则                 showLoading:false,//是否显示加载             }         },         methods: {             //登录             async submitForm() {                 sessionStorage.clear()//清除所有缓存                 localStorage.clear()                 await this.$refs.loginForm.validate()                 this.showLoading=true                 axios({                     url:`/login`,                     method:'POST',                     data:this.loginForm                 }).then(async response=>{                     console.log(response)                     if(response){                         let userRoutes=await reqUserRoutes()                         localStorage.setItem(userRoutes,JSON.stringify(userRoutes))                         userRoutes=setUserRoutesData(JSON.parse(JSON.stringify(userRoutes)))                         console.log(userRoutes)                         this.$store.dispatch('setUserRoutes',JSON.parse(JSON.stringify(userRoutes)))                         let menus=getAndFilterMenus([...baseRoutes,...JSON.parse(JSON.stringify(userRoutes))])                         localStorage.setItem(menus,JSON.stringify(menus))                         //重置路由                         resetRouter()                         // console.log(router)                         //通过addRoutes方法添加路由                         this.$router.addRoutes([...userRoutes])                         // console.log(router)                         将菜单存到vuex                         this.$store.dispatch('setMenus',menus)                         if(!menus.length || !userRoutes.length){                             return                         }                         setTimeout(()=>{                             router.replace(path)                         },500)                     }                 }).catch(error=>{                     console.error(error)                     this.showLoading=false                 })              },         },     }; </script> <style lang="scss" scoped>     .login-wrap{         position: relative;         background:#111111;         height:100vh;         overflow: hidden;         .loginContent{             .loginFormBox{                 position: absolute;                 left:50%;                 top:50%;                 -webkit-transform: translate(-50%,-50%);                 -moz-transform: translate(-50%,-50%);                 -ms-transform: translate(-50%,-50%);                 -o-transform: translate(-50%,-50%);                 transform: translate(-50%,-50%);                 .loginForm{                     width:300px;                     padding:20px;                     .loginTitleBox{                         margin-bottom:36px;                         .loginTitle{                             color:#ffffff;                          }                     }                      .loginBtnBox{                         width:100%;                         margin-top:5vh;                         .loginBtn{                             width:100%;                             height:40px;                             background:linear-gradient(90deg,rgba(15,70,193,1),rgba(0,147,168,1));                             border-radius:4px;                             color:#ffffff;                             cursor:pointer;                         }                     }                 }             }          }     }  </style> <style>     .login-wrap .el-input{         border-bottom: 1px solid;         border-image: -webkit-linear-gradient(90deg,rgba(1,233,189,1) 0%,rgba(0,124,222,1) 100%) 30 30;         border-image: -moz-linear-gradient(90deg,rgba(1,233,189,1) 0%,rgba(0,124,222,1) 100%) 30 30;         border-image: linear-gradient(90deg,rgba(1,233,189,1) 0%,rgba(0,124,222,1) 100%) 30 30;     }     .login-wrap .el-input__inner{         background:transparent;         -webkit-border-radius: 0;         -moz-border-radius: 0;         border-radius: 0;         border:none;         color:#ffffff;     }     .login-wrap .el-input-group__append{         padding:0 !important;         border:none !important;         background:transparent !important;     }  </style> 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!