vue动态权限路由

121 篇文章 7 订阅
100 篇文章 5 订阅

任何一个管理系统几乎都存在权限管理和动态路由,它一般和用户管理一同出现,可能有的人觉得这个东西每个管理系统都存在,觉得这个模块没有这么的重要;而在我的认知里,像权限控制和用户管理是管理系统中最重要的一部分,它是整个管理系统中最基础的部分,只有这部分完善了,其他模块才可以非常顺利的进行开发。

本篇文章通过文字+图片+动图演示的形式介绍了一个最基本的动态权限路由实现思路以及实现过程,阅读这篇文章你可能会收获:

  1. 用户管理部分的E-R图(简单版);

  2. 通过plop脚手架快速创建同类型文件;

  3. pinia如何使用;

  4. 如何使用mock

  5. 如何编写动态权限路由

PS:数据来源于mock、前端框架是Vue3、状态管理是Pinia、UI框架是ElementPlus。

🍈 实现思路

为了方便理解,我这里画了一张图,如下:

现在我们来详细讲解一下思路:

  1. 用户登录获取用户id或者token;

  2. 根据token或者用户id去获取用户对应的权限;

  3. 获取到用户权限后进行缓存,并存储到pinia中;

  4. 根据获取的用户权限生成对应的菜单;

  5. 根据编写好的route结合用户权限生成对应的router,并通过addRoute添加到路由实例中。

🍉 数据库设计

想要实现动态路由,最好的方式是通过后端配合实现,这里讲解一下数据库如何设计,先来看一下E-R图:

这里是最简单的一个版本,你可以根据这个E-R图进行扩展。

根据E-R图可以得知的数据库表如下:

  • 用户表

  • 角色表

  • 权限表

  • 角色权限表

其中,角色条件表中的地址,对应Route中的path选项(你也可以换一个字段,总之就是需要权限表中存在一个字段与****中相对应)。

我们还可以将路由的**配置在权限表中,比如****、******等信息。

用户和角色是一对多的关系,也就是说一个用户只有一个角色,也可以根据你的系统进行调整为多对多,无非就是增加一个中间表而已。

🍊 mock数据

如果你有后端的话,这一步就可以直接跳过了,这里我为了演示模拟了一些数据。

首先安装依赖

npm i vite-plugin-mock

第二步在vite.config.ts中只用这个插件,示例代码如下:

import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig(({ mode }: ConfigEnv) => {
  return {
    plugins: [
      // 配置mock
      viteMockServe({
        mockPath: '/mock',
        localEnabled: true,
      }),
    ],
  }
})

然后自己造一些假数据,可以参考,如下;

export const userList = [
  {
    id: 0,
    name: '短暂又灿烂的',
    role: {
      roleId: 0,
      name: 'superAdmin',
    },
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
  {
    id: 1,
    name: '臭小甜',
    role: {
      roleId: 1,
      name: 'admin',
    },
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
  {
    id: 2,
    name: '笨小贝',
    role: {
      roleId: 2,
      name: 'user',
    },
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
]
export const roleList = [
  {
    id: 0,
    name: 'superAdmin',
    // 权限列表的id
    permission: [0, 1, 2, 4, 5, 6, 7],
    permissionNames: [],
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
  {
    id: 1,
    name: 'admin',
    // 权限列表的id
    permission: [0, 1, 2, 4, 5],
    permissionNames: [],
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
  {
    id: 2,
    name: 'user',
    // 权限列表的id
    permission: [0, 1, 4],
    permissionNames: [],
    createTime: '2022-05-05',
    updateTime: '2022-05-05',
  },
]
export const permissionList = [
  {
    id: 0,
    name: '工作台',
    type: 0,
    pid: null,
    path: '/dashboard/workplace',
  },
  {
    id: 1,
    name: '数据可视化',
    type: 0,
    pid: null,
    path: '/visualization',
  },
  {
    id: 2,
    name: '系统管理',
    type: 0,
    pid: null,
    path: '/system',
  },
  {
    id: 4,
    name: 'ECharts图表',
    type: 1,
    pid: 1,
    path: '/visualization/echarts',
  },
  {
    id: 5,
    name: '用户管理',
    type: 1,
    pid: 2,
    path: '/system/user',
  },
  {
    id: 6,
    name: '角色管理',
    type: 1,
    pid: 2,
    path: '/system/role',
  },
  {
    id: 7,
    name: '权限管理',
    type: 1,
    pid: 2,
    path: '/system/permission',
  },
]

然后编写一些假的接口,如下代码展示了如何编写一个接口:  

const mockList: MockMethod[] = [
  {
    url: '/mock/login',
    method: 'post', // 请求方式
    statusCode: 200, // 返回的http状态码
    response: opt => { // opt 对象中包含 url  body  query  headers
      return {
        // 返回的结果集
        statusCode: 200,
        desc: '登录成功',
        result: {
          name: '短暂又灿烂的',
        },
      }
    },
  },
]
export default mockList

我伪造的接口可以参考;如下;

import { MockMethod } from 'vite-plugin-mock'
import { userList, permissionList, roleList } from './data'
export const listToTree = (array: any[]) => {
  const arr = JSON.parse(JSON.stringify(array))
  const result = []
  const map = new Map()
  arr.forEach(item => {
    map.set(item.id, item)
  })
  for (const item of arr) {
    // 判断pid是否在map中,如果在说明这是一个子节点
    if (map.has(item.pid)) {
      if (!map.get(item.pid).children) {
        // 判断是否存在children属性,如果不存在则添加
        map.get(item.pid).children = []
      }
      map.get(item.pid).children.push(item)
    } else {
      // 直接放到数组中,作为父节点
      result.push(item)
    }
  }
  return result
}
const mockList: MockMethod[] = [
  {
    url: '/mock/login',
    method: 'post', // 请求方式
    statusCode: 200, // 返回的http状态码
    response: opt => {
      console.log(opt)

      return {
        // 返回的结果集
        statusCode: 200,
        desc: '登录成功',
        result: {
          name: '短暂又灿烂的',
        },
      }
    },
  },
  {
    url: '/mock/getUserList',
    method: 'get',
    statusCode: 200,
    response: () => {
      return {
        statusCode: 200,
        desc: '获取成功',
        result: userList,
      }
    },
  },
  {
    url: '/mock/getRoleList',
    method: 'get',
    statusCode: 200,
    response: () => {
      roleList.forEach(role => {
        role.permissionNames = []
        for (const i in role.permission) {
          role.permissionNames.push(
            permissionList.find(power => power.id === role.permission[i]).name,
          )
        }
      })
      return {
        statusCode: 200,
        desc: '获取成功',
        result: roleList,
      }
    },
  },
  {
    url: '/mock/getPermissionList',
    method: 'get',
    statusCode: 200,
    response: () => {
      return {
        statusCode: 200,
        desc: '获取成功',
        result: permissionList,
      }
    },
  },
  {
    url: '/mock/getUserDetail',
    method: 'get',
    statusCode: 200,
    response: ({ query }) => {
      const id = query.id
      if (id === undefined) {
        return {
          statusCode: 400,
          desc: 'id必传',
          // 返回最终数据
          result: null,
        }
      }
      const _userList = JSON.parse(JSON.stringify(userList))
      // 获取用户
      const user = _userList.find(u => u.id === parseInt(id))
      // 获取用户权限
      const permissionIdS = roleList.find(
        r => r.id === user.role.roleId,
      ).permission
      // 获取权限列表
      const pList = permissionIdS.map(i => {
        return permissionList.find(p => p.id === i)
      })

      return {
        statusCode: 200,
        desc: '获取成功',
        // 返回最终数据
        result: Object.assign(user, { permissionList: listToTree(pList) }),
      }
    },
  },
]
export default mockList

🍋 请求数据

请求数据我使用之前封装axios,示例代如下:

import request from '/@/service'
import type {
  IPermissionList,
  IRoleList,
  IUserDetail,
  IUserList,
} from './types/mock'
/* more request */
export const getUserDetail = (data: { id: any }) => {
  return request<any, IUserDetail>({
    url: '/mock/getUserDetail',
    method: 'get',
    data,
  })
}

🍍 使用pinia保存数据

我在这里就不多说pinia是什么了,简而言之这个🍍就是Vuex5,基本使用可以参考这里

如下代码展示了如何在pinia中获取数据:

import { defineStore } from 'pinia'
// 获取路由实例
import router from '/@/router'
import {
  getPermissionList,
  getRoleList,
  getUserDetail,
  getUserList,
} from '/@/api/mock'
import type { IUser } from './types'

export const useUserStore = defineStore({
  id: 'user', // id必填,且需要唯一
  // state
  state: (): IUser => {
    return {
      permissionList: [],
      roleList: [],
      userList: [],
      userDetail: undefined,
    }
  },
  // getters
  getters: {
    menuList: state => {
      return state.userDetail?.permissionList
    },
  },
  // actions
  actions: {
    async getData() {
      this.userList = (await getUserList()).result
      this.permissionList = (await getPermissionList()).result
      this.roleList = (await getRoleList()).result
      this.userDetail = (await getUserDetail({ id: this.curId })).result
      // TODO 动态添加路由
    },
  },
})

可以根据需要决定是否进行数据的缓存,如果缓存下次可以直接从缓存中获取,流程图如下:

🍌 动态路由实现

🥭 实现动态菜单

我在getUserDetail接口中返回了该用户的菜单,它的类型定义如下:

export interface IPermissionList {
  id: number
  // 菜单名称
  name: string
  // 菜单类型 1表示一级菜单、2表示二级菜单
  type: number
  // 父级
  pid?: number
  // 地址栏的路径,与route中的path对应,从而找到组件
  path: string
}
interface PermissionList extends IPermissionList {
  children: IPermissionList[]
}
export interface IUserDetail extends IUserList {
  // 权限列表
  permissionList: PermissionList[]
}
interface IUserRole {
  roleId: number
  name: string
}
export interface IUserList {
  id: number
  name: string
  role: IUserRole
  createTime: string
  updateTime: string
}

我们在pinia中编写了一个getters获取了菜单的配置,直接使用就好,实现代码如下:

<script lang="ts" setup>
import { useUserStore } from '/@/store'
const activeIndex = computed(() => {
  return useRoute().path
})
const menuList = computed(() => {
  return useUserStore().menuList
})
</script>
<template>
  <el-menu
    :default-active="activeIndex"
    class="el-menu-demo"
    router
    mode="horizontal"
  >
    <template v-for="menu in menuList" :key="menu.id">
      <!-- 没有子路由的情况 -->
      <el-menu-item v-if="!menu.children" :index="'/main' + menu.path">
        <span>{{ menu.name }}</span>
      </el-menu-item>

      <!-- 存在子路由的情况 -->
      <el-sub-menu v-else :index="'/main' + menu.path">
        <template #title>{{ menu.name }}</template>
        <el-menu-item
          v-for="_menu in menu.children"
          :key="_menu.id"
          :index="'/main' + _menu.path"
          >{{ _menu.name }}</el-menu-item
        >
      </el-sub-menu>
    </template>
  </el-menu>
</template>

最终如下图所示:

🍎 使用plop生成同类型的文件

当我们创建一个路由时,需要编写很多重复易错的代码,且这些代码没有任何含量,除了path和一些meat属性都是一样的。

这个时候我们就可以使用plop这款脚手架来创建同类型的文件,这个脚手架使用也比较简单,如下所示:

首先我们将Plop作为一个npm模块进行安装,命令如下:

npm i plop --dev

第二步,创建项目的根目录创建入口文件,文件名为plopfile.js,然后写入如下代码:

// Plop工作的入口文件,需要导出一个函数
// 此函数接收一个 plop 对象,用于创建生成器任务
export default function (plop) {
    // setGenerator方法接受两个参数,第一个参数作为生成器的名字,第二个参数是生成器的一些配置选项
    plop.setGenerator('main', {
    description: '创建新的路由以及组件',
    // 在命令行看到交互信息
    prompts: [
      {
        type: 'input',
        name: 'pathName',
        message: 'component path:',
      },
      {
        type: 'input',
        name: 'urlName',
        message: 'url:',
      },
      {
        type: 'input',
        name: 'componentName',
        message: 'component name:',
      },
    ],
    // 在命令行中执行的动作,数组中的每一个对象表示一个任务
    actions: [
      {
        // type 为 add 表示添加文件
        type: 'add',
        path: 'src/views/main/{{pathName}}/{{componentName}}.vue',
        templateFile: 'plop-templates/main/vue.hbs',
      },
      {
        type: 'add',
        path: 'src/router/main/{{urlName}}/index.ts',
        // 指定模板文件
        templateFile: 'plop-templates/main/router.hbs',
      },
    ],
  })
}


Plop中使用是Handlebars模板引擎,所以支持插槽的模式。pathName表示我们上面输入的那个name

然后就是创建我们的模板文件,通常在根目录下创建plop-templates文件夹,然后写入相应的模板文件,示例代码如下:

vue.hbs

<script setup lang="ts"></script>

<template>
  <div>{{ componentName }}</div>
</template>

<style scoped></style>

router.hbs

const router = { name: '{{componentName}}', path: '/main/{{pathName}}/{{urlName}}',
component: () => import('/@/views/main/{{pathName}}/{{componentName}}.vue'), }
export default router

最后在命令行中键入如下命令使用

npx plop main

我这里仅仅是做了最基础的配置,你也可以配置meta属性,总之plop的功能还是很强大的。

🍑 动态添加路由

首先我们通过import.meta.glob()函数获取指定目录下的模块,示例代码如下:

// 获取所有路由配置文件的函数
const getMainRouteFileList = async () => {
  const allRoutes: RouteRecordRaw[] = []
  // import.meta.glob 批量导入文件
  const routeFileList = import.meta.glob('../router/main/**')
  for (const path in routeFileList) {
    const mod = await routeFileList[path]()
    allRoutes.push(mod.default)
  }
  return allRoutes
}

然后我们根据menuList动态生成路由配置文件,示例代码如下:

// src\utils\router.ts
// 处理动态路由
/**
 * 1. 获取所有路由配置文件
 * 2. 根据 menuList 动态生成 Route
 */
import type { RouteRecordRaw } from 'vue-router'
// 递归获取Route
const recurseGetRoute = (menus: any[], allRoutes: any[], route: any[]) => {
  // 遍历传递的菜单
  for (const menu of menus) {
    // 如果没有children属性,则将该项直接push到route中
    if (!menu.children) {
      // 找到对应的路由配置文件
      const r = allRoutes.find(
        (route: any) => route.path === '/main' + menu.path,
      )
      // 如果找到匹配的则进行添加
      r && route.push(r)
    } else {
      recurseGetRoute(menu.children, allRoutes, route)
    }
  }
}
// 根据菜单生成路由
export const menuToRoutes = (userMenu: any[]): Promise<RouteRecordRaw[]> => {
  return new Promise(resolve => {
    const routes: RouteRecordRaw[] = []

    getMainRouteFileList().then(res => {
      // 1. 获取所有的routes
      const allRoutes: RouteRecordRaw[] = res
      // 2. 配置该权限的routes
      recurseGetRoute(userMenu, allRoutes, routes)

      resolve(routes)
    })
  })
}

现在我们封装的menuToRoutes方法就可以获取全部的动态路由,在获取数据后进行动态的添加路由即可,示例代码如下:

actions: {
  async getData() {
    /* more request code */
    // 动态添加路由
    if (this.menuList) {
      const routes = await menuToRoutes(this.menuList)
      for (const route of routes) {
        router.addRoute('main', route)
      }
    }
  },
},

最后,也是最重要的一步,在main.ts中调用函数,示例代码如下:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { useUserStore } from './store'
import store from './store'
const app = createApp(App).use(store)

// 获取基础数据
await useUserStore().getData()

app.use(router).mount('#app')

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

短暂又灿烂的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值