Vue2管理系统实现动态路由权限管理和动态侧边菜单栏

2,416 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

前言

最近在开发健康管理系统,管理系统一定逃不掉权限这一块,需求:需要根据不用的用户匹配不同的管理权限,既:匹配不同的操作导航,尤其体现在后台管理系统内,如果仅仅只是在导航菜单内不予显示,仍然是可以通过路径直接打开页面,因为其路由信息已经在路由信息对象(new Router({}))函数中进行了注册,项目模版vue-admin-template

目录

├── build                      # 构建相关
├── mock                       # 模拟数据
├── public                     # 静态资源
│   │── favicon.ico            # favicon图标
│   └── index.html             # html模板
├── src                        # 源代码
│   ├── api                    # 所有请求
│   ├── assets                 # 主题 字体等静态资源
│   ├── components             # 全局公用组件
│   ├── icons                  # 项目所有 svg icons
│   ├── layout                 # 全局 layout
│   ├── router                 # 路由
│   ├── store                  # 全局 store管理
│   ├── styles                 # 全局样式
│   ├── utils                  # 全局公用方法
│   ├── views                  # views 所有页面
│   ├── A pp.vue               # 入口页面
│   ├── main.js                # 入口文件 加载组件 初始化等
│   └── permission.js          # 权限管理
├── tests                      # 测试
├── .env.xxx                   # 环境变量配置
├── .eslintrc.js               # eslint 配置项
├── .babelrc                   # babel-loader 配置
├── .travis.yml                # 自动化CI配置
├── vue.config.js              # vue-cli 配置
├── postcss.config.js          # postcss 配置
└── package.json               # package.json

开发

逻辑

  • 配置两个路由数组:

    • 一个是公共的,无需权限都可以加载,比如首页,登录页,404页面等;
    • 一个是动态的,配置角色权限,从而动态选择是否显示;
  • 点击登录后,会返回该用户的权限信息,拿去和动态路由数组的角色权限做配对,把该用户可以访问的路由筛选出来。

  • 最后通过 vue 的 addRoutes方法把筛选出来的数组动态添加到实际路由对象即可。

准备工作

1.修改element-ui为中文状态

// 在src/main.js中
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明
Vue.use(ElementUI)

2.删除沉余路由,只保留登录页和首页

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' }
    }]
  },
]

3.关闭eslint校验(也可以不管,个人不太感冒)

//在vue.config.js中
module.exports = {
  publicPath: '/admin',
  outputDir: 'dist',
  assetsDir: 'static',
  // lintOnSave: process.env.NODE_ENV === 'development',
  lintOnSave:false,   //关闭eslint校验
  ......
}

4.在api文件夹内新建index.js

引入request请求工具,将登录,用户信息,侧边栏路由表接口封装成请求方法

import request from '@/utils/request'
// 登录接口
export function login(data) {
    return request({
      url: '/userinfo/login',
      method: 'POST',
      data
    })
}
// 用户信息接口
export function getUserInfo() {
    return request({ 
      url: '/index/info',
      method: 'GET',
    })
}
// 侧边栏路由表接口
export function getMoveRouter() {
    return request({
      url: '/index/menu',
      method: 'GET',
    })
}

反向代理

//在vue.config.js中
module.exports = {
  publicPath: '/admin',
  outputDir: 'dist',
  assetsDir: 'static',
  // lintOnSave: process.env.NODE_ENV === 'development',
  lintOnSave:false,       //关闭eslint校验
  productionSourceMap: false,
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    proxy:{
      //配置跨域
      '/api':{
        target:"https://xxx-health.net", //接口域名
        changOrgin:true,
        pathRewrite: {
          '^/api': '/'
        }
      }
    },
    // before: require('./mock/mock-server.js') // 模拟数据
  },

修改.env.development

#just a flag
ENV = 'development'

# base api
VUE_APP_BASE_API = '/api'

修改vue.config.js后npm run dev重启项目

登录

1 .修改src/views/login/index.vue 中的handleLogin登录方法

handleLogin() {
      this.$refs.loginForm.validate((valid) => {
        if (valid) {
          this.loading = true; //开启加载动画
          this.$store
            .dispatch("user/login", this.loginForm) //调用vuex中的login异步方法
            .then(() => {
              this.$router.push({ path: "/" });
              this.loading = false;//关闭加载动画
            })
            .catch(() => {
              this.loading = false;//关闭加载动画
            });
        } else {
          console.log("error submit!!");
          return false;
        }
      });
},

2.修改src/store/modules/user.js 的login方法

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { username, password,userType } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password,userType:userType }).then(response => {
        const { token } = response.data.userInfo
        commit('SET_TOKEN', token)
        setToken(token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },
}

3.修改封装的请求工具 在src/utils/request.js 中

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent
    
    if (store.getters.token) {
      // let each request carry token
      // ['token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['token'] = getToken()  //改为后端所需要的Key值
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  response => {
    const res = response.data
    const successArr = [0,200, 201, 204,20000] //成功的状态码
    // if the custom code is not in successArr, it is judged as an error.
    if (successArr.includes(res.code)==false) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      ......
  },

4.修改用户信息接口 在src/store/modules/user.js中

 // get user info
  getInfo({
    commit,
    state
  }) {
    return new Promise((resolve, reject) => {
      getUserInfo().then(response => {
        const {
          name,
          avatar
        } = response.data            //从data中解构出昵称和头像
        commit('SET_NAME', name)     //把name 保存到vuex中
        commit('SET_AVATAR', avatar) //把avatar 保存到vuex中
        resolve(name)
      }).catch(error => {
        reject(error)
      })
    })
  },

5.修改头像路径 在src/layout/components/Navbar.vue中

<template>
  <div class="navbar">
    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb class="breadcrumb-container" />
    <div class="right-menu">
      <el-dropdown class="avatar-container" trigger="click">
        <div class="avatar-wrapper">
          <img :src="avatar" class="user-avatar">   //删除路径后面拼接的字符串
          <i class="el-icon-caret-bottom" />
        </div>
        ......
    </div>
  </div>
</template>

到这里我们已经实现了登录以及获取用户信息,接下来请求侧边栏路由表接口来动态渲染侧边栏吧

请求动态路由渲染侧边栏

1 .配置

//在src/router 文件夹下新建两个js文件
_import_development.js 和 _import_production.js

_import_development.js

// 开发环境导入组件
module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+

_import_production.js

// 生产环境导入组件
module.exports = file => () => import('@/views/' + file + '.vue')

2 .修改permission.js

const _import = require('./router/_import_' + process.env.NODE_ENV) // 获取组件
import Layout from "@/layout";

router.beforeEach(async (to, from, next) => {
......
 if (hasGetUserInfo) {
        next()
 } else {
        try {
          // 如果没有获取到vuex中到name
          await store.dispatch('user/getInfo') //触发获取用户信息的接口
          await store.dispatch('user/getRouter') //触发获取路由表的接口   
          //next()
        } catch (error) {
          await store.dispatch('user/resetToken') //触发vuex中  resetToken
          Message.error(error || 'Has Error') //弹出异常
          next(`/login?redirect=${to.path}`) //然后就执行这里 跳转到 login  redirect把从哪个页面出错的 
          NProgress.done()
        }
 }
}

3 .在src/store/modules/user.js中新增Action函数来请求路由表接口

import { login,getMoveRouter,getUserInfo } from '@/api/index'
......
const getDefaultState = () => {
  return {
    ......
    menus:'',      //存放路由表
  }
}
const mutations = {
    ......
    SET_MENU: (state, menus) => {
        state.menus = menus
    },
},
const actions = {
    .......
  // getRouter
  getRouter({ commit }) {
    return new Promise((resolve, reject) => {
      getMoveRouter().then(response => {
        const { permissionList:menus } = response.data
       //添加404 页面
       menus.push({
        path: "/404",
        component: "404",
        hidden: true
      }, {
        path: "*",
        redirect: "/404",
        hidden: true
      })
        commit('SET_MENU', menus) 
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },
}

4 .处理获取的动态路由 src/permission.js

获取的动态路由中components属性是字符串,例components:"Layout",但我们需要的是一个对象所以我们要转换一下

......
router.beforeEach(async (to, from, next) => {
  // start progress barstore
  NProgress.start()
  document.title = getPageTitle(to.meta.title)
  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 {
          // 如果没有获取到vuex中到name
          await store.dispatch('user/getInfo') //触发获取用户信息的接口
          await store.dispatch('user/getRouter') //触发获取路由表的接口   
          if (store.getters.menus.length < 1) {
            store.getters.menus = []
            next()
          }
          const menus = filterAsyncRouter(store.getters.menus) //过滤动态路由
          router.addRoutes(menus) //动态挂载路由
          await store.dispatch('user/keepMenu', menus) //将过滤后的异步路由传递给vuex中的menus,做侧边栏渲染的工作

          next({
            ...to,
            replace: true
          })
        } catch (error) {
          await store.dispatch('user/resetToken') //触发vuex中  resetToken
          Message.error(error || 'Has Error') //弹出异常
          next(`/login?redirect=${to.path}`) //然后就执行这里 跳转到 login  redirect把从哪个页面出错的 
          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()
    }
  }
})


//  遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap) {
  const accessedRouters = asyncRouterMap.filter(route => {
    if (route.component) {
      if (route.component === 'Layout') {
        route.component = Layout       //将字符串转换为对象
      } else {
        try {
          var key = "icon";
          var value = "form"
          route.meta[key] = value;
          route.component = _import(route.component) // 导入组件
        } catch (error) {
          route.component = _import('system/menu/index') // 导入组件
        //弹出异常('请修改或者删除不存在的组件路径')
        }
      }
    }
    if (route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children)
    }
    return true
  })
  return accessedRouters
}

处理之后点击登录报错 image.png 这个报错是因为vuex获取的动态路由找不到该路径的文件

vue动态路由数据库表设计

image.png

image.png

USE [CHMS]
GO
 
/****** Object:  Table [dbo].[SysMenu]    Script Date: 2021/5/5 23:09:37 ******/
SET ANSI_NULLS ON
GO
 
SET QUOTED_IDENTIFIER ON
GO
 
CREATE TABLE [dbo].[SysMenu](
	[Id] [int] IDENTITY(10000,1) NOT NULL,
	[Name] [nvarchar](32) NOT NULL,
	[ParentId] [int] NULL,
	[Path] [nvarchar](256) NOT NULL,
	[Weight] [int] NOT NULL,
	[ExtData] [text] NULL,
	[UpdateTime] [bigint] NOT NULL,
	[CreateTime] [bigint] NOT NULL,
	[Updator] [varchar](16) NULL,
	[Creator] [varchar](16) NOT NULL,
	[Children] [text] NULL,
	[Component] [nvarchar](256) NULL,
	[Redirect] [nvarchar](256) NULL,
	[Title] [nvarchar](50) NULL,
	[Icon] [nvarchar](50) NULL,
	[Hidden] [nvarchar](50) NULL,
	[Tag] [nvarchar](50) NULL,
	[Class_layer] [int] NULL,
	[Action_type] [nvarchar](500) NULL,
	[DyId] [nvarchar](50) NULL,
 CONSTRAINT [PK_SysMenu] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
 
ALTER TABLE [dbo].[SysMenu] ADD  CONSTRAINT [DF_SysMenu_Weight]  DEFAULT ((0)) FOR [Weight]
GO
 
ALTER TABLE [dbo].[SysMenu] ADD  CONSTRAINT [DF_SysMenu_UpdateTime]  DEFAULT ((0)) FOR [UpdateTime]
GO
 
ALTER TABLE [dbo].[SysMenu] ADD  CONSTRAINT [DF_SysMenu_CreateTime]  DEFAULT ((0)) FOR [CreateTime]
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'数据ID' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Id'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'路由名称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Name'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'父级数据ID' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'ParentId'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'路由地址' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Path'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'排序权重' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Weight'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'扩展数据' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'ExtData'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'更新时间(JS时间戳)' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'UpdateTime'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间(JS时间戳)' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'CreateTime'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'更新人用户名' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Updator'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建人用户名' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Creator'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'嵌套子类' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Children'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'组件地址' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Component'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'跳转' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Redirect'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'标题' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Title'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'图片' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Icon'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'是否隐藏' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Hidden'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'显示名称' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Tag'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'层级' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Class_layer'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'权限集合' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'Action_type'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'调用ID' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu', @level2type=N'COLUMN',@level2name=N'DyId'
GO
 
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'系统菜单' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SysMenu'
GO
 

根据路由表的component列名的路径,创建对应的文件,输入账号,密码后登录报错没有了

但是侧边栏并没有动态渲染,我们需要使用vuex里的动态路由表,根据vuex中可访问的路由渲染侧边栏组件

动态渲染

//在src/layout/components/sidebar/index.vue
import store from '@/store'
export default {
  ......
  computed: {
    routes() { 
     //将通用路由表和动态需要根据权限加载的路由表合并,通过v-for动态渲染
      return this.$router.options.routes.concat(store.getters.menus)  
    },
    ......
  },
}

效果图

admin登录

image.png

销售员登录

image.png