手把手教你实现一个vue3+ts+nodeJS后台管理系统(十一)

276 阅读4分钟

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

前言

完成了用户和角色路由的编写,接下来就要完成权限的编写,即权限菜单与权限按钮。但我们首先明确一下这三者之间的关系。一个用户拥有多个角色,每个角色又有多个权限,所以要建立一个角色权限表以便存储两者之间的关系,除此之外还需要建立一个权限表。

image.png

sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
​
-- ----------------------------
-- Table structure for menus
-- ----------------------------
DROP TABLE IF EXISTS `menus`;
CREATE TABLE `menus`  (
  `menu_id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `parent_id` int NOT NULL COMMENT '上级ID',
  `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '标题',
  `sort` int NOT NULL DEFAULT 0 COMMENT '排序',
  `type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '类型',
  `icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图标',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路由名称',
  `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路由组件',
  `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路由地址',
  `redirect` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '跳转地址',
  `permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
  `hidden` tinyint(1) NULL DEFAULT NULL COMMENT '隐藏',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
​
-- ----------------------------
-- Table structure for roles_menus
-- ----------------------------
DROP TABLE IF EXISTS `roles_menus`;
CREATE TABLE `roles_menus`  (
  `role_menu_id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '角色菜单联合表id',
  `role_id` int NOT NULL COMMENT '角色id',
  `menu_id` int NOT NULL COMMENT '菜单id',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`role_menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

建立权限模型

首先还是依据数据库字段一一对应构造模型。这里重点说明一下,每个权限(菜单+按钮)都有对应的父级id(若是顶级菜单则父id为0),然后按照type字段为'C'目录、'M'菜单、为'B'按钮区分出菜单及按钮。path、component对应vue路由中的path路由路径和component路由地址(文件地址)。sort是菜单的排列顺序,hidden字段则是是否在vue前端的菜单栏中显示。

model/menus.js

const Sequelize = require('sequelize');
const moment = require('moment');
const sequelize = require('./init');
const { Op } = Sequelize;
// 定义表的模型
const MenusModel = sequelize.define('menus', {
  menu_id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  parent_id: {
    type: Sequelize.INTEGER,
    defaultValue: 0
  },
  title: {
    type: Sequelize.STRING(255),
    defaultValue: ''
  },
  sort: {
    type: Sequelize.INTEGER,
    defaultValue: 0
  },
  type: {
    type: Sequelize.CHAR(1),
    defaultValue: 'C'
  },
  icon: {
    type: Sequelize.STRING(255)
  },
  name: {
    type: Sequelize.STRING(255)
  },
  component: {
    type: Sequelize.STRING(255)
  },
  path: {
    type: Sequelize.STRING(255)
  },
  permission: {
    type: Sequelize.STRING(255)
  },
  redirect: {
    type: Sequelize.STRING(255)
  },
  hidden: {
    type: Sequelize.TINYINT(1),
    defaultValue: 0
  },
  update_time: {
    type: Sequelize.DATE,
    get() {
      return this.getDataValue('update_time')
        ? moment(this.getDataValue('update_time')).format('YYYY-MM-DD HH:mm:ss')
        : null;
    }
  },
  create_time: {
    type: Sequelize.DATE,
    defaultValue: Sequelize.NOW,
    get() {
      return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss');
    }
  }
});
// 导出菜单模型
module.exports = MenusModel;

而角色权限表只需要在表中通过二者的id来存储关系即可。

model/roles-menus.js

const Sequelize = require('sequelize');
const moment = require('moment');
const sequelize = require('./init');

// 定义表的模型
const RolesMenusModel = sequelize.define('roles_menus', {
  role_menu_id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  role_id: {
    type: Sequelize.INTEGER
  },
  menu_id: {
    type: Sequelize.INTEGER
  },
  create_time: {
    type: Sequelize.DATE,
    defaultValue: Sequelize.NOW,
    get() {
      return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss');
    }
  }
});

module.exports = RolesMenusModel;

获取权限的树状结构数据

在获取表中数据时,要依据父菜单id来组成类似如下所示的结构:

{
  title,
  path,
  component,
  children:[
    {
       title
       ...
    },{...}
  ]
  ...
},
{
  title,
  ...
  children
}

若父id为0,则为顶级菜单。其余若父id为其它菜单的id则为其孩子,以此类推。所以我们得写一个方法来构造此结构。分三个步骤

  • 先在表中查询取出所有菜单项包括按钮项的元数据。(元数据的意思是sequelize查询出来除了数据集sequelize可能还包装了一些配置属性)

  • 通过plain属性将元数据转换为只有第一项的数据集

    如果plain为true,则sequelize将仅返回结果集的第一条记录. 如果是false,它将返回所有记录.

  • 将数据集转换为树状结构

model/menus.js

...
const MenusModel = sequelize.define(...)
// 获得权限的树状数据结构
MenusModel.getListTree = async function (where = {}) {
  let menus = [];
  // 查询数据库获得元数据
  // 有标题入参时
  if (where.title) {
    menus = await MenusModel.findAll({
      where: {
        title: {
          [Op.like]: `%${where.title}%`
        }
      },
      order: [['sort']]
    });
  } else {
    menus = await MenusModel.findAll({
      order: [['sort']]
    });
  }
  // 将元数据转换为单纯的数据集
  const menusArr = menus.map(function (item) {
    return item.get({ plain: true });
  });
  // 将数据集转换为树状结构
  return tools.getTreeData(menusArr, null, 'menu_id');
};
...

将获取树状结构的方法封装成公共工具方法,便于调用。我们现在已经得到了按sort升序排列的权限数组。之后我们做到以下几个步骤。

  • 第一次进去先获取所有数据的父id,然后得到最小的父id(一般为0),对上面权限数组遍历菜单id为此最小父id的即为顶级菜单用一个数组存着。
  • 然后我们能得到这些顶级菜单的菜单id,我们再通过此递归调用查出它的子孩子(若有)

下面我们用代码详细看看

/**
 * 获取树形结构数据
 * @param data 数据
 * @param level 父id层级
 * @param idField 字段名
 * @param pidField 上一级字段名
 * @returns {null|[]}
 */
const getTreeData = function (data, level = null, idField = 'menu_id', pidField = 'parent_id') {
  const tree = [];
  const _level = [];
  // 第一次进来获取所有父id
  if (level === null) {
    data.forEach(function (item) {
      _level.push(item[pidField]);
    });
    level = Math.min(..._level);
  }
  data.forEach(function (item) {
    if (item[pidField] === level) {
      tree.push(item);
    }
  });
  if (tree.length === 0) {
    return null;
  }
​
  // 对于父id为0的进行循环,然后查出父节点为上面结果id的节点内容
  tree.forEach(function (item) {
    if(item.type!=='B'){
        const childData = getTreeData(data, item[idField], idField, pidField);
        if (childData != null) {
          item['children'] = childData;
        }
    }
  });
  return tree;
};

这样就能够得到权限的树状结构数据了。