我们使用vue-element-admin前端框架开发后台管理系统时,一般都会涉及到菜单的权限控制问题。这里介绍后端控制菜单的一种实践。前端每次刷新时根据用户角色,获取后台的菜单数据,添加到动态路由中,格式化成菜单数据,最终实现如下面:

以下是实现的代码:

 1、route内index.js,配置好静态路由,声明动态路由,保留404组件

import Vue from'vue'
import Router from'vue-router'

Vue.use(Router)

import Layout from'@/layout'

/* 静态路由 */
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: '/401',
    component: () => import('@/views/error-page/401'),
    hidden: true
  },
  {
    path: '',
    component: Layout,
    redirect: 'dashboard',
    isShow: true,
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: { title: 'dashboard', icon: 'home', noCache: true, affix: true }
      }
    ]
  }
]

/* 动态路由 */
export const asyncRoutes = [
  {
    path: "*",
    component: () => import('@/views/error-page/404'),
  }
]

const createRouter = () => new Router({
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher
}

export default router

 2、修改permission.js,使用钩子函数对路由进行权限跳转

import router from'./router'
import store from'./store'
import NProgress from'nprogress' 
import'nprogress/nprogress.css'
import{ getToken, setToken }from'@/utils/auth' 
import getPageTitle from'@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) 

const whiteList = ['/login', '/auth-redirect']

router.beforeEach(async(to, from, next) => {
  NProgress.start()

  document.title = getPageTitle(to.meta.title)

  const hasToken = getToken()

  if(hasToken) {
    if(to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    }else{
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if(hasRoles) {
        next()
      }else{
        try{
          store.dispatch('user/getInfo').then(() => { // 拉取info
            store.dispatch('permission/generateRoutes').then(res => { // 生成可访问的路由表
              router.addRoutes(res) // 动态添加可访问路由表
              const redirect = decodeURIComponent(from.query.redirect || to.path)
              next({ ...to, replace: true })
            })
          }).catch(err => {
            console.log(err);
          })
        }catch(error) {
          console.log(error)
          const origin = window.location.origin
          if(origin.includes('localhost')) {
            setToken('localhost')
          }else{
            window.location = origin + '/login'
          }
          NProgress.done()
        }
      }
    }
  }else{
    if(whiteList.indexOf(to.path) !== -1) {
      next()
    }else{
      window.location = origin + '/login'
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

3、修改store/modules/permission.js,请求后台获取菜单,解析后加入到动态路由

import { asyncRoutes, constantRoutes } from '@/router'
import Layout from '@/layout'
import { getToken }from'@/utils/auth'
import { getUserMenu } from '@/api/user'

function hasPermission (roles, route) {
  if(route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}


export function filterAsyncRoutes (routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route }
    if(hasPermission(roles, tmp)) {
      if(tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

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

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

export function generaMenu(routes, data) {
  data.forEach(item => {
    const menu = {
      path: item.url,
      children: [],
      name: item.name,
      isShow: true,
      meta: { title: item.name, keepAlive: true }
    }
    if(item.url.indexOf("token") >= 0){
      menu.path = item.url + getToken()
    }
    if(item.component!=null&&item.component!==''){
      menu.component = item.component === 'Layout' ? Layout : resolve => require([`@/views${item.component}`], resolve)
    }
    if(item.redirect!=null&&item.redirect!==''){
      menu.redirect = item.redirect
    }
    if(item.icon!=null&&item.icon!==''){
      menu.meta.icon = item.icon
    }else{
      menu.meta.icon = 'task'
    }
    if (item.component === 'Layout') {
      generaMenu(menu.children, item.childList)
    }
    routes.push(menu)
  })
}

export function generaBtn(asyncBtns, data) {
  data.forEach(item => {
    if(item.buttonCode!=null&&item.buttonCode!==''){
      asyncBtns.push(item.buttonCode)
    }
  })
}

const actions = {
  generateRoutes({commit}) {
    return new Promise(resolve => {
      getUserMenu().then(response => {
        let asyncBtns = []
        let menuArr = response.data.object.menuList
        let btnArr = response.data.object.btnList
        generaMenu(asyncRoutes, menuArr)
        generaBtn(asyncBtns, btnArr)
        console.log(asyncRoutes)
        commit('SET_ROUTES', asyncRoutes)
        commit('SET_BTNROLES', asyncBtns)
        resolve(asyncRoutes)
      })
    })
  }
}

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

4、修改store/getters.js,对state.permission.routes设置别名

const getters = {
  sidebar: state => state.app.sidebar,
  language: state => state.app.language,
  size: state => state.app.size,
  device: state => state.app.device,
  visitedViews: state => state.tagsView.visitedViews,
  cachedViews: state => state.tagsView.cachedViews,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  introduction: state => state.user.introduction,
  roles: state => state.user.roles,
  permission_routes: state => state.permission.routes
};
export default getters

5、修改layout/components/Sidebar/index.vue,根据store的路由数据,渲染菜单

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

 至此前端就可以了,和后端约定好菜单数据结构即可,本地也可以使用mock模拟数据来测试

这里提供一份我测试的菜单结构供参考

{
  "code":"0",
  "data":{
    "menuList":[
      {
        "name":"task",
        "component":"Layout",
        "hidden":null,
        "icon":"task",
        "url":"/task",
        "childList":[
          {
            "authSign":"taskInbound",
            "name":"taskInbound",
            "component":"/task/task-inbound-search",
            "hidden":null,
            "icon":"task",
            "url":"task-inbound-search",
            "childList":[]
          }
        ]
      },
      {
        "childList":[
          {
            "authSign":"mapMonitor",
            "childList":[],
            "component":"",
            "hidden":null,
            "icon":"monitor",
            "name":"mapMonitor",
            "url":"/index.html?token="
          }
        ],
        "component":"Layout",
        "hidden":null,
        "icon":"monitor",
        "name":"monitor",
        "url":"/monitor"
      }
    ]]
  },
  "msg":"访问成功",
  "serverTime":"2020-05-22 13:53:05"
}

 如果需要控制到按钮级别,可以参考我写的Vue按钮权限控制实践

-_-如果有其他更好的方式,欢迎下方留言-_-

Logo

前往低代码交流专区

更多推荐