首先官网上的树形控件教程地址为Element - The world's most popular Vue UI framework
案例一:
要实现这种类型的树控件,并且后边相关操作:
1.1后端准备工作
首先,数据库表为:
查询接口返回的实体类为:
@Data
@NoArgsConstructor
@RequiredArgsConstructor // 有参构造
@EqualsAndHashCode(callSuper = false ,of = "name")// 表示以name去重写的Equals和HashCode
@Accessors(chain = true)
@TableName("t_department")
@ApiModel(value="Department对象", description="")
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "部门名称")
@Excel(name = "部门")
@NonNull
private String name;
@ApiModelProperty(value = "父id")
private Integer parentId;
@ApiModelProperty(value = "路径")
private String depPath;
@ApiModelProperty(value = "是否启用")
private Boolean enabled;
@ApiModelProperty(value = "是否上级")
private Boolean isParent;
@ApiModelProperty(value = "子部门列表")
@TableField(exist = false)
private List<Department> children;
@ApiModelProperty(value = "返回结果,存储过程使用")
@TableField(exist = false)
private Integer result;
}
查询接口返回的数据格式:通过属性 children来判断是否有子节点
[
{
"id": 1,
"name": "股东会",
"parentId": -1,
"depPath": ".1",
"enabled": true,
"isParent": true,
"children": [
{
"id": 2,
"name": "董事会",
"parentId": 1,
"depPath": ".1.2",
"enabled": true,
"isParent": true,
"children": [
{
"id": 3,
"name": "总办",
"parentId": 2,
"depPath": ".1.2.3",
"enabled": true,
"isParent": true,
"children": [
{
"id": 4,
"name": "财务部",
"parentId": 3,
"depPath": ".1.2.3.4",
"enabled": true,
"isParent": false,
"children": [],
"result": null
},
{
"id": 5,
"name": "市场部",
"parentId": 3,
"depPath": ".1.2.3.5",
"enabled": true,
"isParent": true,
"children": [
{
"id": 6,
"name": "华东市场部",
"parentId": 5,
"depPath": "1.2.3.5.6",
"enabled": true,
"isParent": true,
"children": [
{
"id": 8,
"name": "上海市场部",
"parentId": 6,
"depPath": "1.2.3.5.6.8",
"enabled": true,
"isParent": false,
"children": [],
"result": null
}
],
"result": null
},
{
"id": 7,
"name": "华南市场部",
"parentId": 5,
"depPath": "1.2.3.5.7",
"enabled": true,
"isParent": false,
"children": [],
"result": null
},
{
"id": 9,
"name": "西北市场部",
"parentId": 5,
"depPath": ".1.2.3.5.9",
"enabled": true,
"isParent": true,
"children": [
{
"id": 10,
"name": "贵阳市场",
"parentId": 9,
"depPath": ".1.2.3.5.9.10",
"enabled": true,
"isParent": true,
"children": [
{
"id": 11,
"name": "乌当区市场",
"parentId": 10,
"depPath": ".1.2.3.5.9.10.11",
"enabled": true,
"isParent": false,
"children": [],
"result": null
}
],
"result": null
}
],
"result": null
}
],
"result": null
},
{
"id": 12,
"name": "技术部",
"parentId": 3,
"depPath": ".1.2.3.12",
"enabled": true,
"isParent": false,
"children": [],
"result": null
},
{
"id": 13,
"name": "运维部",
"parentId": 3,
"depPath": ".1.2.3.13",
"enabled": true,
"isParent": false,
"children": [],
"result": null
}
],
"result": null
}
],
"result": null
},
{
"id": 150,
"name": "aaa",
"parentId": 1,
"depPath": ".1.150",
"enabled": true,
"isParent": true,
"children": [
{
"id": 151,
"name": "abbb",
"parentId": 150,
"depPath": ".1.150.151",
"enabled": true,
"isParent": false,
"children": [],
"result": null
}
],
"result": null
},
{
"id": 154,
"name": "ccc",
"parentId": 1,
"depPath": ".1.154",
"enabled": true,
"isParent": false,
"children": [],
"result": null
},
{
"id": 157,
"name": "dddd",
"parentId": 1,
"depPath": ".1.157",
"enabled": true,
"isParent": false,
"children": [],
"result": null
}
],
"result": null
}
]
1.2前端代码
从官网上复制一个模板过来,搜索的话,直接往下找一个有搜索框的,把搜索框复制过来,复制好就可以开始修改,写增删改查的方法调用了。
写好的代码如下:
<template>
<div style="width: 550px;">
<el-input
placeholder="请输入部门名称进行搜索..."
prefix-icon="el-icon-search"
v-model="filterText">
</el-input>
<el-tree
:data="deps"
:props="defaultProps"
:filter-node-method="filterNode"
:expand-on-click-node="false"
ref="tree">
<!-- slot-scope可以自定义树节点里的内容,node当前节点的node对象,data后端对应返回的数据 -->
<span class="custom-tree-node" slot-scope="{ node, data }" style="display:flex; justify-content: space-between;width: 100%;">
<span>{{ data.name }}</span>
<span>
<el-button
type="primary"
size="mini"
class="depBtn"
@click="() => showAddDep(data)">
添加部门
</el-button>
<el-button
type="danger"
size="mini"
class="depBtn"
@click="() => deleteDep(data)">
删除部门
</el-button>
</span>
</span>
</el-tree>
<!-- 添加部门的弹出框 -->
<el-dialog
title="添加部门"
:visible.sync="dialogVisible"
width="30%">
<div>
<table>
<tr>
<td><el-tag>上级部门</el-tag></td>
<td><el-tag>{{pname}}</el-tag></td>
</tr>
<tr>
<td><el-tag>部门名称</el-tag></td>
<td><el-input v-model="dep.name" placeholder="请输入部门名称..."></el-input></td>
</tr>
</table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAddDep">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data(){
return{
// 树的搜索条件
filterText: '',
// 树的数据
deps: [],
// 树的配置
defaultProps: {
children: 'children',
label: 'name'
},
// 添加弹出框
dialogVisible: false,
// 添加的部门数据
dep:{
name:'',
parentId: -1
},
// 上级部门名称
pname: ''
}
},
mounted(){
this.initDeps();
},
methods: {
// 初始化数据
initDeps(){
this.getRequest('/system/basic/department/').then(resp=>{
this.deps = resp;
})
},
// 树的搜索
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
// 添加弹框
showAddDep(data){
console.log(data)
this.dep.parentId = data.id;
this.pname = data.name;
this.dialogVisible = 'true'
},
// 添加
doAddDep(){
this.postRequest('/system/basic/department/',this.dep).then(resp=>{
if(resp.code==200){
resp.obj.children = []
this.addDep2Deps(this.deps,resp.obj);
this.initDep();
this.dialogVisible = false;
}
})
},
initDep(){
this.dep = {
name: '',
parentId: -1
}
this.panme = ''
},
// 添加成功后手动的给树加数据
addDep2Deps(deps,dep){
for(let i = 0; i<deps.length; i++){
let d= deps[i];
if(d.id == dep.parentId){
d.children = d.children.concat(dep);
if(d.children.length>0){
d.isParent = true;
}
return;
}else{
// 递归
this.addDep2Deps(d.children,dep);
}
}
},
// 删除
deleteDep(data){
console.log(data)
if(data.isParent){
this.$message.error("父部门无法删除");
}else{
this.$confirm('此操作将永久['+data.name+']部门, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/system/basic/department/'+data.id).then(resp=>{
if(resp.code==200){
this.removeDepFromDeps(null,this.deps,data.id);
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
},
// 手动删除 (父部门,总部门数据,要删除的部门id)
removeDepFromDeps(p,deps,id){
for(let i=0; i<deps.length; i++){
let d = deps[i];
if(d.id==id){
deps.splice(i,1);
if(deps.length==0){
p.isParent = false;
}
return;
}else{
this.removeDepFromDeps(d,d.children,id);
}
}
}
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
}
}
}
</script>
<style>
.depBtn{
padding: 3px;
}
</style>
需要注意的是:
1:树组件中 :data="deps" 加载树组建的数据。:props="defaultProps" 加载树组件的配置,其中label是给个枝叶的名字对应的字段,children是子节点对应的字段。:filter-node-method="filterNode" 树组件的搜索,filterNode过滤的回调,这个官网拷贝即可。:expand-on-click-node="false" 关闭点击折叠,只有点击前面的小三角才能展开折叠。
2:搜索框绑定的数据filterText,下面监听这个数据的改变,如果改变了,就调用树的filter方法(this.$refs.tree.filter(val);),进行过滤,这个方法的回调在树组件的属性中定义:filter-node-method="filterNode" ,也就是输入框值改变了,会去掉filterNode这个方法,这个方法传入两个值,value是文本框输入值,data是树组件的数据,value为空直接返回,不为空则过滤。
3: 添加部门的时候,添加成功后不能单纯的掉接口,重新请求一次树中的数据,这样的话,每添加一个部门,整个树就会折叠起来。这时候就需要不调接口请求新的数据,在js中操作树中的数据this.deps,让它和数据库保持同步。首先在成功后调用方法: addDep2Deps(deps,dep) 这个方法第一个参数是整个树中的数据,第二个参数是新添加的树的数据(这个数据是添加成功接口回显的数据,也就是添加的这条记录信息)在这个方法中,会循环,找到对应的父节点,给它的子节点拼接数据,修改父节点isParent属性(是否有孩子),之后就一直递归,知道添加完成为止。这个数据添加成功后,清空弹出框中输入的信息,关闭弹出框。
4:删除操作,同样,删除时也要手动的在js中把这条数据删除,不能请求新数据,请求新数据会导致树整个关闭,用户体验十分不好,需要在js中把树中的数据和数据库中进行同步。
调用接口删除成功后,调用removeDepFromDeps(p,deps,id)方法,在js中进行数据删除,调用时,第一个参数传null表示从顶层节点开始往下找,第二个参数是树的数据,第三个参数是要删除部门的id。同样进行循环,添加中比较的是父节点的id,删除是,比较的就是这个叶子的id,如果相同,那就删除,同时,判断把这个叶子删掉后,这个枝下面还有没有叶子,因为有叶子的枝是不能删除的,所以要再判断一下修改isParent的状态,之后还是一样,在一层中没有查询到的话,就递归到下一层去找。
案例二
这种前面带有选择框的,进行授权操作
2.1后端准备
对应的数据库表
对应的实体类:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_menu")
@ApiModel(value="Menu对象", description="")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "url")
private String url;
@ApiModelProperty(value = "path")
private String path;
@ApiModelProperty(value = "组件")
private String component;
@ApiModelProperty(value = "菜单名")
private String name;
@ApiModelProperty(value = "图标")
private String iconCls;
@ApiModelProperty(value = "是否保持激活")
private Boolean keepAlive;
@ApiModelProperty(value = "是否要求权限")
private Boolean requireAuth;
@ApiModelProperty(value = "父id")
private Integer parentId;
@ApiModelProperty(value = "是否启用")
private Boolean enabled;
@ApiModelProperty(value = "子菜单")
@TableField(exist = false) // 告诉mybatisplus这个字段不在表中,查询的时候不要去查
private List<Menu> children;
@ApiModelProperty(value = "角色列表")
@TableField(exist = false)
private List<Role> roles;
}
查询接口返回的数据
{
"code": 200,
"message": "查询成功",
"obj": [
{
"id": 1,
"url": null,
"path": null,
"component": null,
"name": "所有",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 2,
"url": null,
"path": null,
"component": null,
"name": "员工资料",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 7,
"url": null,
"path": null,
"component": null,
"name": "基本资料",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 8,
"url": null,
"path": null,
"component": null,
"name": "高级资料",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
}
],
"roles": null
},
{
"id": 3,
"url": null,
"path": null,
"component": null,
"name": "人事管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 9,
"url": null,
"path": null,
"component": null,
"name": "员工资料",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 10,
"url": null,
"path": null,
"component": null,
"name": "员工奖惩",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 11,
"url": null,
"path": null,
"component": null,
"name": "员工培训",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 12,
"url": null,
"path": null,
"component": null,
"name": "员工调薪",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 13,
"url": null,
"path": null,
"component": null,
"name": "员工调动",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
}
],
"roles": null
},
{
"id": 4,
"url": null,
"path": null,
"component": null,
"name": "薪资管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 14,
"url": null,
"path": null,
"component": null,
"name": "工资账套管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 15,
"url": null,
"path": null,
"component": null,
"name": "员工账套设置",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 16,
"url": null,
"path": null,
"component": null,
"name": "工资表管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 17,
"url": null,
"path": null,
"component": null,
"name": "月末处理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 18,
"url": null,
"path": null,
"component": null,
"name": "工资表查询",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
}
],
"roles": null
},
{
"id": 5,
"url": null,
"path": null,
"component": null,
"name": "统计管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 19,
"url": null,
"path": null,
"component": null,
"name": "综合信息统计",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 20,
"url": null,
"path": null,
"component": null,
"name": "员工积分统计",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 21,
"url": null,
"path": null,
"component": null,
"name": "人事信息统计",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 22,
"url": null,
"path": null,
"component": null,
"name": "人事记录统计",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
}
],
"roles": null
},
{
"id": 6,
"url": null,
"path": null,
"component": null,
"name": "系统管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": [
{
"id": 23,
"url": null,
"path": null,
"component": null,
"name": "基础信息设置",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 24,
"url": null,
"path": null,
"component": null,
"name": "系统管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 25,
"url": null,
"path": null,
"component": null,
"name": "操作日志管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 26,
"url": null,
"path": null,
"component": null,
"name": "操作员管理",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 27,
"url": null,
"path": null,
"component": null,
"name": "备份恢复数据库",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
},
{
"id": 28,
"url": null,
"path": null,
"component": null,
"name": "初始化数据库",
"iconCls": null,
"keepAlive": null,
"requireAuth": null,
"parentId": null,
"enabled": null,
"children": null,
"roles": null
}
],
"roles": null
}
],
"roles": null
}
]
}
2.2前端代码
<template>
<div>
<!-- 添加角色 -->
<div class="permissManaTool">
<el-input size="small" placeholder="请输入角色英文名" v-model="role.name">
<template slot="prepend">ROLE_</template>
</el-input>
<el-input size="small" placeholder="请输入角色中文名" v-model="role.nameZh" @keydown.enter.native="addRole"></el-input>
<el-button size="small" type="primary" icon="el-icon-plus" @click="addRole">添加角色</el-button>
</div>
<!-- 手风琴 -->
<div style="margin-top:10px; width:660px">
<el-collapse v-model="activeName" accordion @change="change">
<el-collapse-item :title="r.nameZh" :name="r.id" v-for="(r,index) in roles" :key="index">
<!-- 卡片 -->
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>可访问资源</span>
<el-button style="float: right; padding: 3px 0; color: #ff0000;" type="text" icon="el-icon-delete" @click="doDeleteRole(r)"></el-button>
</div>
<div>
<!-- 树 -->
<el-tree
show-checkbox
node-key="id"
ref="tree"
:key="index"
:default-checked-keys="selectMenus"
:data="allMenus" :props="defaultProps"></el-tree>
<div style="display:flex; justify-content: flex-end">
<el-button size="mini" @click="cancelUpdate">取消修改</el-button>
<el-button size="mini" type="primary" @click="doUpdate(r.id,index)">确定修改</el-button>
</div>
</div>
</el-card>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
export default {
data(){
return{
role:{
name:'',
nameZh:''
},
activeName : '-1',
roles:[],
allMenus: [],
// 树菜单的属性,children子菜单的属性,label显示出来的名字,都是后端接口返回来的字段名字
defaultProps: {
children: 'children',
label: 'name'
},
// 树中选中的节点
selectMenus: []
}
},
mounted(){
this.initRoles();
},
methods:{
// 初始化所有菜单
initAllMenus(){
this.getRequest('/system/basic/permiss/menus').then(resp=>{
if(resp.code==200){
this.allMenus = resp.obj;
}
})
},
// 根据角色id获取菜单
initSelectdMenus(rid){
this.getRequest('/system/basic/permiss/mid/'+rid).then(resp=>{
if(resp.code==200){
this.selectMenus = resp.obj;
}
})
},
// 获取角色列表
initRoles(){
this.getRequest('/system/basic/permiss/').then(resp=>{
if(resp.code==200){
this.roles = resp.obj;
}
})
},
// 手风琴点击事件,展开rid是每个name,name对应着后端字段id,关闭时rid为空
change(rid){
if(rid){
this.initAllMenus();
this.initSelectdMenus(rid);
}
},
// 修改角色权限
doUpdate(rid,index){
// 拿到这个手风琴下面的树
let tree = this.$refs.tree[index];
// 传true只打印叶子节点的id
let selectedKeys = tree.getCheckedKeys(true);
let url = '/system/basic/permiss/?rid='+ rid;
selectedKeys.forEach(key => {
url += '&mids='+ key;
});
this.putRequest(url).then(resp=>{
if(resp.code==200){
this.activeName = '-1'
}
})
},
cancelUpdate(){
this.activeName = '-1'
},
// 添加角色
addRole(){
if(this.role.name && this.role.nameZh){
this.postRequest('/system/basic/permiss/',this.role).then(resp=>{
if(resp.code==200){
this.initRoles();
this.role.name = '';
this.role.nameZh = '';
}
})
}else{
this.$message.error("所有字段不能为空");
}
},
// 删除角色
doDeleteRole(role){
this.$confirm('此操作将永久删除'+role.nameZh+'角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/system/basic/permiss/role/'+r.id).then(resp => {
if(resp.code == 200){
this.initRoles();
}else{
this.$message.error(resp.message)
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
}
}
</script>
<style>
.permissManaTool{
display: flex;
justify-content: flex-start;
}
.permissManaTool .el-input{
width: 300px;
margin-right: 10px;
}
</style>
值得注意的是:
1:每个角色下面展开的权限树列表用的是手风琴组件(折叠面板)这里还要给树加上key,因为每个手风琴下面都是一个树 折叠面板饿了吗官网
2:树要展示前面的选择框,要给树组件加上show-checkbox属性。:default-checked-keys="selectMenus" 默认选中的key,这个selectMenus需要去data中定义一个数组。
3:在添加时,可通过 let tree = this.$refs.tree[index]; 拿到整个手风琴下面的树,let selectedKeys = tree.getCheckedKeys(true) 获取选中节点的id,修改成功后,把手风琴的折叠属性,定义成-1,折叠即可,因为下次再打开的时候,会去数据库查出这个角色对应菜单的最新数据。