开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情
前言
完成了用户和角色路由的编写,接下来就要完成权限的编写,即权限菜单与权限按钮。但我们首先明确一下这三者之间的关系。一个用户拥有多个角色,每个角色又有多个权限,所以要建立一个角色权限表以便存储两者之间的关系,除此之外还需要建立一个权限表。
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;
};
这样就能够得到权限的树状结构数据了。