扩展 layui 的权限树 authtree,适用于类 RBAC 的权限控制

分享 未结 精帖 189 25463
藏锋入鞘丨
悬赏:20飞吻
layui自身提供一个tree树形菜单,但是并不适用于权限控制中,比如:选择用户组的权限(树形结构),需要使用form提交用户所选权限数据。
项目中遇到了此类需求,所以特意封装了一个扩展用于渲染此类操作。
github: https://github.com/wangerzi/layui-authtree
特别注意:权限树的渲染目标需要在 .layui-form .layui-item下,否则将无法渲染出样式。
环境提示:预览环境需要部署在服务器下,不然无法异步获取权限树的数据
版本更新提示:v0.3 将inputname(表单名)、layfilter(lay-filter属性)和新增的openall(默认展开全部)配置更正为以对象的形式传递,覆盖更新后请注意此变化。

功能演示:

在线演示
最新插件演示地址:
http://authtree.wj2015.com/

沟通交流
QQ群号:789188686


社区期望收集
如果您有好的建议或者想法,可以回帖或者发送到我的邮箱( admin@wj2015.com ),如果建议合适,我将收集于此便于以后更新。


快速上手
由于插件规模扩大和功能的增加,导致插件上手难度有一定的增加。但如果只使用核心功能,其实没有必要去研究插件的所有方法,故在此把此插件解决核心需求的方法展示出来。

<form class="layui-form">
<div class="layui-form-item">
<label class="layui-form-label">角色名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="name" placeholder="请输入角色名称" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">选择权限</label>
<div class="layui-input-block">
<div id="LAY-auth-tree-index"></div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" type="submit" lay-submit lay-filter="LAY-auth-tree-submit">提交</button>
<button class="layui-btn layui-btn-primary" type="reset">重置</button>
</div>
</div>
</form>

layui.config({
base: 'extends/',
}).extend({
authtree: 'authtree',
});

layui.use(['jquery', 'authtree', 'form', 'layer'], function(){
var $ = layui.jquery;
var authtree = layui.authtree;
var form = layui.form;
var layer = layui.layer;
// 一般来说,权限数据是异步传递过来的
$.ajax({
url: 'tree.json',
dataType: 'json',
success: function(data){
var trees = data.data.trees;
// 如果后台返回的不是树结构,请使用 authtree.listConvert 转换
authtree.render('#LAY-auth-tree-index', trees, {
inputname: 'authids[]',
layfilter: 'lay-check-auth',
autowidth: true,
});
}
});
});
至此,一个最基础的权限树就已经完成了,如果想实现演示GIF内的各种扩展功能,请继续往下看,并参考源码

数据库设计和后台程序参考
正在完善中....

函数列表


参数配置:
render 参数配置
render()函数是本插件的核心方法,调用 `render(dst, trees, opt)` 函数时,opt 中可以传递的参数如下


**自动选中父级节点和自动取消父级选中 用法描述:**

自动选中父级节点:开启后,选中某节点,会将其上层所有未选中父节点设为选中,并且将其下层所有节点设为选中;取消选中某节点,其所有子节点均取消。
自动取消父级节点:开启后,取消选中某一子节点,当其兄弟节点均处于未选中状态,自动取消父级节点

两种状态一般同时开启,或者同时关闭,不然可能体验有点奇怪
treeConvertSelect参数配置
`treeConverSelect` 是用于树转换为『单选树』的方法,其中 opt 参数如下


listConvert 参数配置
listConvert 是用于转换列表和树结构的方法,参数比较灵活,调用 `listConvert(list, opt)` 时,opt可传入参数如下



后台编辑权限API代码示例(PHP)
<?php
$role_id = $_GET['role_id'];
$auth_list = .....;// 获取数据库中 权限表数据
$role_auth_list = ....;// 获取数据库中某个角色的 角色-权限表数据

$data = [
'code' => 0,
'msg' => '获取成功',
'data' => [
'list' => $auth_list,
'checkedId' => array_column($role_auth_list, 'authid'),
],
];
echo json_encode($data);
API返回数据示例
{
"code": 0,
"msg": "获取成功",
"data": {
"list": [
{ "id": 1, "name": "用户管理", "pid": 0 },
{ "id": 2, "name": "用户组管理", "pid": 0 },
{ "id": 3, "name": "角色管理", "pid": 2 },
{ "id": 4, "name": "添加角色", "pid": 3},
{ "id": 5, "name": "角色列表", "pid": 3 },
{ "id": 6, "name": "管理员管理", "pid": 0 },
{ "id": 7, "name": "添加管理员", "pid": 6 },
{ "id": 8, "name": "管理员列表", "pid": 6 }
],
"checkedId": [ 1, 2, 3, 4 ]
}
}
列表转树前端使用样例
这里注意 `startPid` 参数的数据类型需要和列表返回的一致,`id` 和 `pid` 的数据类型需一致,如果列表返回的id数据均为字符串,则 startPid 应该为 `'0'`
var trees = authtree.listConvert(res.data.list, {
primaryKey: 'id'
,startPid: 0
,parentKey: 'pid'
,nameKey: 'name'
,valueKey: 'id'
,checkedKey: res.data.checkedId
});
监听事件:

功能概览:
1. 支持无限级渲染结构树
2. 点击深层次节点,父级节点中没有被选中的节点会被自动选中
3. 单独点击父节点,子节点会全部 选中/去选中
4. 支持默认选中(适用于编辑权限)
5. 支持自定义表单名称(上传数据的name)
6. 支持自定义lay-filter用于监听权限树选中(v0.2新增)
7. 支持获取选中叶子结点信息(v0.2新增)
8. 自适应标签名字长度配置(v0.5新增)
9. 支持各种方式花样获取数据(v1.0 新增,具体参考函数表)
10. 支持普通列表转树(v1.1 支持)
11. 支持自动展开所有选中节点(v1.1 支持)
12. 支持列表转树(v1.1 支持)
13. 支持双击展开子节点(v1.1 支持)
14. 支持配置渲染参数、单选树、下拉树(v1.2 支持)

更新记录:
2018-11-19 v1.2 支持单选树、下拉树、展开/收起图标配置
2018-09-23 v1.1 新增自动展开所有选中节点,列表转树,支持双击展开子节点等方法,消除BUG
2018-09-23 v1.0 正式版,方法效率优化以及新增监听事件,消除各种BUG
2018-09-19 v1.0 完善权限树的方法,新增方法请见函数列表和演示样例
2018-05-03 v0.4 新增获取全部数据、全部已选数据、全部未选数据方法,修复编码问题。
2018-05-03 v.03 新增默认展开全部的配置项(openall),并将部分配置项作为可选参数通过对象传递。
2018-03-30 v0.2 修复一级菜单没有子菜单时显示错位的问题,支持获取叶子结点数据,支持自定义lay-filter
2018-03-24 v0.1 最初版本

测试用权限树结构(tree.json):
{
"code": 0,
"msg": "获取成功",
"data": {
"trees":[
{"name": "用户管理", "value": "xsgl", "checked": true, "list": [
{"name": "用户组", "value": "xsgl-basic", "checked": true, "list": [
{"name": "本站用户", "value": "xsgl-basic-xsxm", "checked": true, "list": [
{"name": "用户列表", "value": "xsgl-basic-xsxm-readonly", "checked": true},
{"name": "新增用户", "value": "xsgl-basic-xsxm-editable", "checked": false}
]},
{"name": "第三方用户", "value": "xsgl-basic-xsxm", "checked": true, "list": [
{"name": "用户列表", "value": "xsgl-basic-xsxm-readonly", "checked": true}
]}
]}
]},
{"name": "用户组管理", "value": "sbgl", "checked": true, "list": [
{"name": "角色管理", "value": "sbgl-sbsjlb", "checked": true, "list":[
{"name": "添加角色", "value": "sbgl-sbsjlb-dj", "checked": true},
{"name": "角色列表", "value": "sbgl-sbsjlb-yl", "checked": false}
]},
{"name": "管理员管理", "value": "sbgl-sbsjlb", "checked": true, "list":[
{"name": "添加管理员", "value": "sbgl-sbsjlb-dj", "checked": true},
{"name": "管理员列表", "value": "sbgl-sbsjlb-yl", "checked": false}
]}
]}
]
}
}
测试用列表转树结构(list.json):
{
"code": 0,
"msg": "获取成功",
"data": {
"list": [
{ "id": 1, "name": "用户管理", "alias": "yhgl", "palias": "0" },
{ "id": 2, "name": "用户组管理", "alias": "yhzgl", "palias": "0" },
{ "id": 3, "name": "角色管理", "alias": "yhzgl-jsgl", "palias": "yhzgl" },
{ "id": 4, "name": "添加角色", "alias": "yhzgl-jsgl-tjjs", "palias": "yhzgl-jsgl" },
{ "id": 5, "name": "角色列表", "alias": "yhzgl-jsgl-jslb", "palias": "yhzgl-jsgl" },
{ "id": 6, "name": "管理员管理", "alias": "glygl", "palias": "0" },
{ "id": 7, "name": "添加管理员", "alias": "glygl-tjgly", "palias": "glygl" },
{ "id": 8, "name": "管理员列表", "alias": "glygl-glylb", "palias": "glygl" }
],
"checkedAlias": [ "yhgl", "yhzgl", "yhzgl-jsgl", "yhzgl-jsgl-tjjs" ]
}
}
测试用列表转树结构(list-2.json)
{
"code": 0,
"msg": "获取成功",
"data": {
"list": [
{ "id": 1, "name": "用户管理", "pid": 0 },
{ "id": 2, "name": "用户组管理", "pid": 0 },
{ "id": 3, "name": "角色管理", "pid": 2 },
{ "id": 4, "name": "添加角色", "pid": 3},
{ "id": 5, "name": "角色列表", "pid": 3 },
{ "id": 6, "name": "管理员管理", "pid": 0 },
{ "id": 7, "name": "添加管理员", "pid": 6 },
{ "id": 8, "name": "管理员列表", "pid": 6 }
],
"checkedId": [ 1, 2, 3, 4 ]
}
}
authtree.js
由于插件规模的扩大,不适合直接放在帖子内,请移步 GITHUB 下载,地址: https://github.com/wangerzi/layui-authtree

调用样例(index.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>权限树扩展分享</title>
<link rel="stylesheet" type="text/css" href="layui/css/layui.css">
<script type="text/javascript" src="layui/layui.js"></script>
</head>
<body>
<div class="layui-container">
<div class="layui-row">
<div class="layui-col-md11 layui-col-md-offset1">
<fieldset class="layui-elem-field layui-field-title"><legend>权限树操作演示</legend></fieldset>
<div class="layui-form">
<div class="layui-form-item">
<div class="layui-form-label">普通操作</div>
<div class="layui-form-block">
<button type="button" class="layui-btn layui-btn-primary" onclick="getMaxDept('#LAY-auth-tree-index')">获取树的深度</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="checkAll('#LAY-auth-tree-index')">全选</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="uncheckAll('#LAY-auth-tree-index')">全不选</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="showAll('#LAY-auth-tree-index')">全部展开</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="closeAll('#LAY-auth-tree-index')">全部隐藏</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="getNodeStatus('#LAY-auth-tree-index')">获取节点状态</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-form-label">特殊操作</div>
<div class="layui-form-block">
<button type="button" class="layui-btn layui-btn-primary" onclick="showDept('#LAY-auth-tree-index')">展开到某层</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="closeDept('#LAY-auth-tree-index')">关闭某层后所有的层</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="listConvert('list.json')">列表转树</button>
</div>
</div>
</div>
</div>
<div class="layui-col-md6 layui-col-md-offset1">
<fieldset class="layui-elem-field layui-field-title"><legend>权限树扩展分享</legend></fieldset>
<!-- 此扩展能递归渲染一个权限树,点击深层次节点,父级节点中没有被选中的节点会被自动选中,单独点击父节点,子节点会全部 选中/去选中 -->
<form class="layui-form">
<div class="layui-form-item">
<label class="layui-form-label">角色名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="name" placeholder="请输入角色名称" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">选择权限</label>
<div class="layui-input-block">
<div id="LAY-auth-tree-index"></div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" type="submit" lay-submit lay-filter="LAY-auth-tree-submit">提交</button>
<button class="layui-btn layui-btn-primary" type="reset">重置</button>
</div>
</div>
</form>
</div>
</div>
</div>
</body>
<script type="text/javascript">
layui.config({
base: 'extends/',
}).extend({
authtree: 'authtree',
});
layui.use(['jquery', 'authtree', 'form', 'layer'], function(){
var $ = layui.jquery;
var authtree = layui.authtree;
var form = layui.form;
var layer = layui.layer;
// 初始化
$.ajax({
url: 'tree.json',
dataType: 'json',
success: function(data){
// 渲染时传入渲染目标ID,树形结构数据(具体结构看样例,checked表示默认选中),以及input表单的名字
authtree.render('#LAY-auth-tree-index', data.data.trees, {
inputname: 'authids[]'
,layfilter: 'lay-check-auth'
// ,autoclose: false
// ,autochecked: false
// ,openchecked: true
// ,openall: true
,autowidth: true
});

// PS:使用 form.on() 会引起了事件冒泡延迟的BUG,需要 setTimeout(),并且无法监听全选/全不选
// PS:如果开启双击展开配置,form.on()会记录两次点击事件,authtree.on()不会
form.on('checkbox(lay-check-auth)', function(data){
// 注意这里:需要等待事件冒泡完成,不然获取叶子节点不准确。
setTimeout(function(){
console.log('监听 form 触发事件数据', data);
// 获取选中的叶子节点
var leaf = authtree.getLeaf('#LAY-auth-tree-index');
console.log('leaf', leaf);
// 获取最新选中
var lastChecked = authtree.getLastChecked('#LAY-auth-tree-index');
console.log('lastChecked', lastChecked);
// 获取最新取消
var lastNotChecked = authtree.getLastNotChecked('#LAY-auth-tree-index');
console.log('lastNotChecked', lastNotChecked);
}, 100);
});
// 使用 authtree.on() 不会有冒泡延迟
authtree.on('change(lay-check-auth)', function(data) {
console.log('监听 authtree 触发事件数据', data);
// 获取所有节点
var all = authtree.getAll('#LAY-auth-tree-index');
console.log('all', all);
// 获取所有已选中节点
var checked = authtree.getChecked('#LAY-auth-tree-index');
console.log('checked', checked);
// 获取所有未选中节点
var notchecked = authtree.getNotChecked('#LAY-auth-tree-index');
console.log('notchecked', notchecked);
// 获取选中的叶子节点
var leaf = authtree.getLeaf('#LAY-auth-tree-index');
console.log('leaf', leaf);
// 获取最新选中
var lastChecked = authtree.getLastChecked('#LAY-auth-tree-index');
console.log('lastChecked', lastChecked);
// 获取最新取消
var lastNotChecked = authtree.getLastNotChecked('#LAY-auth-tree-index');
console.log('lastNotChecked', lastNotChecked);
});
authtree.on('deptChange(lay-check-auth)', function(data) {
console.log('监听到显示层数改变',data);
});
}
});
form.on('submit(LAY-auth-tree-submit)', function(obj){
var authids = authtree.getAll('#LAY-auth-tree-index');
console.log('Choosed authids is', authids);
obj.field.authids = authids;
$.ajax({
url: 'tree.json',
dataType: 'json',
data: obj.field,
success: function(res){
layer.alert('提交成功!');
}
});
return false;
});
});
</script>
<script type="text/javascript">
// 获取最大深度样例
function getMaxDept(dst){
layui.use(['jquery', 'layer', 'authtree'], function(){
var layer = layui.layer;
var authtree = layui.authtree;

layer.alert('树'+dst+'的最大深度为:'+authtree.getMaxDept(dst));
});
}
// 全选样例
function checkAll(dst){
layui.use(['jquery', 'layer', 'authtree'], function(){
var layer = layui.layer;
var authtree = layui.authtree;

authtree.checkAll(dst);
});
}
// 全不选样例
function uncheckAll(dst){
layui.use(['jquery', 'layer', 'authtree'], function(){
var layer = layui.layer;
var authtree = layui.authtree;

authtree.uncheckAll(dst);
});
}
// 显示全部
function showAll(dst){
layui.use(['jquery', 'layer', 'authtree'], function(){
var layer = layui.layer;
var authtree = layui.authtree;

authtree.showAll(dst);
});
}
// 隐藏全部
function closeAll(dst){
layui.use(['jquery', 'layer', 'authtree'], function(){
var layer = layui.layer;
var authtree = layui.authtree;

authtree.closeAll(dst);
});
}
// 获取节点状态
function getNodeStatus(dst){
layui.use(['jquery', 'layer', 'authtree', 'laytpl'], function(){
var layer = layui.layer;
var authtree = layui.authtree;
var laytpl = layui.laytpl;

// 获取所有节点
var all = authtree.getAll('#LAY-auth-tree-index');
// 获取所有已选中节点
var checked = authtree.getChecked('#LAY-auth-tree-index');
// 获取所有未选中节点
var notchecked = authtree.getNotChecked('#LAY-auth-tree-index');
// 获取选中的叶子节点
var leaf = authtree.getLeaf('#LAY-auth-tree-index');
// 获取最新选中
var lastChecked = authtree.getLastChecked('#LAY-auth-tree-index');
// 获取最新取消
var lastNotChecked = authtree.getLastNotChecked('#LAY-auth-tree-index');

var data = [
{func: 'getAll', desc: '获取所有节点', data: all},
{func: 'getChecked', desc: '获取所有已选中节点', data: checked},
{func: 'getNotChecked', desc: '获取所有未选中节点', data: notchecked},
{func: 'getLeaf', desc: '获取选中的叶子节点', data: leaf},
{func: 'getLastChecked', desc: '获取最新选中', data: lastChecked},
{func: 'getLastNotChecked', desc: '获取最新取消', data: lastNotChecked},
];

var string = laytpl($('#LAY-auth-tree-nodes').html()).render({
data: data,
});
layer.open({
title: '节点状态'
,content: string
,area: '800px'
,tipsMore: true
});
$('body').unbind('click').on('click', '.LAY-auth-tree-show-detail', function(){
layer.open({
type: 1,
title: $(this).data('title')+'-节点详情',
content: '['+$(this).data('content')+']',
tipsMore: true
});
});
});
}
// 显示到某层
function showDept(dst) {
layui.use(['layer', 'authtree', 'jquery'], function(){
var jquery = layui.jquery;
var layer = layui.layer;
var authtree = layui.authtree;

layer.prompt({title: '显示到某层'}, function(value, index, elem) {
authtree.showDept(dst, value);
layer.close(index);
});
});
}
// 关闭某层以后的所有层
function closeDept(dst) {
layui.use(['layer', 'authtree', 'jquery'], function(){
var jquery = layui.jquery;
var layer = layui.layer;
var authtree = layui.authtree;

layer.prompt({title: '关闭某层以后的所有层'}, function(value, index, elem) {
authtree.closeDept(dst, value);
layer.close(index);
});
});
}
// 转换列表
function listConvert(url) {
layui.use(['layer', 'authtree', 'jquery', 'form', 'code'], function(){
var jquery = layui.jquery;
var layer = layui.layer;
var authtree = layui.authtree;
var form = layui.form;

layer.open({
title: '列表转树演示'
,content: '<fieldset class="layui-elem-field layui-field-title"><legend>列表数据转权限树</legend></fieldset><form class="layui-form"> <div class="layui-form-item"> <label class="layui-form-label">多选权限</label> <div class="layui-input-block"> <div id="LAY-auth-tree-convert-index"></div> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" type="submit" lay-submit lay-filter="LAY-auth-tree-submit">提交</button> <button class="layui-btn layui-btn-primary" type="reset">重置</button> </div> </div></form><pre class="layui-code" id="LAY-auth-tree-convert-code"></pre>'
,area: ['800px', '400px']
,tipsMore: true
,success: function() {
$.ajax({
url: url,
dataType: 'json',
success: function(res){
$('#LAY-auth-tree-convert-code').text(JSON.stringify(res, null, 2));
layui.code({
title: '返回的列表数据'
});
// 支持自定义递归字段、数组权限判断等
// 深坑注意:如果API返回的数据是字符串,那么 startPid 的数据类型也需要是字符串
var trees = authtree.listConvert(res.data.list, {
primaryKey: 'alias'
,startPid: '0'
,parentKey: 'palias'
,nameKey: 'name'
,valueKey: 'alias'
,checkedKey: res.data.checkedAlias
});
// 如果页面中多个树共存,需要注意 layfilter 需要不一样
authtree.render('#LAY-auth-tree-convert-index', trees, {
inputname: 'authids[]',
layfilter: 'lay-check-convert-auth',
// openall: true,
autowidth: true,
});
}
});
},
});
});
}
</script>
<!-- 状态模板 -->
<script type="text/html" id="LAY-auth-tree-nodes">
<style type="text/css">
.layui-layer-page .layui-layer-content{
padding: 20px;
line-height: 22px;
}
</style>
<table class="layui-table">
<thead>
<th>方法名</th>
<th>描述</th>
<th>节点</th>
</thead>
<tbody>
{{# layui.each(d.data, function(index, item) { }}
<tr>
<td>{{item.func}}</td>
<td>{{item.desc}}</td>
<td><a class="LAY-auth-tree-show-detail" href="javascript:;" data-title="{{item.desc}}" data-content="{{item.data.join(']<br>[')}}">查看详情</a>({{item.data.length}})</td>
</tr>
{{# });}}
</tbody>
</table>
</script>
</html>

回帖
  • @cobee码侠 目前 GITHUB 中 master 分支上的代码新增了『禁止被选中』的配置,包括『列表转树』、『渲染树』、『树转下拉树』等地均已支持,文档请参见 GITHUB ,社区文档我会随着下一版更新而更新的。
    https://github.com/wangerzi/layui-authtree
    0 回复
  • @flyer223 貌似,已经过去1个月了,,之前可能没瞅到,,目前组件是没有提供直接生成你截图上的那种结果的。

    有两种情况你可以参考下:
    1. 如果你『上传的值(value)』和『展示的标题(title)』一样的话,那好办,直接调用 getChecked()就可以了。
    2. 如果不一样的话,就只能先生成一个『key为上传到的值,value为展示的标题』的辅助对象了,这样就可以快速的通过 『上传的值(value)』转换为『展示的标题(title)』了。
    0 回复
  • @flyer223 然后,社区的帖子不一定能注意到,如果需要交流可以选择邮件 『admin@wj2015.com 』或者加QQ沟通群 『789188686』,点击链接加入群聊【authtree插件问题交流群】: https://jq.qq.com/?_wv=1027&k=5RfHXaC
    0 回复
  • 我用的 layui-v2.4.5 在我的项目中你的插件有个样式被挡了
    0 回复
  • 请问支持IE8吗?
    0 回复
  • @丿心霾 支持的
    0 回复
  • @高先生_c 你好,我的样例也是用的 2.4.5,但是并没有出现展示中的问题,请问是魔改官方的 checkbox 组件了么?
    样例: http://authtree.wj2015.com/
    0 回复
  • Ken100
    2018-12-21
    @藏锋入鞘丨 用的ASPNetCore
    0 回复
  • 想问下,指定排序的参数么?
    0 回复