LayUI—tree树形结构的使用解析
作者:小马 同学
树形结构在实际开发中很长用到,比如部门管理,权限菜单等。因为用树形结构来展示会显的很清晰明了。
最近写了一个个人博客小项目中用到了LayUI的树形结构,之后写了一个小案例整理一下。
先看一下显示的效果图
点击节点右面会显示对应部门的详情信息,可以修改。可以自定义按钮添加部门,也可以直接用自带的方法对部门进行新增,修改和删除。可以获取选中的节点,根据项目需求(有的需要选中保存)。
先需要引入LayUI的样式文件JS和CSS。
案例对应的实体类Dept
@Entity public class Dept { private Integer id; private String name; //部门名称 private String deptName; //部门负责人 private String phone; //电话号 private String number; //编号 private double idx; //排序 @JsonIgnore private Dept parent; @JsonIgnore private List<Dept> children = new ArrayList<>(); @Id @GeneratedValue public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public double getIdx() { return idx; } public void setIdx(double idx) { this.idx = idx; } @ManyToOne @CreatedBy public Dept getParent() { return parent; } public void setParent(Dept parent) { this.parent = parent; } @OneToMany(cascade=CascadeType.ALL,mappedBy="parent") @OrderBy(value="idx") public List<Dept> getChildren() { return children; } public void setChildren(List<Dept> children) { this.children = children; } public Dept(Integer id, String name, String deptName, String phone, String number, double idx, Dept parent, List<Dept> children) { this.id = id; this.name = name; this.deptName = deptName; this.phone = phone; this.number = number; this.idx = idx; this.parent = parent; this.children = children; } public Dept(Integer id) { this.id = id; } public Dept() { } }
显示LayUI树形菜单,只需要一个标签容器即可。
<div id="dept_tree"> </div>
在案例中还有一些其他样式,比如右边的详情信息,新增按钮等。
完整代码如下
<style type="text/css"> #dept_main, #dept_particulars{ width: 48.5%; display: inline-block; vertical-align: top; padding: 20px; background: white; box-sizing: border-box; } #dept_tree{ margin-top: 20px; } </style>
<div id="dept_main" style="margin-right: 2%;"> <fieldset class="layui-elem-field layui-field-title"> <legend>所有部门</legend> </fieldset> <button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="addDept"><i class="layui-icon"></i>添加部门</button> <button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="gain">获取选中节点</button> <div id="dept_tree"> </div> </div> <div id="dept_particulars"> <fieldset class="layui-elem-field layui-field-title"> <legend>部门详情</legend> </fieldset> <div id="dept_home"> <div class="layui-tree-emptyText">无数据</div> </div> </div>
JS请求数据渲染页面代码,data为请求数据源,当时直接放入的请求链接,好像不行,所以之后才写了一个方法去请求数据源。
layui.use(['tree', 'util'], function() { var tree = layui.tree; var util = layui.util; tree.render({ elem: '#dept_tree', data: getData(), id: 'treeId', showCheckbox: true, //是否显示复选框 onlyIconControl: true }); }); function getData(){ var data = []; $.ajax({ url: "dept/treeload", //后台数据请求地址 type: "post", async:false, success: function(resut){ data = resut; } }); return data; }
tree 组件提供的有以下基础参数,可根据需要进行相应的设置。
参数选项 | 说明 | 类型 | 示例值 |
---|---|---|---|
elem | 指向容器选择器 | String/Object | - |
data | 数据源 | Array | - |
id | 设定实例唯一索引,用于基础方法传参使用。 | String | - |
showCheckbox | 是否显示复选框 | Boolean | false |
edit | 是否开启节点的操作图标。默认 false。
| Boolean/Array | ['update', 'del'] |
accordion | 是否开启手风琴模式,默认 false | Boolean | false |
onlyIconControl | 是否仅允许节点左侧图标控制展开收缩。默认 false(即点击节点本身也可控制)。若为 true,则只能通过节点左侧图标来展开收缩 | Boolean | false |
isJump | 是否允许点击节点时弹出新窗口跳转。默认 false,若开启,需在节点数据中设定 link 参数(值为 url 格式) | Boolean | false |
showLine | 是否开启连接线。默认 true,若设为 false,则节点左侧出现三角图标。 | Boolean | true |
text | 自定义各类默认文本,目前支持以下设定: text: { defaultNodeName: '未命名' //节点默认名称 ,none: '无数据' //数据为空时的提示文本 } | Object | - |
因为tree指定了json数据的键名称,所以后台传递过来的数据对应的键名不一样时需要做一下处理,或者实体类中的属性名就和tree的JSON数据的键名称一样。
键名:
属性选项 | 说明 | 类型 | 示例值 |
---|---|---|---|
title | 节点标题 | String | 未命名 |
id | 节点唯一索引,用于对指定节点进行各类操作 | String/Number | 任意唯一的字符或数字 |
children | 子节点。支持设定选项同父节点 | Array | [{title: '子节点1', id: '111'}] |
href | 点击节点弹出新窗口对应的 url。需开启 isJump 参数 | String | 任意 URL |
spread | 节点是否初始展开,默认 false | Boolean | true |
checked | 节点是否初始为选中状态(如果开启复选框的话),默认 false | Boolean | true |
disabled | 节点是否为禁用状态。默认 false | Boolean | false |
后台请求数据的方法。
@RequestMapping(value = "/treeload") @ResponseBody public Object treeload(){ Sort sort = Sort.by("idx"); List<Dept> dpet = deptService.findByParentIsNull(sort); //查找所有菜单 List<HashMap<String, Object>> result = new ArrayList<>(); //定义一个map处理json键名问题 return fun(dpet, result); } private Object fun(List<Dept> dpet, List<HashMap<String, Object>> result) { for(Dept d : dpet){ HashMap<String, Object> map = new HashMap<>(); map.put("id", d.getId()); map.put("title", d.getName()); map.put("spread", true); //设置是否展开 List<HashMap<String, Object>> result1 = new ArrayList<>(); List<Dept> children = d.getChildren(); //下级菜单 //这里可以根据自己需求判断节点默认选中 /*if(m.getParent() != null || m.getChildren().size() == 0){ map.put("checked", true); //设置为选中状态 }*/ map.put("children", fun(children, result1)); result.add(map); } return result; }
因为这里新建的实体类字段名和tree指定了json数据的键名称不一样,所以这里用了一个fun递归方法处理的。中间可以根据项目需求,根据条件判断是否需要选中该节点。
返回的JSON数据格式
[ { "children": [ //子节点 { "children": [ { "children": [], "id": 30, "title": "测试", "spread": true }, { "children": [], "id": 31, "title": "开发", "spread": true }, { "children": [ { "children": [], "id": 36, "title": "测试节点", "spread": true } ], "id": 32, "title": "测试", "spread": true } ], "id": 2, "title": "技术部", "spread": true }, { "children": [], "id": 19, "title": "财务部", "spread": true } ], "id": 1, //节点id "title": "某某公司", //节点名称 "spread": true }, { "children": [], "id": 33, "title": "测试", "spread": true } ]
设置节点点击回调方法(在加载数据方法tree.render中添加以下代码)。
click: function (obj) { var id = obj.data.id; $("#dept_home").load("dept/show?id="+id); }
把请求过来的详情页面load到右边的div中显示。后台请求方法
@RequestMapping(value = "/show") public void show(DeptForm form, ModelMap map) throws InstantiationException, IllegalAccessException { Dept model = new Dept(); Integer id = form.getId(); Integer parentId = 0; if(id!=null) { model = deptService.findById(id); parentId = model.getParent()==null?0:model.getParent().getId(); } map.put("parentId", parentId); map.put("model", model); }
DeptForm类为一个接收类,其中字段和实体类中一样。根据请求传递过来的id,查询这条数据的详细信息,之后把查询的当前部门详情数据及父级节点id(用于下拉树TreeSelect)传递给详情页面。
show.html详情页面代码。
<meta charset="UTF-8" /> <style type="text/css"> .myData .layui-form-item{ margin: 20px 100px 10px 45px; } .myData .layui-form-label{ width: 85px; } .layui-input-block { margin-left: 120px; } </style> <form class="layui-form myData" action="save" method="post" lay-filter="stuform"> <input type="hidden" name="id" data-th-value="${model.id}" /> <div class="layui-form-item"> <label class="layui-form-label">上级部门:</label> <div class="layui-input-block"> <input type="text" name="parentId" id="tree" lay-filter="tree" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">部门名称:</label> <div class="layui-input-block"> <input type="text" name="name" lay-verify="required" th:value="${model.name}" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">部门负责人:</label> <div class="layui-input-block"> <input type="text" name="deptName" th:value="${model.deptName}" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">电话:</label> <div class="layui-input-block"> <input type="text" name="phone" th:value="${model.phone}" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">编号:</label> <div class="layui-input-block"> <input type="text" name="number" th:value="${model.number}" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">排序:</label> <div class="layui-input-block"> <input type="text" name="idx" value="0" th:value="${model.idx}" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label"></label> <div class="layui-input-block"> <button lay-submit class="layui-btn layui-btn-radius layui-btn-normal" lay-filter="btnSub"> <i class="layui-icon"></i>修改并保存 </button> </div> </div> </form> <script th:inline="javascript"> layui.use(["treeSelect", "form", "tree"], function () { var form = layui.form; var tree = layui.tree; form.render('select'); var treeSelect = layui.treeSelect; treeSelect.render({ // 选择器 elem: '#tree', // 数据 data: 'dept/treeSelect?id='+[[${model.id==null ? 0 : model.id}]], // 异步加载方式:get/post,默认get type: 'post', // 占位符 placeholder: '上级菜单', // 是否开启搜索功能:true/false,默认false search: true, // 一些可定制的样式 style: { folder: { enable: true }, line: { enable: true } }, // 加载完成后的回调函数 success: function (d) { // 选中节点,根据id筛选 treeSelect.checkNode('tree', [[${model.parent == null? parentId: model.parent.id}]]); treeSelect.refresh('tree'); } }); form.on('submit(btnSub)', function (data) { $.post('dept/save', data.field, function (result) { if (result.success) { tree.reload('treeId', {data: getData()}); } layer.msg(result.msg, {offset: 'rb'}); }); return false; }); }); </script>
上级部门使用的是LayUI下拉树显示的,下拉树数据请求方法。关于下拉树的使用,可以访问LayUI下拉树TreeSelect的使用
@RequestMapping(value="/treeSelect") @ResponseBody public Object treeSelect(Integer id) { Sort sort = Sort.by("idx"); Specification<Dept> spec = buildSpec1(); List<Dept> list = deptService.findAll(spec,sort); return buildTree(list, id); } private Object buildTree(List<Dept> list, Integer id) { List<HashMap<String, Object>> result=new ArrayList<>(); for (Dept dept : list) { if(dept.getId() != id) { HashMap<String, Object> node=new HashMap<>(); node.put("id", dept.getId()); node.put("name",dept.getName()); node.put("open", false); node.put("checked", false); if(dept.getChildren().size() != 0) { node.put("children",buildTree(dept.getChildren(), id)); } result.add(node); } } return result; } public Specification<Dept> buildSpec1() { Specification<Dept> specification = new Specification<Dept>() { private static final long serialVersionUID = 1L; @Override public Predicate toPredicate(Root<Dept> root, CriteriaQuery<?> query, CriteriaBuilder cb) { HashSet<Predicate> rules=new HashSet<>(); Predicate parent = cb.isNull(root.get("parent")); rules.add(parent); return cb.and(rules.toArray(new Predicate[rules.size()])); } }; return specification; }
显示的效果。
上面修改并保存后台方法(因为修改和新增共用的一个方法,用id区分的)。
@Override public Object save(DeptForm form) { try { Dept model = new Dept(); Integer id = form.getId(); if(id != null) { model = deptService.findById(id); } //父级菜单id Integer parentId = form.getParentId(); if(parentId == null) { model.setParent(null); }else { model.setParent(new Dept(parentId)); } BeanUtils.copyProperties(form, model,"id", "parent"); deptService.save(model); return new AjaxResult("数据保存成功!"); } catch (Exception e) { return new AjaxResult(false,"数据保存失败"); } }
设置节点操作(在加载数据方法tree.render中添加以下代码)。
edit: ['add', 'update', 'del'], //操作节点的图标 operate: function(obj){ var type = obj.type; //得到操作类型:add、edit、del var data = obj.data; //得到当前节点的数据 var elem = obj.elem; //得到当前节点元素 var id = data.id; var name = data.title; if(type === 'add'){ //增加节点 $.post("dept/save", {parentId: id, name: "未命名"}, function (result) { tree.reload('treeId', {data: getData()}); }) //返回 key 值 return ; } else if(type === 'update'){ //修改节点 $.post("dept/update", {id: id, name: name}, function () { tree.reload('treeId', {data: getData()}); }) } else if(type === 'del'){ //删除节点 $.post("dept/delete", {id: id}, function () { tree.reload('treeId', {data: getData()}); }); }; }
其中operate为操作节点回调方法。
obj.type为操作类型,add为新增,update为修改,edl为删除。obj.data为操作节点后的数据。
新增节点后,向后台发送请求添加节点,save方法和上面修改方法一样,id为新建节点的父级节点id。
修改节点,同样,向后台发送修改请求,并传递对象的id,和修改后的数据作为参数。后台响应方法。
@RequestMapping(value = "/update") @ResponseBody public Object update(DeptForm form) { try { Dept model = deptService.findById(form.getId()); model.setName(form.getName()); deptService.save(model); return new AjaxResult("数据保存成功!"); } catch (Exception e) { return new AjaxResult(false,"数据保存失败"); } }
删除节点同理,传递删除节点的id。删除请求方法。
@RequestMapping(value="/delete") @ResponseBody public Object delete(Integer id) { try { deptService.deleteById(id); return new AjaxResult("数据删除成功"); } catch (Exception e) { return new AjaxResult(false,"数据删除失败"); } }
使用按钮操作树形菜单。
现在页面中定义两个按钮,给按钮添加lay-demo=""属性,并设置属性值,JS通过这个属性值,绑定点击事件。
<button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="addDept"><i class="layui-icon"></i>添加部门</button> <button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="gain">获取选中节点</button>
绑定添加部门和获取选中节点按钮的点击事件的JS代码。
util.event('lay-demo', { addDept: function(othis){ $.get('dept/edit', function(data) { layer.open({ type: 1, title: '新增', area: ['530px'], content: data, btn: ['提交', '退出'], yes: function () { }, success: function (layero, index) { layui.use('form', function () { var form = layui.form; layero.addClass('layui-form'); var submitBtn = layero.find('.layui-layer-btn0'); submitBtn.attr('lay-filter', 'formVerify').attr('lay-submit', ''); layero.keydown(function (e) { if (e.keyCode == 13) { submitBtn.click(); } }); form.on('submit(formVerify)', function (data) { $.post('dept/save', data.field, function (result) { if (result.success) { layer.close(index); tree.reload('treeId', {data: getData()}); } layer.msg(result.msg, {offset: 'rb'}); }); return false; }); }); } }) }) }, gain: function () { var checkData = tree.getChecked('treeId'); var str = JSON.stringify(checkData); $.post('dept/checkedGain', {data: str}, function () { }); layer.alert(JSON.stringify(checkData), {shade:0}); } });
添加部门按钮点击事件,先发送请求到后台,跳转到eidt新增页面,edit.html新增页面代码,和上面的show.html显示部门详情页面差不多。上级部门同样使用的LayUI下拉树显示的,下拉树数据请求方法,和上面的详情页面下拉树请求方法一致。LayUI下拉树TreeSelect的使用。新增后的保存方法也和上面的保存方法一致。
后台请求方法代码,跳转到edit页面。
@RequestMapping(value = "/edit") public void edit(){ }
edit.html页面完整代码如下。
<meta charset="UTF-8" /> <style type="text/css"> .myData .layui-form-item{ margin: 20px 100px 10px 45px; } .myData .layui-form-label{ width: 85px; } .layui-input-block { margin-left: 120px; } </style> <form class="layui-form myData" action="save" method="post" lay-filter="stuform"> <div class="layui-form-item"> <label class="layui-form-label">上级部门:</label> <div class="layui-input-block"> <input type="text" name="parentId" id="tree2" lay-filter="tree2" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">部门名称:</label> <div class="layui-input-block"> <input type="text" name="name" lay-verify="required" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">部门负责人:</label> <div class="layui-input-block"> <input type="text" name="deptName" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">电话:</label> <div class="layui-input-block"> <input type="text" name="phone" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">编号:</label> <div class="layui-input-block"> <input type="text" name="number" class="layui-input" /> </div> </div> <div class="layui-form-item" > <label class="layui-form-label">排序:</label> <div class="layui-input-block"> <input type="text" name="idx" value="0" class="layui-input" /> </div> </div> </form> <script th:inline="javascript"> layui.use(["treeSelect", "form"], function () { var form = layui.form; form.render('select'); var treeSelect = layui.treeSelect; treeSelect.render({ // 选择器 elem: '#tree2', // 数据 data: 'dept/treeSelect', // 异步加载方式:get/post,默认get type: 'post', // 占位符 placeholder: '上级菜单', // 是否开启搜索功能:true/false,默认false search: true, // 一些可定制的样式 style: { folder: { enable: true }, line: { enable: true } }, // 加载完成后的回调函数 success: function (d) { } }); }); </script>
页面效果。
获取选中节点按钮点击事件。如果项目需要保存数据时,就需要获取到选中节点的数据了。这里可以获取到选中节点的数据,之后当参数传递到后台。传递到后台是一个JSON数据的字符串,需要转换一下,这里给推荐大家两个很好用的JSON转换工具net.sf.json.JSONObject和Alibaba Fastjson。这里用的是Alibaba Fastjson,需要引入以下依赖。
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
这里用于输出,新建了一个和tree的json数据的键名称一样的工具类DeptTree,代码如下。
import java.util.ArrayList; import java.util.List; public class DeptTree { private Integer id; private String title; private boolean checked; private boolean spread; private List<DeptTree> children = new ArrayList<>(); public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public List<DeptTree> getChildren() { return children; } public void setChildren(List<DeptTree> children) { this.children = children; } public boolean isChecked() { return checked; } public void setChecked(boolean checked) { this.checked = checked; } public boolean isSpread() { return spread; } public void setSpread(boolean spread) { this.spread = spread; } }
后台接收到传递过来的JSON数据字符串,转换后输出方法。
@RequestMapping(value = "/checkedGain") @ResponseBody public void checkedGain(String data){ List<DeptTree> array2 = JSONArray.parseArray(data, DeptTree.class); treeData(array2); } //递归输出选中数据 private void treeData(List<DeptTree> array2) { for (DeptTree tree: array2){ System.out.println(tree.getTitle()+"==="+tree.getId()); if(tree.getChildren() != null){ treeData(tree.getChildren()); } } }
选中节点,点击获取选中节点数据。
后台对应方法接收到数据,转换后输出结果。
数据拿到了,之后保存方法就简单了。
后台方法代码基本都在上面了,页面全部代码。
<style type="text/css"> #dept_main, #dept_particulars{ width: 48.5%; display: inline-block; vertical-align: top; padding: 20px; background: white; box-sizing: border-box; } #dept_tree{ margin-top: 20px; } </style> <div id="dept_main" style="margin-right: 2%;"> <fieldset class="layui-elem-field layui-field-title"> <legend>所有部门</legend> </fieldset> <button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="addDept"><i class="layui-icon"></i>添加部门</button> <button class="layui-btn layui-btn-sm layui-btn-radius layui-btn-normal" lay-demo="gain">获取选中节点</button> <div id="dept_tree"> </div> </div> <div id="dept_particulars"> <fieldset class="layui-elem-field layui-field-title"> <legend>部门详情</legend> </fieldset> <div id="dept_home"> <div class="layui-tree-emptyText">无数据</div> </div> </div> <script type="text/javascript"> layui.use(['tree', 'util', 'layer'], function() { var tree = layui.tree; var util = layui.util; var layer = layui.layer; tree.render({ elem: '#dept_tree', data: getData(), id: 'treeId', showCheckbox: true, //时候显示复选框 onlyIconControl: true, edit: ['add', 'update', 'del'], //操作节点的图标 click: function (obj) { var id = obj.data.id; $("#dept_home").load("dept/show?id="+id); }, operate: function(obj){ var type = obj.type; //得到操作类型:add、edit、del var data = obj.data; //得到当前节点的数据 var elem = obj.elem; //得到当前节点元素 var id = data.id; var name = data.title; if(type === 'add'){ //增加节点 $.post("dept/save", {parentId: id, name: "未命名"}, function (result) { tree.reload('treeId', {data: getData()}); }) //返回 key 值 return ; } else if(type === 'update'){ //修改节点 $.post("dept/update", {id: id, name: name}, function () { tree.reload('treeId', {data: getData()}); }) } else if(type === 'del'){ //删除节点 $.post("dept/delete", {id: id}, function () { tree.reload('treeId', {data: getData()}); }); }; } }); util.event('lay-demo', { addDept: function(othis){ $.get('dept/edit', function(data) { layer.open({ type: 1, title: '新增', area: ['530px'], content: data, btn: ['提交', '退出'], yes: function () { }, success: function (layero, index) { layui.use('form', function () { var form = layui.form; layero.addClass('layui-form'); var submitBtn = layero.find('.layui-layer-btn0'); submitBtn.attr('lay-filter', 'formVerify').attr('lay-submit', ''); layero.keydown(function (e) { if (e.keyCode == 13) { submitBtn.click(); } }); form.on('submit(formVerify)', function (data) { $.post('dept/save', data.field, function (result) { if (result.success) { layer.close(index); tree.reload('treeId', {data: getData()}); } layer.msg(result.msg, {offset: 'rb'}); }); return false; }); }); } }) }) }, gain: function () { var checkData = tree.getChecked('treeId'); var str = JSON.stringify(checkData); $.post('dept/checkedGain', {data: str}, function () { }); layer.alert(JSON.stringify(checkData), {shade:0}); } }); }); function getData(){ var data = []; $.ajax({ url: "dept/treeload", //后台数据请求地址 type: "post", async:false, success: function(resut){ data = resut; } }); return data; } </script>
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。