vue-element-admin后台管理插件配置Option

335 阅读9分钟

上一篇:介绍vue-element-admin后台管理插件

注意事项写在前面:

  • 插件依赖VUE Element,请确保组件和CSS都正确引入
  • 插件依赖的Element图标需要单独安装
  • 凡是layout加载的管理页面都需要一个根div,它不能为空template也不能多个div并列

Option接口

admin/option.ts

export interface Option {
    webConfig?: webConfig // 基本配置 网站名称,logo
    getUser(): Promise<User | null> // 用于获取用户信息
    logout(): void // 退出登录时回调函数

    adminRoutes: RouteRecordRaw [] // 管理员路由数组
    roleModule?: RoleModule // 角色权限管理模块配置
   }

webConfig

后台管理基础配置

const webConfig = {
    name: "后台管理系统",   //网站名称
    logo: import("@/assets/logo.svg"),   // logo
    loginPath:"/login",    // 登录页面(用于游客越权访问时的跳转)
    errorPath:"/error403"  // 用户越权访问时的"无权访问页面"
}

getUser

插件根据用户角色信息user.role来控制路由访问以及侧边栏的选择性展示,因此option需要传入一个用于获取用户信息的异步函数。getUser的函数签名()=> Promise<User | null>。下面介绍getUser函数的编写。

假如你有这样一个获取 userInfo 的api函数:

import axios from "axios";

const getUserInfo = () => {
    return axios.get("http://localhost/get-info")
}

那么getUser函数可以写成这样

async function getUser(): Promise<User | null> {
    // 重要!从cookies中获取token, 如果token不存在(未登录状态)需要返回null
    if (!Cookies.get("token")) {
        return null
    }
    return getUserInfo().then(resp => <User>resp.data).catch(e => null)
}

但是本例中有一个前提,就是api函数getUserInfo返回的response.data恰好是接口User的一个实现

interface User

// User接口
export interface User {
    id: number,
    role: Role | null // 关于Role后面再讲
}

response.data

{
  "id": 10086,
  "role": {
    "id": 73,
    "name": "超级管理员",
    "desc": "角色描述",
    "pms": [ // 角色权限列表
      ...
    ]
    
  }
  //....省略其它用户信息
}

如果api函数返回的response.data与接口User不符只需要做一下转换即可。关于api接口中的user.role从何而来,后面角色权限管理部分会说明

const response = await getUserInfo()
// return response.data
return {
    id: response.data.xxid,
    role: response.data.xxxRole
}

logout

用于点击管理后台"退出"按钮的回调

// logout 可以是这样
funtion logout(){
    Cookies.remove("token") // 清除cookies
    router.push("/") // 跳转到某个路由
}

adminRoutes

与后台管理相关的所有路由记录 (RouteRecord)都需要从原来的routes中剥离,并重新组织到adminRoutes中。

import {AuthLevel} from '@/admin/permission'

const adminRoutes = [
    {
        path: 'role',
        // redirect: 'https://element-plus.org/', // redirect以http开头会被当作一个外链
        name: 'RoleManage', // 注意adminRoutes中的路由名称必须填写
        meta: {
            title: "角色权限管理",// 侧边栏,任务栏,面包屑中展示的标题
            inSidebar: true, // 是否在侧边栏中展示 默认false
            icon: "lock", // 侧边栏图标,传入string类型时使用element默认图标,import("@/assets/edit.svg")使用自定义图标
            cacheable: false, // 是否启用<keep-alive> 缓存 默认false
            authLevel: AuthLevel.NO_AUTH // 验证级别 import  {AuthLevel} from '@/admin/permission'

        },
        component: () => import('@/admin/views/role/Index.vue')
    }
]

插件会自动将adminRoutes中的路由添加到路由Admin的children中作为Admin的子路由. 因此无需再在外面嵌套一层

// 插件在install中会自动添加管理路由到Admin的children中
router.addRoute({
    path: '/admin',
    name: 'Admin',
    meta: {title: '管理首页'},
    component: () => import('@/admin/layout/Layout.vue'),
    children: [...adminRoutes]
})

path: /admin/role

image

roleModule

roleModule(角色权限管理模块)是整个插件配置中最繁琐的部分。虽然它被定义为Option接口的可选项,但它却是整个插件最为重要的核心。使用roleModule可以真正做到前后端角色权限的统一。

interface RoleModule

// 角色权限模块配置接口
export interface RoleModule {
    pmsTreeData: Permission[] | Ref<Permission[]> // 供选择的权限树数据

    roleList(): Promise<Role[]> // 获取角色列表

    addRole(role: Role): Promise<number> // 添加角色,成功返回的角色ID

    delRole(id: number): Promise<number>// 删除角色,返回影响的行数(成功总是1)

    editRole(role: Role): Promise<number> // 编辑角色,返回影响的行数(成功总是1)

    exceptionHandler?(err: any): void // 用于以上方法的异常处理函数
}

pmsTreeData

pmsTreeData用于定义添加角色时的权限树,也可以将其理解为权限表。它通常是一个Permission[] 类型的数组。 但有时候我们需要根据不同的用户或者角色显示不同的权限树, 那么可以传入一个Ref<Permission[]>或者computed来实现该需求。

image

合理设计pmsTreeData至关重要,因为它不仅控制前端路由的访问权限和某些按钮的显示,还负责限制后端API接口的访问权限,它直接关乎到整个系统的安全。在介绍如何编写pmsTreeData之前我们先了解下Permission

interface Permission

type Permission = PermNode & PermLeaf

interface PermNode {
    name: string // 权限名称,全局唯一不可重复
    requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
    pms?: Permission[] // 权限子节点,用于组织权限树的层次结构
}

interface PermLeaf {
    name: string // 权限名称,全局唯一不可重复
    requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
    api?: string // 需要调用的后端api (相对地址:/role/add )主要用于后端权限控制
    routeName?: string // 路由名称,并不是所有权限都有自己的路由,比如"删除角色"它仅仅属于一个动作,因此这里无需设置
}

// requirePms 不会出现在权限树的节点中,因为它总是作为依赖被选择

Permission其实是PermNodePermLeaf的交叉类型。 PermNode不负责编写特定权限,它作为根茎节点只用作挂载某个模块下的权限叶子节点(PermLeaf),但是PermNoderequirePms可以挂载该模块下的依赖权限。 PermLeaf叶子节点才是真正用于编写权限的节点。也就是说权限只能编写在叶子节点上。我们说挂载某个权限,依赖某个权限指的就是PermLeaf叶子节点

代码演示如何编写如下图所示的pmsTreeData

image
import type {Permission, PermLeaf, PermNode} from "@/admin/permission";

// -------用户管理模块------
const addUser: PermLeaf = {
    name: "添加用户",
    api: "/user/add",
    routeName: "AddUser"
}

const delUser: PermLeaf = {
    name: "删除用户",
    api: "/user/del",
    // routeName: 删除权限仅仅是个操作按钮,而没有专门的路由,因此无需配置routeName
}

const userList: PermLeaf = {
    name: "用户列表",
    api: "/user/list",
    routeName: "UserList"
}
// 用户管理(节点)
const userModule: PermNode = {
    // 用户管理节点下的所有叶子都依赖userList权限,因此将依赖配置在userModule节点下
    requirePms: [userList],
    name: "用户管理",
    // 被依赖权限可以不出现在pms中,这里配置newsList是因为用户列表可以单独授权,也就是"只读模式"
    pms: [userList, addUser, delUser]
}


// ------角色模块------
const roleList: PermLeaf = {
    name: "角色列表",
    api: "/role/del",
    routeName: "RoleList"
}
const addRole: PermLeaf = {
    name: "添加角色",
    api: "/role/add",
    routeName: "AddRole"
}
const delRole: PermLeaf = {
    // 删除角色通常在角色列表中出现,因此它依赖于roleList权限
    requirePms: [roleList],
    name: "删除角色",
    api: "/role/del"
}
const roleModule: PermNode = {
    name: "角色模块",
    pms: [roleList, addRole, delRole]
}

// 系统管理(节点)
const sysManage: PermNode = {
    name: "系统管理",
    pms: [userModule, roleModule]
}

// ------新闻模块------
const newsList: PermLeaf = {
    name: "新闻列表",
    api: "/news/list",
    routeName: "NewsList"
}
const addNews: PermLeaf = {
    name: "添加新闻",
    api: "/news/add",
    routeName: "AddNews"
}

const delNews: PermLeaf = {
    // 删除新闻依赖于newsList权限
    requirePms: [newsList],
    name: "删除新闻",
    api: "/role/del"
}
// 新闻管理(节点)
const newsModule: PermNode = {
    name: "新闻管理",
    pms: [newsList, addNews, delNews]
}

// 一个叶子权限它只有一个routeName 那么它可能是一个外链
const link: PermLeaf = {
    name: "友情链接",
    routeName: "Link",
}

// 最终将所有权限节点组织成pmsTreeData
export const pmsTreeData: Permission[] = [sysManage, newsModule, link]

Ref<Permission[]> 类型的pmsTreeData

// 可以是一个Permission[]
const pmsTreeData = [permission1, permission2]
// 也可以是一个 Ref<Permission[]>
const pmsTreeData = computed(() => {
    if (role === "xxxx") {
        return [permission1, ...]
    } else {
        return [permission2, ...]
    }
})

在设计pmsTreeData时需要注意的是何时使用PremNode,何时使用PremLeaf

roleList 等模块方法

roleList方法用于返回所有的roleList,它被展示在角色管理模块中。roleList 函数签名 :()=> Promise<Role[]>

image

编写roleList: 假如你有这样一个获取角色列表的api函数

import axios from "axios";
export const roleListApi = () => {
    // response.data = [{role1}, {role2}...]
    return axios.get("http://localhost/role-list")
}

那么roleList可以写成

function roleList() {
        return roleListApi().then(resp => <Role[]>resp.data)
    }

addRole, delRole以及editRole可以以此类推

/api/role.ts

import axios from "axios";

export const addRoleApi = (data: Role) => {
    // 成功返回角色ID , response.data = 25 
    return axios.post("http://localhost/role-add", data)
}

export const editRoleApi = (data: Role) => {
    // 成功返回影响的行数 , response.data = 1 
    return axios.post("http://localhost/role-update", data)
}

export const delRoleApi = (id: number) => {
    // 成功返回影响的行数 , response.data = 1 
    return axios.get("http://localhost/role-del", {params: {id: id}})
}

最终的roleModule配置

const roleModule: RoleModule = {
    pmsTreeData: pmsTreeData,
    roleList() {
        return roleListApi().then(resp => <Role[]>resp.data)
    },
    // 添加一个角色
    addRole(role: Role) {
        return addRoleApi(role).then(resp => <number>resp.data)
    },
    // 编辑角色
    editRole(role: Role) {
        return editRoleApi(role).then(resp => <number>resp.data)
    },
    // 删除角色
    delRole(id: number) {
        return delRoleApi(id).then(resp => <number>resp.data)
    }
}

前后端权限控制的统一

在目前流行的前后端分离的开发模式下,后端需要定义角色来限制API接口的调用。 而前端又需要定义角色来控制路由的访问以及侧边栏的展示,甚至细化到某个按钮的显示。在角色定义时前后端往往会产生分歧和冲突。那么有没有一种方案,在前端尽量不涉足后端的情况下,把前后端角色权限结合并统一起来?roleModule正是为了解决这一问题。

回顾下Role及Permission接口:

// 角色接口
export interface Role {
    id?: number // 角色ID
    name: string // 角色名称
    desc?: string // 角色介绍
    pms: Permission[] // 权限列表
}

// 由于`Permission`是`PermNode`和`PermLeaf`的交叉类型,等同如下接口
export interface Permission {
    name: string // 权限名称,全局唯一不可重复
    requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
    api?: string // 需要调用的后端api (相对地址:/role/add )主要用于后端权限控制
    routeName?: string // 路由名称,并不是所有权限都有自己的路由,比如"删除角色"它仅仅属于一个动作,因此这里无需设置
    pms?: Permission[] // 权限子节点,用于组织权限树的层次结构
}

先看Role,本篇上述getUserInfoAPI接口获取的user.role其实就是该接口的实现。在Role中我们定义了一个pms,可以把它当作"角色的权限策略",它由角色权限模块的权限树选择生成(pmsTreeData的子集合),结果是一个数组。其中保存的是角色所拥有的全部Permissionrole由roleModule生成并通过addRole方法提交给后端,后端在业务中比如"分配用户角色"时绑定给了某个user。

再看Permission,后端先是拿到user.role.pms,再根据Permission中的api属性来控制API接口的访问权限, 而插件则是利用routeName属性在路由守卫中控制每个路由的访问权限。

按钮级别的权限控制,自定义指令v-action

<el-button v-action="['/role/add']">添加角色</el-button>

v-action 的原理就是判断当前用户的角色权限,如果当前用户的user.role.pms中不存在api /role/add,那么直接将该元素的display设置为none element.style.display = "none"。因此v-action存在兼容性问题,可能对有些组件并不适用。