前言

在前端使用ElementUI开发中,可能会遇到需要展示选择树状数据的表单组件,但是在ElementUI中,并没有提供能够处理树状数据选择的select选择框组件。在开发中遇到类似的问题,为此也作出了相关的方案总结,方便大家根据自己的使用场景去进行选择使用。主要是针对三种方案的总结,接下来给大家讲述一下!

一、vue + element 树形选择框实现

由于做项目需要一个树形选择器,项目用的也是element-ui框架,然而它自带的选择器组件没有树形选项,又不想引入其他的框架组件,于是利用el-selectel-tree改造了一个,感觉还挺好用的,就封装成了一个组件,如下图:

element-uiel-select组件的选项只能是列表形式:
el-select

改造后的树形选择器:
el-tree-select

此树形选择器组件是基于elment-ui框架的el-selectel-tree组件的基础上改造的,其解决了原el-select组件的选项列表不能是树形的问题,集合了前两个组件的属性和方法封装成了一个组件,引入即可使用。其实现了树形列表、默认展开、默认选中、清空选值等功能,基本上可以满足大部分选择器的使用需求。

主要代码

组合el-select和el-tree组件 => SelectTree.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<el-select :value="valueTitle" :clearable="clearable" @clear="clearHandle">
<el-option :value="valueTitle" :label="valueTitle" class="options">
<el-tree
id="tree-option"
ref="selectTree"
:accordion="accordion"
:data="options"
:props="props"
:node-key="props.value"
:default-expanded-keys="defaultExpandedKey"
@node-click="handleNodeClick">
</el-tree>
</el-option>
</el-select>
</template>

封装组件:

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
<script>
export default {
name: "el-tree-select",
props:{

// 配置项
props:{
type: Object,
default: {
value:'id', // ID字段名
label: 'title', // 显示名称
children: 'children' // 子级字段名
}
},

// 选项列表数据(树形结构的对象数组)
options:{ type: Array, default: [] },

// 初始值
value:{ type: Number, default: null },

// 可清空选项
clearable:{ type:Boolean, default: true },

// 自动收起
accordion:{ type:Boolean, default: false }

},
data() {
return {
valueId: null,
valueTitle:'',
defaultExpandedKey:[]
}
},
mounted(){
this.valueId = this.value, // 初始值
this.initHandle()
},
methods: {
// 初始化值
initHandle(){
if(this.valueId){
this.valueTitle = this.$refs.selectTree.getNode(this.valueId).data[this.props.label] // 初始化显示
this.$refs.selectTree.setCurrentKey(this.valueId) // 设置默认选中
this.defaultExpandedKey = [this.valueId] // 设置默认展开
}
this.initScroll()
},

// 初始化滚动条
initScroll(){
this.$nextTick(()=>{
let scrollWrap = document.querySelectorAll('.el-scrollbar .el-select-dropdown__wrap')[0]
let scrollBar = document.querySelectorAll('.el-scrollbar .el-scrollbar__bar')
scrollWrap.style.cssText = 'margin: 0px; max-height: none; overflow: hidden;'
scrollBar.forEach(ele => ele.style.width = 0)
})
},

// 切换选项
handleNodeClick(node){
this.valueTitle = node[this.props.label]
this.valueId = node[this.props.value]
this.$emit('getValue',this.valueId)
this.defaultExpandedKey = []
},

// 清除选中
clearHandle(){
this.valueTitle = ''
this.valueId = null
this.defaultExpandedKey = []
this.clearSelected()
this.$emit('getValue',null)
},

// 清空选中样式
clearSelected(){
let allNode = document.querySelectorAll('#tree-option .el-tree-node')
allNode.forEach((element)=>element.classList.remove('is-current'))
}

},

watch: {
value(){
this.valueId = this.value
this.initHandle()
}
},
}
</script>

css样式(可以根据自己项目的需求进行自定义):

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
<style scoped>
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item{
height: auto;
max-height: 274px;
padding: 0;
overflow: hidden;
overflow-y: auto;
}
.el-select-dropdown__item.selected{
font-weight: normal;
}
ul li >>>.el-tree .el-tree-node__content{
height:auto;
padding: 0 20px;
}
.el-tree-node__label{
font-weight: normal;
}
.el-tree >>>.is-current .el-tree-node__label{
color: #409EFF;
font-weight: 700;
}
.el-tree >>>.is-current .el-tree-node__children .el-tree-node__label{
color:#606266;
font-weight: normal;
}
</style>

使用Demo:

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
<template>
<div id="app">
<h1>基于Element-UI组件改造的树形选择器</h1>
<!-- 调用树形下拉框组件 -->
<SelectTree
:props="props"
:options="optionData"
:value="valueId"
:clearable="isClearable"
:accordion="isAccordion"
@getValue="getValue($event)"/>
</div>
</template>

<script>
import SelectTree from "./components/treeSelect.vue";
export default {
name: "app",
components: {
SelectTree
},
data() {
return {
isClearable:true, // 可清空(可选)
isAccordion:true, // 可收起(可选)
valueId:20, // 初始ID(可选)
props:{ // 配置项(必选)
value: 'id',
label: 'name',
children: 'children',
// disabled:true
},
// 选项列表(必选)
list:[
{id:1,parentId:0,name:"一级菜单A",rank:1},
{id:2,parentId:0,name:"一级菜单B",rank:1},
{id:3,parentId:0,name:"一级菜单C",rank:1},
{id:4,parentId:1,name:"二级菜单A-A",rank:2},
{id:5,parentId:1,name:"二级菜单A-B",rank:2},
{id:6,parentId:2,name:"二级菜单B-A",rank:2},
{id:7,parentId:4,name:"三级菜单A-A-A",rank:3},
{id:8,parentId:7,name:"四级菜单A-A-A-A",rank:4},
{id:9,parentId:8,name:"五级菜单A-A-A-A-A",rank:5},
{id:10,parentId:9,name:"六级菜单A-A-A-A-A-A",rank:6},
{id:11,parentId:10,name:"七级菜单A-A-A-A-A-A-A",rank:7},
{id:12,parentId:11,name:"八级菜单A-A-A-A-A-A-A-A",rank:8},
{id:13,parentId:12,name:"九级菜单A-A-A-A-A-A-A-A-A",rank:9},
{id:14,parentId:13,name:"十级菜单A-A-A-A-A-A-A-A-A-A",rank:10},
{id:15,parentId:0,name:"一级菜单C",rank:1},
{id:16,parentId:0,name:"一级菜单C",rank:1},
{id:17,parentId:0,name:"一级菜单C",rank:1},
{id:18,parentId:0,name:"一级菜单C",rank:1},
{id:19,parentId:0,name:"一级菜单C",rank:1},
{id:20,parentId:0,name:"一级菜单C",rank:1},
{id:21,parentId:0,name:"一级菜单C",rank:1},
{id:22,parentId:0,name:"一级菜单C",rank:1},
{id:23,parentId:0,name:"一级菜单C",rank:1},
{id:24,parentId:0,name:"一级菜单C",rank:1},
{id:25,parentId:0,name:"一级菜单C",rank:1},
{id:26,parentId:0,name:"一级菜单C",rank:1},
{id:27,parentId:0,name:"一级菜单C",rank:1},
{id:28,parentId:0,name:"一级菜单C",rank:1},
{id:29,parentId:0,name:"一级菜单C",rank:1},
{id:30,parentId:0,name:"一级菜单C",rank:1},
{id:31,parentId:0,name:"一级菜单C",rank:1},
{id:32,parentId:0,name:"一级菜单C",rank:1},
{id:33,parentId:0,name:"一级菜单C",rank:1},
{id:34,parentId:0,name:"一级菜单C",rank:1},
{id:35,parentId:0,name:"一级菜单C",rank:1},
{id:36,parentId:0,name:"一级菜单C",rank:1},
{id:37,parentId:0,name:"一级菜单C",rank:1},
{id:38,parentId:0,name:"一级菜单C",rank:1},
{id:39,parentId:0,name:"一级菜单C",rank:1},
{id:40,parentId:0,name:"一级菜单end",rank:1}
]
};
},
computed:{
/* 转树形数据 */
optionData(){
let cloneData = JSON.parse(JSON.stringify(this.list)) // 对源数据深度克隆
return cloneData.filter(father=>{ // 循环所有项,并添加children属性
let branchArr = cloneData.filter(child=>father.id == child.parentId); // 返回每一项的子级数组
branchArr.length>0 ? father.children=branchArr : '' //给父级添加一个children属性,并赋值
return father.parentId==0; //返回第一层
});
}
},
methods:{
// 取值
getValue(value){
this.valueId = value
console.log(this.valueId);
}
}
};
</script>

<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}

</style>

注意:此树形选择器要求的值(options)必须是树形对象数组,如你的值是扁平数据,需转换成树形数据。

二、使用element-plus官方已推出的TreeSelect树形选择器

element-plus官方TreeSelect组件地址:https://element-plus.gitee.io/zh-CN/component/tree-select.html

使用方法
1、选择一个你喜欢的包管理器

1
2
3
4
5
6
7
8
# NPM
$ npm install element-plus --save

# Yarn
$ yarn add element-plus

# pnpm
$ pnpm install element-plus

2、手动导入TreeSelect树形选择器

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
<template>
<el-tree-select v-model="value" :data="data" :render-after-expand="false" />
</template>
<script>
import { TreeSelect } from 'element-plus'
export default {
components: { TreeSelect },
data() {
return {
value: '',
data: [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
}
}
}
</script>

3、效果图
TreeSelect

三、使用第三方组件vue-treeselect

vue-treeselect是一个开源的组件,研究过后发现还是很方便的,但是官网全英文,很难懂,网上其他资源也有限,不是很透彻,下面大家用最简单的方式讲一下基本的用法,解决你80%的需求应该是没问题的。
官方地址:https://vue-treeselect.js.org/
git地址:https://github.com/riophae/vue-treeselect
效果图:
vue-treeselect

1、vue-treeselect是一个树形的下拉菜单,至于到底有多少节点那就要看你的数据源有多少层了,挺方便的。下面这个这个不用多说吧,下载依赖

1
npm install --save @riophae/vue-treeselect

2、引入组件和样式
至于是全局引入还是单页面引入那就看你自己需求了,我这里列举单页面引入

1
2
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";

3、使用

1
<treeselect v-model="form.parentId" :options="deptOptions" :normalizer="normalizer" placeholder="选择上级目录" @select="change(node)"/>

v-model:双向数据绑定,不用多说

options:树形下拉菜单选项的数据源

normalizer:自定义展示结构字段,说白了把自己的名字换成树规定的名字

select:选择事件

首先看一段数据结构的代码:

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
data: [{
id: 10,
label: '肉类',
children: [{
id: 11,
label: '猪肉'
}, {
id: 12,
label: '牛肉'
}]
},
{
id:20,
label:'水果',
children:[{
id:21,
label:'苹果',
children:[{
id:211,
label:'青苹果'
},{
id:212,
label:'红苹果'
}]
}]
}],

首先说一下这个normalizer这个属性,看着花里胡哨,其实很好理解:

1
2
3
4
5
6
7
8
9
10
11
normalizer(node) {
//当子节点也就是children=[]时候去掉子节点
if (node.children && !node.children.length) {
delete node.children;
}
return {
id: node.catalogueId,
label: node.catalogueName,
children: node.children
};
},

先把你数据里所有的children为空的节点都删掉,然后规定三个字段:id,label,children来更方便的操作数据;

参数node就是每个节点,通俗说就是每隔子项数据;

id就是你v-model获取到的值,就相当于opiton里的value;

label就是你要展示的值,让用户看到的东西,就相当于option里的label;

children也不用说了,你数据的子节点;

说的在在在通俗点,有可能你的后端工程师给你的数据id叫ids,label叫value,children叫content,但是树只认识id,label,children,那你给他对应上就行了,就这么简单。

下面就是选择事件了,这个好理解,参数node是当前的节点,你可以得到他做你想做的操作

1
2
3
4
change(node){
this.aa=node.firstName
...
}

其他的参数、事件以及插槽的应用大家可以自行去查看官方api,相信能够解决大家项目中的大部分的问题。