手摸手,帶你用vue實現後臺管理權限系統及頂欄三級菜單顯示前端
權限路由思路:
根據用戶登陸的roles信息與路由中配置的roles信息進行比較過濾,生成能夠訪問的路由表,並經過router.addRoutes(store.getters.addRouters)動態添加可訪問權限路由表,從而實現左側和頂欄菜單的展現。vue
實現步驟:webpack
1.在router/index.js中,給相應的菜單設置默認的roles信息;ios
以下:給"權限設置"菜單設置的權限爲:meta:{roles: ['admin', 'editor']},及不一樣的角色均可以看到; 給其子菜單"頁面權限",設置權限爲:meta:{roles: ['admin']},及表示只有"admin"能夠看到該菜單; 給其子菜單"按鈕權限"設置權限爲:meta:{roles: ['editor']},及表示只有"editor"能夠看到該菜單。git
2.經過router.beforeEach()和router.afterEach()進行路由過濾和權限攔截;github
代碼以下:web
// permission judge function function hasPermission(roles, permissionRoles) { if (roles.indexOf('admin') >= 0) return true // admin permission passed directly if (!permissionRoles) return true return roles.some(role => permissionRoles.indexOf(role) >= 0) } const whiteList = ['/login'] // 不重定向白名單 router.beforeEach((to, from, next) => { NProgress.start() // 設置瀏覽器頭部標題 const browserHeaderTitle = to.meta.title store.commit('SET_BROWSERHEADERTITLE', { browserHeaderTitle: browserHeaderTitle }) // 點擊登陸時,拿到了token並存入了vuex; if (getToken()) { /* has token*/ if (store.getters.isLock && to.path !== '/lock') { next({ path: '/lock' }) NProgress.done() } else if (to.path === '/login') { next({ path: '/' }) // 會匹配到path:'',後面的path:'*'尚未生成; NProgress.done() } else { if (store.getters.roles.length === 0) { store.dispatch('GetInfo').then(res => { // 拉取用戶信息 const roles = res.roles store.dispatch('GenerateRoutes', { roles }).then(() => { // 根據roles權限生成可訪問的路由表 router.addRoutes(store.getters.addRouters) // 動態添加可訪問權限路由表 next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 }) }).catch((err) => { store.dispatch('FedLogOut').then(() => { Message.error(err || 'Verification failed, please login again') next({ path: '/' }) }) }) } else { // 沒有動態改變權限的需求可直接next() 刪除下方權限判斷 ↓ if (hasPermission(store.getters.roles, to.meta.roles)) { next()// } else { next({ path: '/401', replace: true, query: { noGoBack: true }}) } } } } else { if (whiteList.indexOf(to.path) !== -1) { next() } else { // 點擊退出時,會定位到這裏 next('/login') NProgress.done() } } }) router.afterEach(() => { NProgress.done() // 結束Progress setTimeout(() => { const browserHeaderTitle = store.getters.browserHeaderTitle setTitle(browserHeaderTitle) }, 0) })
用戶點擊登陸以後的業務邏輯分析:ajax
一、用戶調取登陸接口,獲取到token,進行路由跳轉到首頁;vuex
二、經過路由導航鉤子router.beforeEach((to,from,next)=>{})函數肯定下一步的跳轉邏輯,以下:npm
2.一、用戶已經登陸成功並返回token值; 2.1.一、lock 鎖屏場景; 2.1.二、用戶從新定位到登陸頁面; 2.1.三、根據用戶是否有roles信息,進行不一樣的業務邏輯,以下: (1)、初始狀況下,用戶roles信息爲空; 經過store.dispatch('GetInfo')調取接口,獲取用戶信息; 獲取到roles信息後,將roles,name,avatar保存到vuex; 同時,經過store.dispatch('GenerateRoutes', { roles })去從新過濾和生成路由,並將從新生成以後的權限路由'routes'保存到vuex; 最後,經過router.addRoutes()合併路由表; 若是在獲取用戶信息接口時,出現錯誤,則調取store.dispatch('FedLogOut')接口,返回到login頁面; 用戶FedLogOut以後,須要狀況vuex和localStorage中的token信息; (2)、用戶已經擁有roles信息; 點擊頁面路由,經過roles權限判斷 hasPermission(); 若是用戶有該路由權限,直接跳轉對應的頁面;若是沒有權限,則跳轉至401提示頁面; 2.二、用戶沒有獲取到token值; 2.2.一、若是設置了白名單用戶,則直接跳轉到相應的頁面;反之,則跳轉至登陸頁面;
三、經過路由導航鉤子函數router.afterEach(() => {}),作收尾工做,以下:
3.一、NProgress.done() // 結束Progress 3.二、獲取到title並設置title;
詳細代碼,請參考src/permission.js
四、權限演示說明
測試帳號:
(1). username: admin,password: 123456;admin擁有最高權限,能夠查看全部的頁面和按鈕;
(2). username: editor,password: 123456;editor只有被賦予權限的頁面和按鈕才能夠看到;
如圖所示,在完成通常後臺系統所具備的二級導航菜單功能以後,我發現其實不少的後臺管理系統都有三級導航菜單,可是若是都把三級菜單放到左側菜單作階梯狀排列,就會顯得比較緊湊,所以我以爲把全部的三級菜單放到頂部是一個不錯的選擇。
開發需求:點擊左側菜單,找到其對應的菜單(頂欄菜單)排放於頂部導航欄;
開發步驟:
一、 定義頂部導航組件topMenu.vue;
經過element-ui,NavMenu 導航菜單來進行頂部菜單的展現,注意頂欄和側欄設置的區別;同時將其引用於頭部組件headNav.vue中;
二、定義頂欄路由數據router/topRouter.js;
格式以下:
export const topRouterMap = [ { 'parentName':'infoShow', 'topmenulist':[ { path: 'infoShow1', name: 'infoShow1', meta: { title: '我的信息子菜單1', icon: 'fa-asterisk', routerType: 'topmenu' }, component: () => import('@/page/fundList/moneyData') } ] }, { 'parentName':'chinaTabsList', 'topmenulist':[ { path:'chinaTabsList1', name:'chinaTabsList1', meta:{ title:'區域投資子菜單1', icon:'fa-asterisk', routerType:'topmenu' }, component: () => import('@/page/fundList/moneyData') } ] } ]
定義topRouterMap爲路由總數組;經過parentName來與左側路由創建聯繫;經過topmenulist表示該頂欄路由的值;經過meta.routerType的值爲"topmenu"或"leftmenu"來區分是頂欄路由,仍是左側路由;
三、 準備headNav.vue中渲染數據;
思路:點擊左側菜單,須要顯示頂部對應的菜單。由於左側菜單要和頂部菜單創建聯繫。咱們知道導航菜單在用戶登陸時,會根據用戶的role信息進行權限過濾;那麼,在過濾權限路由數據以前,咱們能夠經過addTopRouter()將全部的三級菜單進行過濾添加,添加完成以後,繼續進行角色過濾,能夠保證將不具有權限的頂部菜單也過濾掉。
// 經過循環過濾,生成新的二級菜單 function addTopRouter(){ asyncRouterMap.forEach( (item) => { if(item.children && item.children.length >= 1){ item.children.forEach((sitem) => { topRouterMap.forEach((citem) => { if(sitem.name === citem.parentName){ let newChildren = item.children.concat(citem.topmenulist); item.children = newChildren; } }) }) } }) return asyncRouterMap; }
四、點擊左側菜單過濾路由並顯示對應數據;
在組件topMenu.vue中,用戶默認進來或者點擊左側菜單,觸發setLeftInnerMenu()函數,以下:
setLeftInnerMenu(){ if(this.$route.meta.routerType == 'leftmenu'){ // 點擊的爲 左側的2級菜單 this.$store.dispatch(''ClickLeftInnerMenu, {'name':this.$route.name} ); }else{ // 點擊頂部的菜單 this.$store.dispatch('ClickTopMenu', {'title':this.$route.meta.title} ); } }
經過當前路由this.$route.meta.routerType的值判斷,用戶是點擊頂部菜單仍是左側菜單。若是點擊頂部菜單,經過this.$store觸發異步動做'ClickLeftInnerMenu'並傳遞參數'name',vuex中經過state.topRouters = filterTopRouters(state.routers,data)過濾當前路由信息;代碼以下:
// 獲取到當前路由對應頂部子菜單 function filterTopRouters(data){ let topRouters = topRouterMap.find((item)=>{ return item.parentName === data.name }) if(!mutils.isEmpty(topRouters)){ return topRouters.topmenulist; } }
topMenu.vue中,經過 computed:{ ...mapGetters(['topRouters'])}進行對應頂部路由數據的展現。用戶每次點擊左側菜單時,頂部路由都進行了從新賦值並渲染,保證了數據的準確性。
五、頂部菜單完善;
當頂部菜單的數據量過大時,咱們須要設置橫向滾動條並設置滾動條的樣式。
如圖:
Easy Mock介紹:
詳細使用方法,包含新建項目,基礎語法,數據佔位符,Swagger等介紹和使用,請參考詳細文檔
easy-mock,在本項目中的使用:
根據項目須要,建立的接口有:用戶登陸接口:"/user/login";獲取用戶信息接口:"/user/info";用戶登出接口:"/user/logout";獲取全部用戶列表接口:"/user/getUserList";如圖:
登陸接口在easy-mock端編寫的邏輯以下:
{ code: function({ _req }) { if (_req.body.username === "admin" || _req.body.username === "editor" && _req.body.password === "123456") { return 200 } else { return -1 } }, message: function({ _req }) { if (_req.body.username !== "admin" || _req.body.username !== "editor") { return "帳號或密碼有誤!" } }, data: function({ _req }) { if (_req.body.username == "admin" && _req.body.password === "123456") { return { code: 0, roles: ['admin'], token: 'admin', introduction: '我是超級管理員', name: 'Super Admin' } } else if (_req.body.username === 'editor' && _req.body.password === "123456") { return { code: 0, roles: ['editor'], token: 'editor', introduction: '我是編輯', name: 'Normal Editor' } } else { return "帳號或密碼有誤!" } } }
module.exports = merge(prodEnv, { NODE_ENV: '"development"', API_BASE_URL: '"https://easy-mock.com/mock/5cd03667adb0973be6a3d8d1/api"', })
3.接口封裝實例;以下:
import request from '@/utils/axios' export function login(username, password) { return request({ url: process.env.API_BASE_URL+'/user/login', method: 'post', data: { username, password } }) }
使用背景:
在使用easy-mock模擬數據的過程當中,發現其對錶格固定數據不能實現增刪改等功能,於是選擇了使用mockjs;
介紹及功能:
Mock.js是一款模擬數據生成器,旨在幫助前端攻城師獨立於後端進行開發,幫助編寫單元測試。提供瞭如下模擬功能:
1.根據數據模板生成模擬數據,經過mockjs提供的方法,你能夠輕鬆地創造大量隨機的文本,數字,布爾值,日期,郵箱,連接,圖片,顏色等.
2.模擬 Ajax 請求,生成並返回模擬數據,mockjs能夠進行強大的ajax攔截.能判斷請求類型,獲取到url,請求參數等.而後能夠返回mock的假數據,或者你本身編好的json文件.功能強大易上手.
3.基於 HTML 模板生成模擬數據
mockjs在本項目中使用:
npm install mockjs --save-dev
2.建立mock文件夾結構並定義相關的功能模塊;如圖:
import Mock from 'mockjs' import tableAPI from './money' // 設置全局延時 沒有延時的話有時候會檢測不到數據變化 建議保留 Mock.setup({ timeout: '300-600' }) // 資金相關 Mock.mock(/\/money\/get/, 'get', tableAPI.getMoneyList) Mock.mock(/\/money\/remove/, 'get', tableAPI.deleteMoney) Mock.mock(/\/money\/batchremove/, 'get', tableAPI.batchremoveMoney) Mock.mock(/\/money\/add/, 'get', tableAPI.createMoney) Mock.mock(/\/money\/edit/, 'get', tableAPI.updateMoney)
mockjs/money.js,則定義相關的函數,實現模擬數據的業務邏輯,好比資金流水數據的增刪改查等;數據的生成規則請參照mockjs官網文檔,上面有詳細的語法說明;
3.在main.js中引入定義好的mockjs;以下:
import './mockjs' //引用mock
4.mockjs,api接口封裝;
src/api/money.js中,進行了統一的接口封裝,在頁面中調用對應函數,便可獲取到相應的模擬數據。代碼以下:
import request from '@/utils/axios' export function getMoneyIncomePay(params) { return request({ url: '/money/get', method: 'get', params: params }) } export function addMoney(params) { return request({ url: '/money/add', method: 'get', params: params }) }
5.組件中,接口調用,獲取數據,渲染頁面;
項目說明:
小愛ADMIN是徹底開源免費的管理系統集成方案,能夠直接應用於相關後臺管理系統模板;不少重點地方都作了詳細的註釋和解釋。若是你也同樣喜歡前端開發,歡迎加入咱們的討論/學習羣,羣內能夠提問答疑,分享學習資料; 歡迎加入答疑qq羣:602515030