五、vue+ElementUI開發(fā)后臺管理模板—精確到按鈕的權限控制

(獲取本節(jié)完整代碼 GitHub/chizijijiadami/vue-elementui-5

0击狮、寫在前面

管理后臺該有的基本功能前面文章已經(jīng)寫完了,現(xiàn)在就寫最后一個很重要的權限控制益老。

說到權限肯定是要有登錄系統(tǒng)的彪蓬,那就涉及到驗證使用者的登錄有效性問題,一般系統(tǒng)服務壓力可控的項目是由后端直接進行 sessionid 操作來識別用戶登錄的捺萌,但若需要進行多業(yè)務系統(tǒng)整合档冬,或者用戶量龐大涉及分發(fā)服務器等情況時后端直接操作 sessionid 就顯得捉襟見肘了,這時token機制就該上場了桃纯。

這篇文章主要內容包括:
● Token驗證酷誓,登錄 / 退出
● 頁面+按鈕權限控制

1、Token驗證态坦,登錄 / 退出

這里從一個普通的SAP登錄退出功能開始盐数。
(1)添加接口和Mock數(shù)據(jù)
src>data>api>Login>index.js

import axiosApi from '@/common/utils/axiosApi'
import * as filter from './filter'
export function toLogin(params) {
    return axiosApi({
        url: '/toLogin',
        method: 'post',
        filter: filter.toLogin,
        params: params
    })
}

src>data>api>Login>filter.js

export const toLogin = {
  request(params) {
    return params
  },
  response(data) {
    return data
  }
}

攔截接口 src>data>mock>index.js

......
+ Mock.mock("/toLogin", "post", () => {
+   return {
+     status: 0,
+     data:{
+        token:"123"
+     },
+     message: "成功"
+   };
+ });

(2)新建登錄狀態(tài)記錄
src>common>utils>index.js

const TokenKey = 'Admin-Token'
const err = 'Error:保存到本地存儲失敗!'
const errlimt = 'Error:本地存儲超過限制!'

export function setStorage(key, value, exprise, type) {
  return new Promise(resolve => {
    // 默認7天過期(毫秒)
    let valueDate = JSON.stringify({
      value: value,
      time: new Date().getTime(),
      exprise: exprise || 60 * 60 * 24 * 7 * 1000,
      type: type || ''
    })
    try {
      window.localStorage.setItem(key || TokenKey, valueDate)
    } catch (e) {
      if (isQuotaExceeded(e)) {
        window.localStorage.clear()
        throw errlimt
      } else {
        throw err
      }
    }
    resolve()
  })
}

export function getStorage(key) {
  if (window.localStorage.getItem(key || TokenKey)) {
    let dataObj = JSON.parse(window.localStorage.getItem(key || TokenKey))
    let isTimed = new Date().getTime() - dataObj.time > dataObj.exprise
    if (isTimed) {
      window.localStorage.removeItem(key || TokenKey)
      return null
    } else {
      return dataObj.value
    }
  } else {
    return null
  }
}

// 非空判斷
export function isNotEmpty(value) {
  return value !== undefined && value !== '' && value !== null
}

function isQuotaExceeded(e) {
  let flag = false
  if (e) {
    if (e.code) {
      switch (e.code) {
        case 22:
          flag = true
          break
        // fireFox
        case 1014:
          if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
            flag = true
          }
          break
      }
    } else if (e.number === -2147024882) {
      // ie
      flag = true
    }
  }
  return flag
}

(3)添加登錄 / 退出 按鈕
src>pages>Layout>Header.vue

<template>
  <div class="app-header">
    <Menu v-if="menuLocation==='H'" />
    <el-button
      v-if="menuLocation!=='H'"
      type="primary"
      plain
      @click="setMenuIsCollapse"
      :icon="isCollapse?'el-icon-s-fold':'el-icon-s-unfold'"
    ></el-button>
+    <el-button type="primary" v-if="!getStorage">
+      <router-link to="/login">登錄</router-link>
+    </el-button>
+    <el-button type="primary" v-else @click="quit">退出</el-button>
  </div>
</template>

<script>
  import Menu from "./Menu";
  import { mapGetters } from "vuex";
+ import { getStorage, setStorage } from "common/utils";
export default {
  components: {
    Menu
  },
  computed: {
    ...mapGetters(["app"]),
    isCollapse() {
      return this.app.menu.isCollapse;
    },
    menuLocation() {
      return this.app.menu.location;
    },
+    getStorage() {
+      return getStorage();
+    }
  },
  methods: {
    setMenuIsCollapse() {
      this.$store.dispatch("setMenuIsCollapse");
    },
+    quit() {
+      setStorage().then(() => {
+         this.$router.push({
+           path: "/login?redirect="+this.$router.history.current.fullPath
+         });
+      });
+    }
  }
};
</script>

(4)新建登錄頁面

<template>
  <div class="app-login">
    <el-form ref="form" :model="form" label-width="80px">
      <el-form-item label="用戶名" prop="name" :rules="regCheck({required:true,min:2,max:30})">
        <el-input v-model.trim="form.name" maxlength="30"></el-input>
      </el-form-item>
      <el-form-item label="密碼" prop="pwd" :rules="regCheck({required:true,min:6})">
        <el-input v-model.trim="form.pwd" type="password" maxlength="20"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit('form')">登錄</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import * as api from "data/api/Login";
export default {
  name: "LoginIndex",
  data() {
    return {
      form: {
        name: "",
        pwd: ""
      }
    };
  },
  methods: {
    onSubmit(form) {
      this.$refs[form].validate(valid => {
        if (valid) {
          api.toLogin(form).then(data => {
            if (data.status === 0) {
            setStorage(null, data.data.token).then(() => {
                this.$router.push({
                  path: this.$route.query.redirect
                    ? this.$route.query.redirect
                    : "/"
                });
            }
          });
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    }
  }
};
</script>

<style lang="stylus" scoped>
.app-login
  width 500px
  margin 50px auto
</style>

(5)修改路由
src>router>index.js

  ......
+    {
+        path: '/login',
+        component: _import('Login/index')
+    },
     {
         path: '/404',
         component: _import('ErrorPages/404')
     },

(6)添加校驗規(guī)則
src>common>validate>index.js

        if (_required) {
            rules.push({
                required: true,
                validator: validatorCode.checkNotNull.bind(item),
                trigger: _trigger
            })
        }
+        if (isNotEmpty(item.min) && (isNotEmpty(item.max) || isNotEmpty(item.maxLength))) {
+            rules.push({
+                min: item.min,
+                max: isNotEmpty(item.max) ? item.max : isNotEmpty(item.maxLength),
+                message: '字符長度在' + item.min + '至' + item.max + '之間!',
+                trigger: _trigger
+            })
+        } else if (isNotEmpty(item.min)) {
+            rules.push({
+                min: item.min,
+                message: '至少' + item.min + '個字符',
+                trigger: _trigger
+            })
+        } else if (isNotEmpty(item.max) || isNotEmpty(item.maxLength)) {
+            rules.push({
+                max: isNotEmpty(item.max) ? item.max : isNotEmpty(item.maxLength),
+                message: '至多' + item.max + '個字符',
+                trigger: _trigger
+            })
+        }
        if (_type) {

添加一個工具文件 src>common>utils>index.js

// 非空判斷
export function isNotEmpty(value) {
    return value !== undefined && value !== '' && value !== null
  }

(7)修改全局路由守衛(wèi)

+  import { getStorage } from '../utils'

......

router.beforeEach(async (to, from, next) => {
    document.title = getPageTitle(to.meta.title)

+    if (getStorage()) {
        if (store.getters.app.menu.list.length === 0) {
            store.dispatch("setMenuList", filterRouter(pagesRouterList))
            next({ ...to, replace: true })
        } else {
-             next()
+            if (to.path === '/login') {
                next('/')
+            } else {
+                next()
+            }
        }
+    } else {
+        if (to.path === '/login') {
+            next()
+        } else {
+            next('/login')
+        }
+    }
})

2、權限對接

(1)頁面權限
a. 新建權限接口
src>data>api>Permission>index.js

import axiosApi from '@/common/utils/axiosApi'
import * as filter from './filter'
export function getPermission(params) {
    return axiosApi({
        url: '/permission',
        method: 'get',
        filter: filter.getPermission,
        params: params
    })
}

src>data>api>Permission>filter.js

export const getPermission = {
  request(params) {
    return params
  },
  response(data) {
    return data
  }
}

b. 新建權限接口Mock數(shù)據(jù)
每個路徑頁面都需要唯一標識符去識別伞梯,接口返回的 code 就是路由文件中的 name玫氢。
src>data>mock>permission>index.js

const Mock = require("mockjs");
Mock.mock("/permission", "get", () => {
    return {
      status: 0,
      data: {
        name: '測試',
        code: 'test',
        permission: {
          page: {
            code: 'pagePermission',
            name: "頁面權限",
            children: [
              {
                code: 'Index',
                name: "首頁",
                children: [
                  {
                    code: 'IndexIndex',
                    name: '首頁',
                    children: [
                      {
                        code: 'IndexIndex_save',
                        name: '保存'
                      }
                    ]
                  }
                ]
              },
              {
                code:'List',
                name:'列表',
                children:[
                  {
                    code:'ListDetai',
                    name:'詳情'
                  },
                  {
                    code:'ListFeature',
                    name:'特性'  
                  }
                ]
              }
            ]
          },
          api: {
            code: 'apiPermission',
            name: "接口權限",
            children: []
          }
        }
      },
      message: "成功"
    };
  });

c. 修改mock引入文件,src>data>mock>index.js

+  import './permission'

......

const Mock = require("mockjs");
// 使用mockjs模擬數(shù)據(jù)
let dataList = Mock.mock({
......

d. 修改路由定義
這里要將 pagesRouterList 中不需要權限控制的路由提取出來賦值給 constantRouterList谜诫,這個 constantRouterList 會進行初始化漾峡,而由權限控制的路由則會從接口獲取后通過vue-router的方法 router-addroutes 動態(tài)添加到路由。

src>router>index.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

const _import = file => () => import('@/pages/' + file + '.vue')
export const constantRouterList = [
    {
        path: '/login',
        component: _import('Login/index')
    },
    {
        path: '/404',
        component: _import('ErrorPages/404')
    },
    {
        path: '*',
        redirect: '/404'
    },
    {
        path: '',
        redirect: '/index/index'
    }
]
export const pagesRouterList = [
    {
        path: '/index',
        component: _import('Layout/index'),
        redirect: '/index/index',
        name: "Index",
        meta: {
            title: "首頁",
            icon: "user",
            isShow: true
        },
        children: [
            {
                path: 'index',
                component: _import('Index/index'),
                name: "IndexIndex",
                meta: {
                    title: "首頁",
                    icon: "user",
                    isShow: false
                }
            }
        ]
    },
    {
        path: '/list',
        component: _import('Layout/index'),
        name: "List",
        meta: {
            title: "列表",
            icon: "document",
            isShow: true
        },
        children: [
            {
                path: 'detail',
                component: _import('List/Detail/index'),
                name: "ListDetai",
                meta: {
                    title: "詳情",
                    icon: "document",
                    isShow: true
                }
            },
            {
                path: 'feature',
                component: _import('List/Feature/index'),
                name: "ListFeature",
                meta: {
                    title: "特性",
                    icon: "document",
                    isShow: true
                }
            }
        ]
    }
]
export default new Router({
    scrollBehavior() {
        return { x: 0, y: 0 }
    },
    routes: constantRouterList
})

e. 添加權限狀態(tài)值
修改 src>data>store>modules>app.js

const app = {
   state: {
       system: {
           title: "大米工廠",
       },
+        auth: {
+            page: [],
+            btn: []
+        },
       menu: {
           isCollapse: false,
           location: "V",   //V喻旷、VH灰殴、H三個值,V表示在左側,VH表示橫跨頭部牺陶,H表示在頭部
           list: [],
           obj: {}
       },
       tabs: {
           isShow: false
       },
       crumbs: {
           isShow: false
       },
       footer: {
           isShow: false
       }
   },
   mutations: {
       SET_MENU_ISCOLLAPSE: state => {
           state.menu.isCollapse = !state.menu.isCollapse
       },
       SETMENU_LIST: (state, menuList) => {
           state.menu.list = menuList
       },
+        SET_AUTH: (state, auth) => {
+            state.auth.page = auth.page
+            state.auth.btn = auth.btn
+        }
   },
   actions: {
       setMenuIsCollapse({ commit }) {
           commit('SET_MENU_ISCOLLAPSE')
       },
       setMenuList({ commit }, menuList) {
           commit('SETMENU_LIST', menuList)
       },
+        setAuth({ commit }, auth) {
+            commit('SET_AUTH', auth)
+        }
   }
}
export default app

f. 修改全局路由守衛(wèi)
src>common>routerFilter>index.js

-  import { getStorage } from '../utils'
+  import { getStorage, setStorage } from '../utils'

......

router.beforeEach(async (to, from, next) => {
    document.title = getPageTitle(to.meta.title)

    if (getStorage()) {
        if (store.getters.app.menu.list.length === 0) {
-            store.dispatch("setMenuList", filterRouter(pagesRouterList))
-            next({ ...to, replace: true })
+            filterRouter(pagesRouterList).then(data => {
+                if (data) {
+                    store.dispatch("setAuth", data.auth)
+                    store.dispatch("setMenuList", data.menuList).then(() => {
+                        router.addRoutes(data.menuList)
+                        console.log(to.path);
+                        if (to.path === '/404') {
+                            next('/')
+                        } else {
+                            next({ ...to, replace: true })
+                        }
+                    })
+                } else {
+                    setStorage()
+                    next('/login')
+                }
+            })
        } else {
            if (to.path === '/login') {
                next('/')
            } else {
                next()
            }
        }
    } else {
        if (to.path === '/login') {
            next()
        } else {
            next('/login')
        }
    }
})

src>common>routerFilter>filter.js

  import { MessageBox } from 'element-ui'
  import store from 'store'
+ import * as api from 'data/api/Permission'
  export function filterRouter(pagesRouterList) {
-    let mennuList = pagesRouterList.filter(ele => ele.meta && ele.meta.isShow)
-    try {
-        if (mennuList.length <= 0) throw "沒有可用菜單";
-        filterPage(mennuList)
-        return mennuList;
-    } catch (err) {
-        MessageBox({
-            message: err,
-            showCancelButton: false,
-            confirmButtonText: '確定',
-            type: 'error'
-        })
-    }
+      return api.getPermission().then(data => {
+          let auth = {
+              page: [],
+              btn: []
+          }
+          let permissionPage = data.data.permission.page;
+          try {
+              if (permissionPage.children && permissionPage.children.length <= 0) throw "您暫無權限請聯(lián)系管理員";
+              authArrFilter(permissionPage, auth)
+              let authRouter = authRouterFilter(pagesRouterList, auth)
+              let menuList = authRouter;
+              filterPage(menuList)
+              return { auth: auth, menuList: menuList };
+          } catch (err) {
+              MessageBox({
+                  message: err,
+                  showCancelButton: false,
+                  confirmButtonText: '確定',
+                  type: 'error'
+              })
+          }
+      })
}

+  function authArrFilter(page, auth) {
+      if (page.children) {
+          page.children.forEach(function (item) {
+              if (item.code.match(/_/)) {
+                  auth.btn.push(item.code)
+              } else {
+                  auth.page.push(item.code)
+              }
+              authArrFilter(item, auth)
+          })
+      }
+  }

+  function authRouterFilter(pagesRouterList, auth) {
+      function _filter(list) {
+          return list.filter(item => {
+              if (item.children && item.children.length) {
+                  item.children = _filter(item.children)
+              }
+              return auth.page.includes(item.name)
+          })
+      }
+      return _filter(pagesRouterList)
+  }

function filterPage(menuList, pathFull, joinSign) {
    let pathFullCurrent = pathFull || ""
    let joinSignCurrent = joinSign || ""
    for (let i = 0; i < menuList.length; i++) {
        const ele = menuList[i];
        ele.pathFull = pathFullCurrent + joinSignCurrent + ele.path
        ele.showChildren = []
        store.getters.app.menu.obj[ele.name] = ele
        if (ele.children && ele.meta.isShow) {
            ele.showChildren = ele.children.filter(ele2 => ele2.meta.isShow)
            filterPage(ele.children, ele.pathFull, "/")
        }
    }
}

到這里頁面權限就好了伟阔,運行修改 權限Mock數(shù)據(jù),src>data>mock>permission>index.js

  page: {
            code: 'pagePermission',
            name: "頁面權限",
            children: [

           ......

              {
                code:'List',
                name:'列表',
                children:[
-                  {
-                    code:'ListDetai',
-                    name:'詳情'
-                  },
                  {
                    code:'ListFeature',
                    name:'特性'  
                  }
                ]
              }
            ]
          },

g. 運行如下圖掰伸,詳情菜單沒有顯示皱炉,如果直接輸入 /list/detail 路徑會發(fā)現(xiàn)跳轉到404去了,因為沒有權限狮鸭。


菜單顯示

(2)按鈕權限
這里要用到 Vue-directive 的知識合搅,前面已經(jīng)有了按鈕權限數(shù)組還是比較簡單的。
● 新建全局指令文件
src>common>directives>index.js

import store from 'data/store'
export default {
  // 是否有按鈕權限判定
  btnHas: {
    inserted(el, binding) {
      if (
        !store.getters.app.auth.btn.includes(binding.value)
      ) {
        if (!!window.ActiveXObject || 'ActiveXObject' in window) {
          el.parentNode.removeChild(el)
        } else {
          el.remove()
        }
      }
    }
  }
}

● main.js 引入

......

   //mockj數(shù)據(jù)
   import 'data/mock'
+  // 全局directive指令
+  import directives from './common/directives'
+  // 注冊本頁全局指令方法
+  Object.keys(directives).forEach(key => {
+    Vue.directive(key, directives[key])
+  })

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

● 頁面使用
我們那下面圖上的兩個按鈕試一下


修改 src>pages>Index>index.vue

......
-          <el-button type="primary"  @click="submitForm('form')">提交</el-button>
-          <el-button  @click="resetForm('form')">重置</el-button>
+          <el-button type="primary" v-btnHas="'IndexIndex_save'" @click="submitForm('form')">提交</el-button>
+          <el-button  v-btnHas="'IndexIndex_reset'" @click="resetForm('form')">重置</el-button>
.....

運行如下圖歧蕉,重置按鈕因為不在權限接口中所以不顯示了灾部。



我們再修改Mock權限接口

......
{
      code: 'IndexIndex',
      name: '首頁',
      children: [
         {
            code: 'IndexIndex_save',
            name: '保存'
         },
+         {
+            code: 'IndexIndex_reset',
+            name: '重置'
+          }
      ]
}
......

可以看到因為賦權了,重置按鈕又出來了惯退。


到這里完整的管理后臺就寫好了赌髓,我們后續(xù)見。

感謝閱讀催跪,喜歡的話點個贊吧:)
更多內容請關注后續(xù)文章锁蠕。。。

一、vue入門基礎開發(fā)—手把手教你用vue開發(fā)
二溪掀、vue+ElementUI開發(fā)后臺管理模板—布局
三喻喳、vue+ElementUI開發(fā)后臺管理模板—功能、資源、全局組件
四、vue+ElementUI開發(fā)后臺管理模板—方法指令、接口數(shù)據(jù)

vue3 + vite + ElementPlus開發(fā)后臺管理模板

vue實踐1.1 企業(yè)官網(wǎng)——prerender-spa-plugin預渲染

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載抡笼,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末黄鳍,一起剝皮案震驚了整個濱河市推姻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌框沟,老刑警劉巖藏古,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異忍燥,居然都是意外死亡拧晕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門梅垄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厂捞,“玉大人,你說我怎么就攤上這事∶夷伲” “怎么了欲鹏?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長臭墨。 經(jīng)常有香客問我赔嚎,道長,這世上最難降的妖魔是什么胧弛? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任尤误,我火速辦了婚禮,結果婚禮上结缚,老公的妹妹穿的比我還像新娘损晤。我一直安慰自己,他們只是感情好红竭,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布尤勋。 她就那樣靜靜地躺著,像睡著了一般德崭。 火紅的嫁衣襯著肌膚如雪斥黑。 梳的紋絲不亂的頭發(fā)上揖盘,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天眉厨,我揣著相機與錄音,去河邊找鬼兽狭。 笑死憾股,一個胖子當著我的面吹牛,可吹牛的內容都是我干的箕慧。 我是一名探鬼主播服球,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼颠焦!你這毒婦竟也來了斩熊?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤伐庭,失蹤者是張志新(化名)和其女友劉穎粉渠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圾另,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡霸株,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了集乔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片去件。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出尤溜,到底是詐尸還是另有隱情倔叼,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布靴跛,位于F島的核電站缀雳,受9級特大地震影響,放射性物質發(fā)生泄漏梢睛。R本人自食惡果不足惜肥印,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绝葡。 院中可真熱鬧深碱,春花似錦、人聲如沸藏畅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愉阎。三九已至绞蹦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榜旦,已是汗流浹背幽七。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留溅呢,地道東北人澡屡。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像咐旧,于是被迫代替她去往敵國和親驶鹉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355