在一次项目中,需要定制一款树形组件,看了下UI组件
中样式都不是太能满足,然后就自己实现了一个版本。感觉还是有不少知识点的,对于递归 的使用更熟练了,向上 递归查找,以及向下 递归查找。
效果如下:

相关功能支持:
-
- 数据结构设计
-
vue
递归组件实现
-
- 支持单选、多选
- 3-1. 选中一个子元素,父元素需要半选
indeterminate = true
- 3-2. 选中了所有子元素,父元素需要勾选
checked = true
- 3-3. 选中了带子级的元素,需要把所有自己都勾选
- 3-4. 取消选中同上
数据结构设计
算是一个典型的树形数据结构了如下
js
const treeData = [
{
id: '1', // id 唯一
name: 'tree-1', // 展示的名字
parentId: 'a1', // 父元素id,我这里其实没有用到
isParent: true, // 是否是父元素,用来判断是否要进行递归等
checked: false, // 是否选中
visible: false, // 是否展开子级
children: [
id: '1-1',
name: 'tree-1-1',
parentId: 'a1',
isParent: false,
checked: false,
visible: false,
]
},
]
其实isParent
可以不用直接用children?.length
去判断逻辑,但是有的场景在交互设计 上不会一个接口返回全量数据 ,会在每次点击 后去获取下一层接口 数据。
实现vue的递归组件
我分别用vue options 和组合式API 来实现下递归组件,以及**emit**
第一种options
方法,name值必须需要定义如下:
vue
<template>
<div class="pro-tree-checkbox":key="uuid">
<div v-for="(item, idx) in data" :key="idx">
<div class="pro-tree-checkbox__item" @click="onClick(item, idx)">
<!-- 文本 -->
<VantCheckbox
@click.stop
shape="square"
@update:modelValue="onCheckboxChange(item)"
:modelValue="item.checked"
:indeterminate="item.indeterminate"
>
{{ item.name }}
</VantCheckbox>
<!-- 使用 item.children.length 判断是不是父级,是父级需要有个展开收取的按钮 -->
<span v-if="item.children && item.children.length > 0">
<right-outlined v-if="!item.visible" />
<down-outlined v-else />
</span>
</div>
<!-- 这里就开始递归下一层 -->
<div class="pro-tree-checkbox__children" v-if="item.children && item.children.length > 0" v-show="item.visible">
<ProTreeCheckbox :data="item.children" @change="onChange" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ProTreeCheckbox', // 必须定义
...
}
</script>
组合式API 使用defineOptions
去定义name值
js
import { defineOptions } from 'vue'
defineOptions({
name: 'ProTreeCheckbox',
inheritAttrs: false
})
接下来实现展开收起事件:
js
const onClick = (item, idx) => {
if (item.children && item.children.length > 0) {
// eslint-disable-next-line vue/no-mutating-props
props.data[idx] = {
...item,
visible: !item.visible
}
uuid.value = Math.random()
}
}
我是直接更改的props
的值,这么会方便很多,不然就需要递归去传递emit
事件,然后用id去查对应数据更改visible
,请注意 我的uuid.value
重新赋值了一个随机数 ,用来更新当前递归组件(<div class="pro-tree-checkbox":key="uuid">...</div>
)。
接下来实现vue递归向上 传emit
事件,请注意下面一段代码
vue
<VantCheckbox
@click.stop
shape="square"
@update:modelValue="onCheckboxChange(item)"
:modelValue="item.checked"
:indeterminate="item.indeterminate"
>
{{ item.name }}
</VantCheckbox>
...
<ProTreeCheckbox :data="item.children" @change="onChange" />
onCheckboxChange
是绑定给checkbox
多选框的change事件:
js
import { defineEmits } from 'vue'
const emit = defineEmits(['change'])
const onCheckboxChange = item => {
emit('change', item)
}
接下来实现子组件递归传上来的事件接收@change="onChange"
:
js
const onChange = item => {
emit('change', item)
}
如果有三级树形数据,点击了最后一层触发事件应该是这样的

实现多选的相关逻辑
整体其实就是围绕着点击的元素进行数据更新,具体逻辑如下:
- 选中:父级判断为全选、半选, 子级为全选
- 取消选中:父级判断为全选、半选,子级为不选
咱们先实现俩个公用向下递归全选,全不选函数如下:
js
// 全选
const checkedAllData = node => {
node.indeterminate = false
node.checked = true
if (node?.children?.length) {
for (const child of node.children) {
checkedAllData(child)
}
}
}
// 全不选
const uncheckedAllData = node => {
node.indeterminate = false
node.checked = false
if (node?.children?.length) {
for (const child of node.children) {
uncheckedAllData(child)
}
}
}
以上两个函数就是个递归处理数据全选、全不选 ,就不在多说了,下面会直接去使用。
接下来咱们实现向下查找 到当前点击的节点处理的方法,入参currentFunc
其实就是上面的全选、全不选函数
js
// 向下查找 子级为全选、全不选
function downUpdataChecked(node, targetId, currentFunc) {
if (node.id === targetId) { // 以ID 做对比
currentFunc(node)
return
}
if (node?.children?.length) {
for (const child of node.children) {
if (child.id === targetId) {
currentFunc(child)
break
}
if (child?.children?.length) {
downUpdataChecked(child, targetId, currentFunc)
}
}
}
}
以上就是查找到该节点,并更改状态
现在咱们写向上查找 ,更改父元素的全选、全不选、半选状态
js
// 向上查找 更新 父级判断为全选、半选
function upUpdataChecked(node, targetId) {
if (node.id === targetId) {
return true
}
if (!node?.children) return false
for (const child of node.children) {
if (upUpdataChecked(child, targetId)) {
const isChildrenCheckedAll = node.children.every(item => item.checked) // 是否全部选中
const isChecked = node.children.some(item => item.checked || item.indeterminate) // 是否有选中
if (isChildrenCheckedAll) {
node.indeterminate = false
node.checked = true
} else if (isChecked) {
node.indeterminate = true
node.checked = false
} else {
node.indeterminate = false
node.checked = false
}
return true
}
}
return false
}
以上就是向上查找更改父元素的状态
使用以上两个方法downUpdataChecked
和upUpdataChecked
的代码如下:
js
export const checkedHandle = (treeData, value) => {
const isChecked = value.checked
const currentFunc = isChecked ? uncheckedAllData : checkedAllData
treeData.forEach(item => {
downUpdataChecked(item, value.id, currentFunc)
upUpdataChecked(item, value.id)
})
}
ztree组件
ztree 需要引入JQ
, 如果不是很介意引入jq
,还是可以选择的。
总结
组件划分、函数处理数据,模块划分好,实现起来就省劲很多了。
本篇文章主要分享了:
-
- vue 递归组件的实现以及传值
-
- 递归处理相关数据