vue-admin-template 动态路由实现

3,616 阅读4分钟

业务需求

基于 vue-admin-template 脚手架,用户根据不同角色所展示相应的页面。 后端接口服务,采用Go Gin框架。

实现思想

实现用户角色权限认证,大致分为两种实现方式:

  1. 前端写好静态路由表,用户登录时请求后端接口,获取用户的角色信息,通过角色展示相应页面。
  2. 前端路由表只保留白名单列表,比如 /login 。用户登录时异步请求获取路由信息,将返回的路由表传入前端,展示相应页面。

代码实现

完整项目,可参考Github仓库中项目

vue-gin-template 前端

gin-vue-template 后端

前端路由表数据结构

后端接口将返回数据格式,如下:

[
 {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    
    children: [{
      path: 'dashboard',
      name: '首页1',
      alwaysShow: true,
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页1', icon: 'dashboard' }
    }]
  },
​
  {
    path: '/example',
    component: Layout,
    redirect: '/example',
    name: '权限管理-test',
    alwaysShow: true,
    meta: { title: '权限管理-test', icon: 'el-icon-s-help'},
    children: [
      {
        path: 'table',
        name: '用户',
        component: () => import('@/views/permission/user'),
        meta: { title: '用户', icon: 'table'}
      },
      {
        path: 'tree',
        name: '角色',
        component: () => import('@/views/tree/index'),
        meta: { title: '角色', icon: 'tree'}
      }
    ]
  },
]

前端

新增permission处理机制

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

import { constantRoutes } from '@/router'
import { getRoutes } from '@/api/role' // 获取路由的接口方法
import Layout from '@/layout'
​
​
// 映射路由表
const componentsMap = {
  '/views/dashboard/index': () => import('@/views/dashboard/index'),
  '/views/permission/role': () => import('@/views/permission/role'),
  '/views/permission/user': () => import('@/views/permission/user'),
};
​
/**
 * 把后台返回菜单组装成routes要求的格式
 * @param {*} 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
}
​
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 asyncRoutes = getAsyncRoutes(routes.data) // 对路由格式进行处理
      commit('SET_ROUTES', asyncRoutes)
      resolve(asyncRoutes)
    })
  }
}
​
export default {
  namespaced: true,
  state,
  mutations,
  actions
}

调整 getters.js

src/store/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
修改 layout 布局

调整 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>
权限拦截器调整

调整 src/permission.js

import router, { constantRoutes } from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'NProgress.configure({ showSpinner: false }) // NProgress Configurationconst whiteList = ['/login'] // no redirect whitelist
​
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 {
          // get user info
          const { roles } =  await store.dispatch('user/getInfo')
          // 在这里获取异步路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 调用router.addRoutes方法,将异步路由添加进去
          router.options.routers = constantRoutes.concat(accessRoutes)
          router.addRoutes(accessRoutes)
          next({ ...to, replace: true })
        } 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()
})
修改路由表

调整 src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'Vue.use(Router)
​
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },
]
​
export const asyncRoutes = [
  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]
​
const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})
​
const router = createRouter()
​
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}
​
export default router

后端

数据库表结构
package models
​
import (
  "github.com/jinzhu/gorm"
)
​
// 角色表
type Role struct {
  gorm.Model
  Name string `gorm:"type:varchar(128);not null;comment:'角色名称'"`
  RoleID string `gorm:"not null;unique_index;comment:'角色标识'"`
  Rid uint `gorm:"not null;comment:'角色ID'"`
  Remark string `gorm:"size:255;comment:'描述'"`
}
​
func (Role) TableName() string {
  return "t_role"
}
​
// 权限表
type Permission struct {
  gorm.Model
  PermissionId uint `gorm:"not null;unique_index;comment:'权限ID'"`
  IsGroup string `gorm:"not null;comment:'是否是组'"`
  
  Title string  `gorm:"not null;comment:'模块名称'"`
  Path string `gorm:"not null;comment:'模块路径'"`
  Redirect string `gorm:"not null;comment:'重定向路径'"`
  Component string `gorm:"not null;comment:'模块部件'"`
  Icon string `gorm:"not null;comment:'模块icon'"`
  
  SubTitle string `gorm:"comment:'子模块标题'"`
  SubComponent string `gorm:"comment:'子模块部件'"`
  SubPath string `gorm:"comment:'子模块路径'"`
  SubIcon string `gorm:"comment:'子模块icon'"`
}
​
func (Permission) TableName() string {
  return "t_permission"
}
​
// 角色权限关联表
type RoleBePermission struct {
  gorm.Model
  Pid uint `gorm:"not null;comment:'权限ID'"`
  Rid uint `gorm:"not null;comment:'角色ID'"`
}
​
func (RoleBePermission) TableName() string {
  return "t_role_permission"
}
获取路由信息函数
/**
* @FuncName 获取路由信息
* @Describe 按照 vue router js格式返回
* @Param role 角色标识
* 
* @return 数组格式
*/
func RouterInfo(rid uint) []map[string]interface{} {
  router := models.GetPermissionFormRid(rid)
  routerList := make([]map[string]interface{},0)
  menuMap := models.GetMenu()
  // 单页面
  pageList := make([]string,0)
  pageList = menuMap["page"].([]string)
  for _, menu := range pageList{
    for _, r := range router {
      if r.Redirect == menu {
        singleMap := make(map[string]interface{})
        singleMap["path"] = r.Path
        singleMap["component"] = r.Component
        singleMap["redirect"] = r.Redirect
        
        childrenList := make([]map[string]interface{},0)
        childrenMap := make(map[string]interface{})
        meta := make(map[string]string)
        childrenMap["path"] = r.SubPath
        childrenMap["name"] = r.Title
        childrenMap["component"] = r.SubComponent
        meta["title"] = r.Title
        meta["icon"] = r.Icon
        childrenMap["meta"] = meta
        childrenList = append(childrenList, childrenMap)
        singleMap["children"] = childrenList
​
        if len(singleMap) != 0 {
          routerList = append(routerList, singleMap)
        }
      }
    }
  }
  // 组页面
  groupList := make([]string,0)
  groupList = menuMap["group"].([]string)
  for _, group := range groupList {
    childrenList := make([]map[string]interface{},0)
    groupMap := make(map[string]interface{})
    for _, r := range router {
      if r.IsGroup == "是" && r.Path == group {
        subChildren := make(map[string]interface{})
        meta := make(map[string]interface{})
        subMeta := make(map[string]interface{})
​
        groupMap["path"] = r.Path
        groupMap["component"] = r.Component
        groupMap["redirect"] = r.Redirect
        groupMap["name"] = r.Title
        meta["title"] = r.Title
        meta["icon"] = r.Icon
        groupMap["meta"] = meta
        
        subChildren["path"] = r.SubPath
        subChildren["name"] = r.SubTitle
        subChildren["component"] = r.SubComponent
        subMeta["title"] = r.SubTitle
        subMeta["icon"] = r.SubIcon
        subChildren["meta"] = subMeta
        childrenList = append(childrenList, subChildren)
        groupMap["children"] = childrenList
      }
    }
    if len(groupMap) != 0 {
      routerList = append(routerList, groupMap)
    }
  }
  return routerList
}
获取路由表接口
// GetRouter
// @Summary 获取路由表信息
// @Description 提供前端路由表
// @Tags 用户认证
// @Success 20000 {object} response.Response "{"code": 20000, "data": [...]}"
// @Router /api/role/router [get]
// @Security 
func GetRouter(c *gin.Context) {
  headerMap := c.Request.Header
  token, _ := jwt.ParseToken(headerMap["X-Token"][0])
  data := service.RouterInfo(token.RoleID)
  c.JSON(http.StatusOK, gin.H{
    "code": 20000,
    "message": "获取路由表成功",
    "data": data,
  })
}