使用Vue實現後臺管理系統的動態路由以及側邊欄選單

豪俠貓弦發表於2020-08-03
介紹及說明

1、背景介紹:最近因為公司的專案,對一個後臺管理系統進行前端重構,原專案是SSM架構的前後端沒有分離,前端用JSP和jQuery來寫的,在完成第一期之後,我強烈要求前後端分離,並使用vue來元件化開發。所以經過幾天的摸索完成了專案的整體搭建和基礎建設。
2、專案要求:因為原先的專案的選單都是後臺來動態生成的,所以用vue來寫也要從後臺獲取路由表,前端再處理成我們想要的路由格式,再完成側邊欄選單;並且前端頁面裡有一個模組專門來管理生成選單和許可權(也就是前端自定義一些選單資訊傳給後端,後端把資料存到資料庫,並根據角色許可權處理完資料再返回路由表給前端,前端就可以動態生成側邊欄選單了)。

實現過程

首先本專案是基於vue-element-admin的後臺管理模板( vue-element-admin),這個模板很多功能都有現成的,很方便,拉取下來之後就可以基於這個進行開發了,這個模板有很詳細的文件說明( vue-element-admin使用文件),裡面也有動態路由的整合方案,本專案選擇的就是路由全部通過後端來生成,前端來自定義需要的路由。
步驟如下:
1、使用者登入成功之後(登入的功能,模板裡也寫的很好,換成公司的登入介面就可以直接用),再根據使用者名稱獲取使用者的許可權,然後根據使用者許可權獲取後端的路由資料:
1)許可權處理和生成動態路由:src/permission.js檔案

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 進度條
import 'nprogress/nprogress.css' // 進度條樣式
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
// import { getRoutes } from './api/role'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/auth-redirect'] // 沒有重定向白名單

router.beforeEach(async(to, from, next) => {
// 開始進度條
NProgress.start()

// 設定頁面標題
document.title = getPageTitle(to.meta.title)

// 確定使用者是否已登入,獲取token
const hasToken = getToken()

if (hasToken) {
  if (to.path === '/login') {
    // 如果已登入,則重定向到主頁
    next({ path: '/' })
    NProgress.done()
  } else {
    // 確定使用者是否通過getInfo獲得了他的許可權角色
    const hasRoles = store.getters.roles && store.getters.roles.length > 0
    if (hasRoles) {
      next()
    } else {
      try {
        // 獲取使用者許可權資訊
        const { roles } = await store.dispatch('user/getInfo')

        // 根據角色生成可訪問路由對映
        const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
        // console.log(accessRoutes)

        // 動態新增可訪問路由
        router.options.routes = router.options.routes.concat(accessRoutes)
        router.addRoutes(accessRoutes)
        // hack方法 以確保addRoutes是完整的
        next({ ...to, replace: true })
        // console.log(router)
      } catch (error) {
        // 刪除令牌,進入登入頁面重新登入
        await store.dispatch('user/resetToken')
        Message.error(error || 'Has Error')
        next(`/login?redirect=${to.path}`)
        NProgress.done()
      }
    }
  }
} else {
  /* 沒有令牌*/

  if (whiteList.indexOf(to.path) !== -1) {
    // 去往免費的登入白名單
    next()
  } else {
    // 沒有訪問許可權的其他頁面被重定向到登入頁面
    next(`/login?redirect=${to.path}`)
    NProgress.done()
  }
}
})

router.afterEach(() => {
// finish progress bar
NProgress.done()
})	

2)獲取使用者許可權的程式碼:src/store/modules/user.js

// 獲取使用者許可權資訊
getInfo({ commit, state }) {
  return new Promise((resolve, reject) => {
    getInfo(state.code).then(response => {
      const { data } = response
      // console.log(data)
      if (!data) {
        reject('驗證失敗,請重新登入.')
      }

      let roles = []
      roles = roles.concat(data.usrGroupcode)

      const name = data.userName

      // // 角色必須是非空陣列
      if (!roles || roles.length <= 0) {
        reject('getInfo: 角色必須是非空陣列!')
      }
      const avatar = data.userCode
      commit('SET_NAME', name)
      commit('SET_ROLES', roles)
      commit('SET_CODE', avatar)
      const users = {
        roles: roles,
        introduction: '',
        code: avatar,
        name: name
      }
      // console.log(users)
      resolve(users)
    }).catch(error => {
      reject(error)
    })
  })
},

2、處理後端返回的路由資訊並存到vuex裡:src/store/modules/permission.js

import { constantRoutes } from '@/router'
import { getMenu } from '../../api/role'
import Layout from '@/layout'

/**
 * 將後端content欄位轉成元件物件
 */
function _import(file) {
  return () => import(`@/views/${file}/index.vue`)
}

/**
 * 通過遞迴過濾非同步路由表
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes) {
  const res = []
  // eslint-disable-next-line no-empty
  for (let i = 0; i < routes.length; i++) {
    res.push({
      path: routes[i].content === 'Layout' ? `/${routes[i].path}` : routes[i].path,
      component: routes[i].content === 'Layout' ? Layout : _import(routes[i].content),
      name: routes[i].path,
      meta: {
        title: routes[i].menuname,
        icon: routes[i].icon
      },
      children: routes[i].children && routes[i].children.length ? filterAsyncRoutes(routes[i].children) : []
    })
  }
  return res
}

const state = {
  routes: constantRoutes,
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise((resolve, reject) => {
      let accessedRoutes
      let asyncRoutes = []
      getMenu(roles[0]).then(res => {
        if (res.msg === 'SUCCESS') {
          asyncRoutes = res.data
        } else {
          asyncRoutes = []
        }
        // console.log(asyncRoutes)
        accessedRoutes = filterAsyncRoutes(asyncRoutes)
        // 最後新增
        const unfound = { path: '*', redirect: '/404', hidden: true }
        accessedRoutes.push(unfound)
        // console.log(accessedRoutes)
        // console.log('store:accessedRoutes')

        commit('SET_ROUTES', accessedRoutes)
        resolve(accessedRoutes)
      }).catch(error => {
        reject(error)
      })
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

基礎路由和統一路由:src/router/index.js

export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path*',
        component: () => import('@/views/redirect/index')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/auth-redirect',
    component: () => import('@/views/login/auth-redirect'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/401'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    name: 'home',
    meta: { title: '首頁', affix: true },
    hidden: true,
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard'),
        hidden: true
      }
    ]
  }
 ]

3、然後再通過遞迴生成側邊欄選單:src/layout
這個時候列印路由資訊是可以得到正常的路由表資訊;根據路由表生成側邊欄:
this.$router.options.routes 可以獲取路由表資訊,具體邏輯vue-element-admin模板裡有現成的,這裡就不解釋了。

最後說一下後端返回的資料結構:

[{
applicationid: 1035
children: [ //子節點,結構和當前一樣
  0:{
  	applicationid: 1035
  	children: [
  		0: {
  			applicationid: 1035
  			children: null
  			content: "system/system-management/menu"
  			formid: 101100
  			formtype: 1
  			icon: ""
  			inactived: 1
  			memo: "EAM"
  			menuid: 101100
  			menuname: "選單管理"
  			parentmenuid: 101000
  			parentrowid: "01KBGO"
  			path: "menu"
  			roles: ""
  			rowid: "01KBGP"
  			seq: 101100
  			treecontrol: "01KBGP01KBGO01KBG9"
  		}
  	]
  	content: "system/system-management"
  	formid: 101000
  	formtype: 1
  	icon: ""
  	inactived: 1
  	memo: "EAM"
  	menuid: 101000
  	menuname: "系統設定"
  	parentmenuid: 100000
  	parentrowid: "01KBG9"
  	path: "system-management"
  	roles: ""
  	rowid: "01KBGO"
  	seq: 101000
  	treecontrol: "01KBGO01KBG9"
  }
]  
content: "Layout"  //對應元件的名稱
formid: 100000
formtype: 1
icon: ""  //選單的圖示
inactived: 1  //是否啟用
memo: "EAM"
menuid: 100000  //選單的id
menuname: "系統管理"  //選單的名稱
parentmenuid: null  //父級選單id
parentrowid: ""
path: "system"  //路由
roles: ""
rowid: "01KBG9"
seq: 100000  //排序
treecontrol: "01KBG9"
}]

相關文章