ElementUI官网

一.树形显示-树形控件

1.树形显示

示例-以谷粒商城项目的商品分类为例

数据中商品分类的数据表,所有的分类数据在同一张表中

image-20230530174911409

商品分类的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.atguigu.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import lombok.Data;

/**
* 商品三级分类
*
* @author JasonGong
* @email JasonGong@gmail.com
* @date 2023-05-19 00:23:36
*/
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 分类id
*/
@TableId
private Long catId;
/**
* 分类名称
*/
private String name;
/**
* 父分类id
*/
private Long parentCid;
/**
* 层级
*/
private Integer catLevel;
/**
* 是否显示[0-不显示,1显示]
*/
private Integer showStatus;
/**
* 排序
*/
private Integer sort;
/**
* 图标地址
*/
private String icon;
/**
* 计量单位
*/
private String productUnit;
/**
* 商品数量
*/
private Integer productCount;

@TableField(exist = false)//这个注解要加,因为这个属性不是数据库中的字段
private List<CategoryEntity> children;//这里的属性名不是瞎起的,与ElementUI中的显示相对应,这里改了,前端也要改

}

封装数据,供前端显示

controller层

1
2
3
4
5
6
7
8
/**
* 查询所有的分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}

service层,这里主要有一个单独的递归方法,使用的时候会改就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 查询所有分类,以树形结构显示
*/
@Override
public List<CategoryEntity> listWithTree() {
//查出所有的分类
List<CategoryEntity> entities = categoryDao.selectList(null);
//组装成父子的树形结构
//找到所有的一级分类
List<CategoryEntity> level1List = entities.stream().filter(categoryEntity ->
//父级分类的id等于0的就是一级分类
categoryEntity.getParentCid() == 0
).map((menu) -> {
//为当前分类的子分类赋值
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
//排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());

return level1List;
}

/**
* 递归查询所有菜单的子菜单
*
* @param root 当前分类
* @param all 所有的分类数据
* @return 子菜单的集合
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(categoryEntity -> {
//1.递归找子菜单
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//2.菜单的排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}

返回数据的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
{
"msg": "success",
"code": 0,
"data": [
{
"catId": 1,
"name": "图书、音像、电子书刊",
"parentCid": 0,
"catLevel": 1,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 22,
"name": "电子书刊",
"parentCid": 1,
"catLevel": 2,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 165,
"name": "电子书",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 166,
"name": "网络原创",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 167,
"name": "数字杂志",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 168,
"name": "多媒体图书",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
}
]
},
{
"catId": 23,
"name": "音像",
"parentCid": 1,
"catLevel": 2,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 169,
"name": "音乐",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 170,
"name": "影视",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 171,
"name": "教育音像",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
}
]
},
略.............................

前端代码

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
data() {
return {
data: [],//发送请求获取的数据(比如 return R.ok().put("data", entities);里面的data)
defaultProps: {
children: 'children',//实体类中的属性(比如:private List<CategoryEntity> children;)
label: 'label'//需要显示的属性的属性名(比如商品分类名,属性名是name,这里就填name)
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
}
}
};
</script>

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {//这里的配置看官方的文档
children: "children",//父节点上的子节点
label: "name",//每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

显示效果

image-20230530181102832

2.树形控件的删除、添加

页面的显示效果

image-20230605145343384

使用的组件 在树形控件的基础上添加相关的代码

image-20230605145433594

添加的时候弹出的对话框使用的组件

image-20230605153935967 image-20230605154629290

前端代码的实现

Tips:删除成功之后依然展开删除的节点

删除和添加的前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<template>
<div>
<!-- 树形控件-->
<!-- :expand-on-click-node="false"设置为点击箭头的时候才会展开,避免点击删除的按钮的时候展开 -->
<!-- show-checkbox表示开启选择框,批量选择-->
<!-- node-key表示在整个树中唯一的表示 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<!-- 添加判断,没有下一级节点的时候就显示删除的按钮 -->
<el-button
v-if="data.children.length <= 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span></el-tree
>
<!-- 添加分类的时候弹出的对话框 -->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {
//这里的配置看官方的文档
children: "children", //父节点上的子节点
label: "name", //每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
expandedKey: [], //需要展开的数组,用于删除之后树形控件仍然展开
dialogVisible: false, //添加菜单的对话框的开启或者关闭
//表单绑定的对象
category: {
name: "", //分类的名称
parentCid: 0, //父类的id
catLevel: 0, //分类的级别
showStatus: 1, //是否显示该分类
sort: 0, //排序
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
//添加分类(弹出的对话框确定按钮执行的方法)
addCategory() {
console.log("提交的数据:", this.category);
//将添加的数据返回给后端
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
message: "菜单添加成功",
type: "success",
});
//刷新一下页面
this.getMenus()
//设置需要默认展开的菜单(依然展开刚才添加的节点)
this.expandedKey = [this.category.parentCid];
});
//关闭对话框
this.dialogVisible = false;
},
//添加分类的方法
append(data) {
//清空一下表单的数据
this.category.name = "";
//打开添加的对话框
this.dialogVisible = true;
//为分类的对象赋值
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1; //防止catLevel是一个字符串
},
//删除节点的方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否要删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才删除的节点)
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

后端代码

删除的后端代码

1
2
3
4
5
6
7
8
9
10
11
/**
* 删除
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds) {
//检查当前删除的菜单,是否被别的地方引用
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}

添加的后端代码

1
2
3
4
5
6
7
8
9
10
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:category:save")
public R save(@RequestBody CategoryEntity category) {
categoryService.save(category);

return R.ok();
}

实现的效果

image-20230605163004850