代碼改變世界

小愛ADMIN系列文章(一):用Vue-cli3+mockjs 實現后臺管理權限和三級菜單功能

2019-06-19 09:14  流浪的詩人  閱讀(...)  評論(...編輯  收藏

最近完成了我的小愛ADMIN后臺管理系統基本功能,同時進行了頁面整體布局和樣式的全新改版。新增了系統權限功能的實現,同時覺得后臺系統所有的菜單都左置,會限制菜單的擴展,因此我改進了三級菜單的顯示。

項目demo展示:

權限功能的實現

權限路由思路:
根據用戶登錄的roles信息與路由中配置的roles信息進行比較過濾,生成可以訪問的路由表,并通過router.addRoutes(store.getters.addRouters)動態添加可訪問權限路由表,從而實現左側和頂欄菜單的展示。

權限功能的實現步驟:

1.給相應的菜單設置默認的roles信息

router/index.js中,給相應的菜單設置默認的roles信息;
如下:

給"權限設置"菜單設置的權限為:

{
    path: '/permission',
    name: 'permission',
    meta: {
      title: '權限設置',
      roles: ['admin', 'editor'] //不同的角色都可以看到
    }
}

給其子菜單"頁面權限",設置權限為:

{
    path: 'page',
    name: 'pagePer',
    meta: {
      title: '頁面權限',
      roles: ['admin'] //只有"admin"可以看到該菜單
    },
    component: () => import('@/page/permission/page'),
}

給其子菜單"按鈕權限"設置權限為:

{
    path: 'directive',
    name: 'directivePer',
    meta: {
      title: '按鈕權限',
      roles:['editor'] //只有"editor"可以看到該菜單
    },
    component: () => import('@/page/permission/directive'),
}

2.通過router.beforeEach()進行路由過濾和權限攔截;

代碼如下:

function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true 
  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并存入了cookie,保證頁面刷新時,始終可以拿到token
  if (getToken('Token')) {
    if(to.path === '/login') {
      next({ path: '/' })  
      NProgress.done() 
    } else {
      // 用戶登錄成功之后,每次點擊路由都進行了角色的判斷;
      if (store.getters.roles.length === 0) {
        let token = getToken('Token');
        getUserInfo({"token":token}).then().then(res => { // 根據token拉取用戶信息
          let userList = res.data.userList;
          store.commit("SET_ROLES",userList.roles);
          store.commit("SET_NAME",userList.name);
          store.commit("SET_AVATAR",userList.avatar);
          store.dispatch('GenerateRoutes', { "roles":userList.roles }).then(() => { // 根據roles權限生成可訪問的路由表
            router.addRoutes(store.getters.addRouters) // 動態添加可訪問權限路由表
            next({ ...to, replace: true }) // hack方法 確保addRoutes已完成
          })
        }).catch((err) => {
          store.dispatch('LogOut').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)
})

本系統權限邏輯分析

1、路由對象區分權限路由對象和非權限路由對象;初始化時,將非權限路由對象賦值給Router;同時設置權限路由中的meta對象,如:meta:{roles:['admin','editor']},表示該roles所擁有的路由權限;

2、通過用戶登錄成功之后返回的roles值,進行路由的匹配并生成新的路由對象;

3、用戶成功登錄并跳轉到首頁時,根據剛剛生成的路由對象,渲染左側的菜單;即,不同的用戶看到的菜單是不一樣的;

用戶點擊登錄之后的業務邏輯分析

1、用戶點擊登錄按鈕,通過路由導航鉤子router.beforeEach()函數確定下一步的跳轉邏輯,如下:

 1.1、用戶已經登錄成功過,并從cookie中拿到了token值;

 1.1.1、用戶訪問登錄頁面,直接定位到登錄頁面;
 
 1.1.1、用戶訪問非登錄頁面,需要根據用戶是否有roles信息,進行不同的業務邏輯,如下:
 
    (1)、初始情況下,用戶roles信息為空;
    
        1.通過getUserInfo()函數,根據token拉取用戶信息;并通過store將該用戶roles,name,avatar信息存儲于vuex;
        
        2.通過store.dispatch('GenerateRoutes', { roles })去重新過濾和生成路由,通過router.addRoutes()合并路由表; 
        
        3.如果在獲取用戶信息接口時出現錯誤,則調取store.dispatch('LogOut')接口,返回到login頁面;
      
    (2)、用戶已經擁有roles信息;
    
        1.點擊頁面路由,通過roles權限判斷 hasPermission()。如果用戶有該路由權限,直接跳轉對應的頁面;如果沒有權限,則跳轉至401提示頁面;

2.用戶點擊退出,token已被清空

1.如果設置了白名單用戶,則直接跳轉到相應的頁面;

2.反之,則跳轉至登錄頁面;

詳細代碼,請參考src/permission.js

權限演示說明

測試賬號:

(1). username: admin,password: 123456;admin擁有最高權限,可以查看所有的頁面和按鈕;

(2). username: editor,password: 123456;editor只有被賦予權限的頁面和按鈕才可以看到;

三級導航菜單頂部欄展示

如圖所示,在完成一般后臺系統所具有的二級導航菜單功能之后,我發現其實很多的后臺管理系統都有三級導航菜單,但是如果都把三級菜單放到左側菜單做階梯狀排列,就會顯得比較緊湊,因此我覺得把所有的三級菜單放到頂部是一個不錯的選擇。

開發需求

點擊左側菜單,找到其對應的菜單(頂欄菜單)排放于頂部導航欄;

開發步驟

1. 定義頂部導航組件topMenu.vue

通過element-ui,NavMenu 導航菜單來進行頂部菜單的展示,注意頂欄和側欄設置的區別;同時將其引用于頭部組件headNav.vue中;

2. 定義頂欄路由數據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"來區分是頂欄路由,還是左側路由;

3. 準備headNav.vue中渲染數據

思路:點擊左側菜單,需要顯示頂部對應的菜單。因為左側菜單要和頂部菜單建立聯系。我們知道導航菜單在用戶登錄時,會根據用戶的role信息進行權限過濾;那么,在過濾權限路由數據之前,我們可以通過addTopRouter()將所有的三級菜單進行過濾添加,添加完成之后,繼續進行角色過濾,可以保證將不具備權限的頂部菜單也過濾掉。

// src/store/permission.js,通過循環過濾,生成新的二級菜單
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;
}

4.點擊左側菜單過濾路由并顯示對應數據

在組件topMenu.vue中,用戶默認進來或者點擊左側菜單,觸發setLeftInnerMenu()函數,如下:

 setLeftInnerMenu(){
    const titleList = this.$route.matched[1].meta.titleList;
    const currentTitle = titleList && this.$route.matched[2].meta.title;
    if( titleList && this.$route.matched[1].meta.routerType === 'leftmenu'){ // 點擊的為 左側的2級菜單
        this.$store.dispatch('ClickLeftInnerMenu',{'titleList':titleList});
        this.$store.dispatch('ClickTopMenu',{'title':currentTitle});
    }else{ // 點擊左側1級菜單
        this.$store.dispatch('ClickLeftInnerMenu',{'titleList':[]});
        this.$store.dispatch('ClickTopMenu',{'title':''});
    }
}

通過當前路由this.$route.meta.routerType的值判斷,用戶是點擊頂部菜單還是左側菜單。如果點擊頂部菜單,通過this.$store觸發異步動作'ClickLeftInnerMenu'并傳遞參數'name',vuex中通過state.topRouters = filterTopRouters(state.routers,data)過濾當前路由信息;代碼如下:

// src/store/permission.js,獲取到當前路由對應頂部子菜單
 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'])}進行對應頂部路由數據的展示。用戶每次點擊左側菜單時,頂部路由都進行了重新賦值并渲染,保證了數據的準確性。

5.頂部菜單完善

當頂部菜單的數據量過大時,我們需要設置橫向滾動條并設置滾動條的樣式。
如圖:

mock數據

使用背景

在使用easy-mock模擬數據的過程中,發現其對表格固定數據不能實現增刪改等功能,并且由于它們是免費提供服務,導致用戶量較大時,服務器經常無法訪問,因而選擇了使用mockjs進行本地數據模擬。

介紹及功能

Mock.js是一款模擬數據生成器,旨在幫助前端攻城師獨立于后端進行開發,幫助編寫單元測試。提供了以下模擬功能:

1.根據數據模板生成模擬數據,通過mockjs提供的方法,你可以輕松地創造大量隨機的文本,數字,布爾值,日期,郵箱,鏈接,圖片,顏色等.

2.模擬 Ajax 請求,生成并返回模擬數據,mockjs可以進行強大的ajax攔截.能判斷請求類型,獲取到url,請求參數等.然后可以返回mock的假數據,或者你自己編好的json文件.功能強大易上手.

3.基于 HTML 模板生成模擬數據

mockjs在本項目中使用

1. 安裝mockjs

npm install mockjs --save-dev

2.創建mock文件夾結構并定義相關的功能模塊

如圖:




mockjs/index.js,負責定義相關的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.組件中,接口調用,獲取數據,渲染頁面

vue-cli3.0 升級記錄

由于項目早期使用vue-cli2.0構建項目,需要進行繁瑣的webpack配置;vue-cli 3.0集成了webpack配置并在性能提升上做了很大優化。因為本項目使用vue-cli3.0進行構建和升級。現將相關注意事項總結如下,詳細文檔,請參考官網介紹

1.vue-cli3.0使用前提介紹

Vue CLI 的包名稱由 vue-cli 改成了 @vue/cli。
如果你已經全局安裝了舊版本的 vue-cli (1.x 或 2.x),你需要先通過

npm uninstall vue-cli -g 或 yarn global remove vue-cli

卸載它。
Vue CLI 需要 Node.js 8.9 或更高版本 (推薦 8.11.0+)。你可以使用 nvm 或 nvm-windows 在同一臺電腦中管理多個 Node 版本。

2.vue-cli3.0安裝及使用

1.vue-cli3.x安裝

npm install -g @vue/cli
# OR
yarn global add @vue/cli

如果希望還保留 vue-cli2.x 的語法或使用 2.x 的模板,建議安裝 cli-init

npm install -g @vue/cli-init
# OR
yarn global add @vue/cli-init

2.使用 vue-cli3.x 創建項目

vue create 項目名稱;
安裝步驟選取相關的配置信息即可,直到完成。

3.新項目需要進行環境變量和模式配置

在根目錄下新建文件.env.development和.env.production,分別表示開發環境和生成環境配置;主要用于定義環境變量,并通過npm run serve或npm run build集成到不同的環境中,供接口調用。代碼如下:

.env.development

NODE_ENV = development
VUE_APP_URL = "https://easy-mock.com/mock/5cd03667adb0973be6a3d8d1/api"

.env.production

NODE_ENV = production
VUE_APP_URL = "https://easy-mock.com/mock/5cd03667adb0973be6a3d8d1/api"

使用方法,如本項目中,配置在utils/env.js中,代碼如下:

// development和production環境是不同的
let app_url = process.env.VUE_APP_URL  
export default {
    app_url
}

4.使用vue.config.js編譯打包詳細配置

由于使用vue-cli3.x生成項目,webpack相關配置已經集成到node_module中,如果希望對 webpack 等進行細致化配置,需要在項目根目錄下新建文件vue.config.js,具體配置可參考文檔,下面是一份基本配置。

const TerserPlugin = require('terser-webpack-plugin')  // 用于在生成環境剔除debuger和console
const path = require('path');
const resolve = dir => {
  return path.join(__dirname, dir);
};

const env = process.env.NODE_ENV
let target = process.env.VUE_APP_URL  // development和production環境是不同的

module.exports = {
  publicPath: '/',
  outputDir: './dist',
  lintOnSave: false, // 關閉eslint
  // 打包時不生成.map文件
  productionSourceMap: false,
  devServer: {
    open: true,
    host: '0.0.0.0',
    port: 8808
    // 由于本項目數據通過easy-mock和mockjs模擬,不存在跨域問題,無需配置代理;
    // proxy: { 
    //   '/v2': {
    //       target: target,
    //       changeOrigin: true
    //   }
    // }
  },
   // webpack相關配置
  chainWebpack: (config) => {
    config.entry.app = ['./src/main.js'];
    config.resolve.alias
      .set('@', resolve('src'))
      .set('cps', resolve('src/components'))
  },
  configureWebpack:config => {
    // 為生產環境修改配置...
    if (process.env.NODE_ENV === 'production') {
      new TerserPlugin({
        cache: true,
        parallel: true,
        sourceMap: true, // Must be set to true if using source-maps in production
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    } else {
      // 為開發環境修改配置...

    }
  },
   // 第三方插件配置
  pluginOptions: {

  }
}

項目配置完成好,安裝好所有的依賴包。執行開發環境打包命令:npm run serve,即可運行項目;執行生成環境打包命令:npm run build,即可生成生產環境文件。

結尾

項目開發至此,一些基本的功能都已經完成,基本上能夠滿足項目需要。下篇文章會繼續介紹"項目分享功能的實現細節"、"項目部署細節及注意事項(包括如何部署子目錄)"、"項目性能優化細節",希望大家敬請期待~

技術答疑

項目說明:

小愛ADMIN是完全開源免費的管理系統集成方案,可以直接應用于相關后臺管理系統模板;很多重點地方都做了詳細的注釋和解釋。如果你也一樣喜歡前端開發,歡迎加入我們的討論/學習群,群內可以提問答疑,分享學習資料;
歡迎加入答疑qq群。

内部期期公开一波中特