前言

最近,有个业务需要快速开发一套后台管理系统,网上对比了很多,觉得vue-element-admin挺好用,功能强大,但是里面集成了很多实际业务用不到的东西,其基础版vue-element-template正好适合,在整合菜单权限开发,实现不同用户登录时展现不同的菜单出现了一点问题,百度找了很久无果,最后参照下面这位大佬的解决方案,结合我项目的实际完美解决。
vue-admin-template 动态路由实现

重点
常见的管理系统动态菜单权限实现主要有两种方式:
第一:前端只有固定可展现的路由表,比如说login、404、首页,其它路由信息都是后台返回,然后动态添加到路由表里。
第二:前端有完整的路由表,后台返回用户的角色,比如某个菜单有[‘admin’,‘user’],那么后台返回的权限信息里有角色信息[‘admin’]就对该菜单进行展示。
本文实现的第一种,观看前先对比下自己的项目
话不多说,开整。

关于后台返回的数据格式

这是标准化应该返回的菜单数据格式,也是本文后续部分所需要的,最好开发时就和后台老哥定义好返回的数据格式、字段名称。

[
  {
    path: '/example',
    component: 'Layout',
    redirect: '/example',
    name: '权限管理-test',
    alwaysShow: true,
    meta: { title: '权限管理-test', icon: 'el-icon-s-help'},
    children: [
      {
        path: 'table',
        name: '用户',
        component:'views/permission/user',
        meta: { title: '用户', icon: 'table'}
      },
      {
        path: 'tree',
        name: '角色',
        component: 'views/tree/index',
        meta: { title: '角色', icon: 'tree'}
      }
    ]
  },
]

这是我项目中,后台返回的数据格式,因为后台离职,没有来得及约定好字段名及格式,对照标准格式发现后台把path字段命名成router,缺失meta,缺失redirect字段等,这里需要我们处理成上面标准的那种格式。

"data": [
    {
      "children": [
        {
          "children": [],
          "code": "",
          "component": "views/user-Management/index",
          "icon": "table",
          "id": "4",
          "name": "用户管理",
          "pid": "1",
          "remark": "菜单",
          "router": "user",
          "sort": 4,
          "type": 2
        }
      ],
      "code": "",
      "component": "Layout",
      "icon": "el-icon-s-help",
      "id": "1",
      "name": "平台管理",
      "pid": "0",
      "remark": "目录",
      "router": "/user-Management",
      "sort": 1,
      "type": 1
    }
  ],

第一,新增getRoutes,获取后台返回的权限信息

直接在src/api/user.js 文件 的最下面添加getRoutes,如下:

export function getRoutes() {
  return request({
    url: '/user/ownMenu', //这里换成自己的请求后台获取权限信息的接口
    method: 'get'
  })
}

第二,处理router下inde文件

调整 src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
// constantRoutes 是公共路由,不管哪个用户都可以看见
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' }
    }]
  }
]
// asyncRoutes 是动态的,后台返回哪些就只有哪些。
export const asyncRoutes = [
  // 404 必须添加
  { path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})
const router = createRouter()
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

第三,新增permission处理机制

创建 src/store/modules/permission.js 文件

import { constantRoutes } from '@/router' // 引入路由表里的固定路由
import { getRoutes } from '@/api/user' // 引入第一步创建的获取权限信息的接口
import Layout from '@/layout' // 引入布局
// 映射路由表,二级菜单component的值为字符串,但是在这里要映射一下它们的实际位置。
const componentsMap = {
  // 平台管理
  'views/user-Management/index': () => import('@/views/user-Management/index')
}
/**
 * 把后台返回菜单组装成routes要求的格式
 */
export function getAsyncRoutes(routes) {
  const res = []
  const keys = ['path', 'name', 'children', 'redirect', 'alwaysShow', 'meta', 'hidden']
  routes.forEach(item => {
    const newItem = {}
    if (item.component) {
      if (item.component === 'Layout') {
        newItem.component = Layout
      } else {
        newItem['component'] = componentsMap[item.component]
      }
    }
    for (const key in item) {
      if (keys.includes(key)) {
        newItem[key] = item[key]
      }
    }
    if (newItem.children) {
      newItem.children = getAsyncRoutes(item.children)
    }
    res.push(newItem)
  })
  return res
}
export function getMenuListData(menuList) {
  // 我自己的,后台返回的数据格式和标准格式一致的就去掉这个方法就行
  for (let i = 0; i < menuList.length; i++) {
    if (menuList[i].children.length > 0) {
      menuList[i].redirect = menuList[i].router + '/' + menuList[i].children[0].router
      menuList[i].path = menuList[i].router
      menuList[i].meta = {
        title: menuList[i].name,
        icon: menuList[i].icon
      }
      menuList[i].name = menuList[i].router
      getMenuListData(menuList[i].children)
    } else {
      menuList[i].path = menuList[i].router
      menuList[i].meta = {
        title: menuList[i].name,
        icon: menuList[i].icon
      }
      menuList[i].name = menuList[i].router
    }
  }
  return menuList
}
const state = {
  routes: [],
  addRoutes: []
}
const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes // 路由访问
    state.routes = constantRoutes.concat(routes) // 菜单显示
  }
}
const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise(async resolve => {
      const routes = await getRoutes() // 接口获取到后台返回的权限信息,包含路由
      const asyncRoutesFront = getMenuListData(routes.data) //这里是因为我返回的格式有点问题,后台暂时没法改,没办法先这么处理了下,返回和标准格式一致的,请直接const asyncRoutes = getAsyncRoutes(routes.data)
      const asyncRoutes = getAsyncRoutes(asyncRoutesFront) // 对路由格式进行处理
      commit('SET_ROUTES', asyncRoutes)
      resolve(asyncRoutes)
    })
  }
}

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

第四,把permission挂到store上

修改src/store/index.js 文件

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/permission' // 把第三步建好的文件挂上去

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
    permission
  },
  getters
})

export default store

第五,对权限拦截器进行调整

修改src/permission.js,注意路径要看对,这个不是第三步新增的src/store/modules/permission.js 文件!

import router, { constantRoutes } from './router' // 这里把constantRoutes 引入进来
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' 
import 'nprogress/nprogress.css' 
import { getToken } from '@/utils/auth' 
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) 
const whiteList = ['/login'] 

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()
  // set page title
  document.title = getPageTitle(to.meta.title)
  // determine whether the user has logged in
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasGetUserInfo = store.getters.name
      if (hasGetUserInfo) {
        next()
      } else {
        try {
          await store.dispatch('user/getInfo')
          // 这里调用的就是第三步新建的generateRoutes
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          router.options.routers = constantRoutes.concat(accessRoutes) // 这一步必须写,不然会出现左侧菜单空白、刷新空白等问题
          router.addRoutes(accessRoutes)
          next({ ...to, replace: true })
          // next()
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

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

第六,调整 getters.js,把动态路由放进去

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  // 动态路由
  permission_routes: state => state.permission.routes
}
export default getters

第七,修改菜单组件页面

修改src/layout/components/Sidebar/index.vue

<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 routes" :key="route.path" :item="route" :base-path="route.path" /> -->
        <!-- 动态路由 -->
        <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',
    ]),
    routes() {
      return this.$router.options.routes
    },
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

至此,前端菜单实现动态权限。
本文大部分内容来自于vue-admin-template 动态路由实现一文,自己书写加强一下记忆,感谢大佬。

Logo

前往低代码交流专区

更多推荐