前端vue如何根據(jù)菜單自動生成路由(動態(tài)配置前端路由)
前言
在需要權(quán)限控制的頁面,往往存在根據(jù)用戶來顯示菜單的情況,單獨(dú)根據(jù)用戶類型判斷顯然不是很好,如果后面用戶類型發(fā)生變化,項(xiàng)目修改維護(hù)可能就會比較麻煩,所以比較好的做法是根據(jù)后端返回的菜單動態(tài)生成頁面路由,以達(dá)到完全權(quán)限控制的目的,并且若權(quán)限發(fā)生變化,僅需該配置數(shù)據(jù)即可
1.創(chuàng)建項(xiàng)目
首先用vue-cli3創(chuàng)建好項(xiàng)目
2.新建文件
創(chuàng)建好項(xiàng)目后,新建我們需要的文件。結(jié)構(gòu)如圖
下載相關(guān)依賴包 :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中寫動態(tài)配置路由的方法
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, // 動態(tài)路由 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函數(shù)用于重置路由,每一次動態(tài)配置路由之前要先重置路由 **/ export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher // reset router LmRouter.install(router, store) } /** 當(dāng)用戶刷新頁面時,路由數(shù)據(jù)會丟失,如果已經(jīng)登錄,需要重新渲染路由。渲染的路由數(shù)據(jù)在登錄時存在瀏覽器本地 **/ 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點(diǎn)擊同一個菜單報錯 * @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.容器頁和加載頁
大部分有二級菜單的頁面,父級頁面通常只需要路由跳轉(zhuǎn)功能,代碼一致,為了精簡代碼,我們新建通用的路由容器頁面layout.vue,二級菜單路由頁面經(jīng)layout頁面跳轉(zhuǎn)
為避免用戶在瀏覽器直接輸入、粘貼路徑時,或者直接訪問頁面根路徑時找不到路由(因?yàn)橛脩艨赡芤呀?jīng)登錄了,但這是沒有已經(jīng)登錄界面),我們新建loading.vue,當(dāng)用戶粘貼路徑,或者訪問根路徑時,在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中寫菜單篩選和路由數(shù)據(jù)方法
首先我們假設(shè)后端返回的數(shù)據(jù)為如下結(jié)構(gòu)(僅列出關(guān)鍵字段)
[ { 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 為菜單顯示標(biāo)題,path 是路由路徑,isLeftMenu用于區(qū)分是否為菜單,這里isLeftMenu=1表示是菜單,routeName路由名,isContainer用于區(qū)分路由是否經(jīng)過容器組件layout,這里isContainer=1表示使用layout容器,component用于指定組件引入的路徑
根據(jù)上面的數(shù)據(jù)結(jié)構(gòu),我們新建好對應(yīng)得vue文件
// 公共函數(shù)模塊,用import引用 // 根據(jù)日期時間值獲取字符串各是日期 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 //只有l(wèi)eLeftMeu=1的是菜單 if(!parseInt(menus[i].isLeftMenu)){ menus.splice(i,1) i-- } if(menus[i] && menus[i].children){ getAndFilterMenus(menus[i].children) } } return menus } //處理用戶路由數(shù)據(jù) 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{ //不使用容器組件的,根據(jù)component用Import動態(tài)引入 routes[i].component= () => import( `../views${component}`) } routes[i].name=routeName /** 如果有子路由,那就用遞歸繼續(xù)生成路由 **/ if((routes[i].children instanceof Array) && routes[i].children.length){ setUserRoutesData(routes[i].children,isChild) } } return routes }
9. 登錄成功后生成路由
最后看下登錄頁,我們將在登錄成功后獲取菜單數(shù)據(jù),然后動態(tài)生成頁面路由,并且通過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' }, ], },//規(guī)則 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>
附:一些常見問題
1. 用 require 而不用 import
我們在根據(jù)字符串生成路由組件的時候,要用 require 而不是 import ,否則會報以下錯誤信息:
2. 側(cè)邊欄消失
在點(diǎn)擊動態(tài)生成的路由時,頁面可以正常跳轉(zhuǎn),但是測試邊不見了。是因?yàn)?動態(tài)生成的路由沒有作為 Layout 組件的子組件,應(yīng)該添加到 Layout 組件的 children 數(shù)組中。
3. 頁面無內(nèi)容
在點(diǎn)擊二級菜單的時候,頁面是空白的,沒有任何內(nèi)容。以上面案例為例,我們只需要在對應(yīng)組件的父目錄下,新建 index.vue 文件,寫入 <router-view /> 即可。
4. 二級菜單展示不全
這個問題不屬于動態(tài)路由的問題,elementui 框架中,如果子菜單只有一個,那么就不會生成多級菜單的形式。
總結(jié)
到此這篇關(guān)于前端vue如何根據(jù)菜單自動生成路由的文章就介紹到這了,更多相關(guān)前端vue菜單自動生成路由內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
elementUI中el-upload文件上傳的實(shí)現(xiàn)方法
ElementUI的組件支持多種事件鉤子,如http-request、before-upload和on-change,以實(shí)現(xiàn)自定義文件上傳處理,這篇文章主要介紹了elementUI中el-upload文件上傳的實(shí)現(xiàn)方法,需要的朋友可以參考下2024-11-11vue源碼解析computed多次訪問會有死循環(huán)原理
這篇文章主要為大家介紹了vue源碼解析computed多次訪問會有死循環(huán)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04Vue.js做select下拉列表的實(shí)例(ul-li標(biāo)簽仿select標(biāo)簽)
下面小編就為大家分享一篇Vue.js做select下拉列表的實(shí)例(ul-li標(biāo)簽仿select標(biāo)簽),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03Vue3中使用fetch實(shí)現(xiàn)數(shù)據(jù)請求的過程詳解
在現(xiàn)代前端開發(fā)中,數(shù)據(jù)請求是一個不可或缺的環(huán)節(jié),而在Vue3中,我們有許多方法可以進(jìn)行數(shù)據(jù)請求,其中使用fetch方法是一個非常常見的選擇,本文將詳細(xì)講解如何在Vue3中使用fetch來實(shí)現(xiàn)數(shù)據(jù)請求,需要的朋友可以參考下2024-09-09詳解vue-cli與webpack結(jié)合如何處理靜態(tài)資源
本篇文章主要介紹了詳解vue-cli與webpack結(jié)合如何處理靜態(tài)資源,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09element-ui 中使用upload多文件上傳只請求一次接口
這篇文章主要介紹了element-ui 中使用upload多文件上傳只請求一次接口,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07解決vue elementUI中table里數(shù)字、字母、中文混合排序問題
這篇文章主要介紹了vue elementUI中table里數(shù)字、字母、中文混合排序問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01使用vue插件axios傳數(shù)據(jù)的Content-Type及格式問題詳解
這篇文章主要介紹了使用vue插件axios傳數(shù)據(jù)的Content-Type以及格式問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09