在开发公司的PC项目的时候,遇到了使用树组件的需求。在PC端直接用element树组件,简单的解决。此刻脑子里突然冒出一个想法,在小程序是否有这种树组件?如果在小程序里实现此功能,如何搞呢?
在uniapp插件市场和各种博客网站上搜索此类组件,有许多这种组件,各种各样的。但是无法满足我的需求。 我想要无限级,支持异步加载的,可选节点的树组件。 网上的插件基本都是缺这个,缺那个。而且都是按满足自己的需求而实现的。
倒是有个插件符合需求,但是用组件递归的方式实现了树组件。在小程序使用此插件,巨吃性能,数据量一旦大一点点就卡住,无法渲染。
所以开发一个扁平化且无限级递归的树组件。 大家学会开发思路以后,可以根据自己的需求开发一个~

实现思路
数据渲染层
如何实现,扁平化的数据用树结构的方式展示呢?


我们看图片的话,发现要展示这种树结构,把各层级的缩进控制即可。所以我们使用扁平化渲染方式,把扁平化数据循环,然后使用paddingLeft样式控制层级缩进。
- 节点容器使用 flex 布局实现水平排列
- 树层级使用 paddingLeft 样式进行缩进完成
- 箭头图标通过 transition 实现平滑旋转动画
- 加载图标使用 keyframes 实现旋转动画
HTML
<view>
<view class="tree-container">
<template v-if="flattenedTree.length">
<!--paddingLeft样式控制缩进-->
<view v-for="node in flattenedTree" :key="node[nodeKey]" class="tree-node"
:style="{ paddingLeft: 20 + node.level * 24 + 'px' }" @click="handleClickNode(node)">
<view class="node-content">
<!--isLeaf:每个节点的isLeaf表示是否有子项-->
<view v-if="node[props.isLeaf]" @click="toggleNode(node)">
<image v-if="node.loading" src="./static/loading.svg" class="loading-spinner" />
<!--isLeaf:expanded 控制是否展开-->
<image v-else src="./static/zhankai.svg" class="arrow"
:style="{ transform: node.expanded ? 'rotate(90deg)' : 'none' }">
</image>
</view>
<!--选择框-->
<checkbox-group @change="checkboxChange(node, $event)">
<label style="display: flex;align-items: center; justify-content: center;">
<view class="checkBox" v-if="isShowCheckbox && node.level >= showLevel">
<checkbox :value="true" :checked="false" style="transform:scale(0.7)" />
</view>
<view class="node-label" :class="{ highlight: node.highlight }">
{{ node[props.label] }} </view>
</label>
</checkbox-group>
</view>
</view>
</template>
<view v-else class="empty-state">
<i class="mdi mdi-file-tree empty-icon"></i>
<p>没有找到匹配的节点</p>
</view>
</view>
</view>
数据处理逻辑
核心:扁平化数据处理
用props 传过来的静态多层级结构的数据,转换成扁平数据。使用栈结构进行深度优先遍历
js
// 将树形数据扁平化
flattenTree() {
const result = [];
// 为了保持顺序,需要反转子节点数组
const stack = [...this.treeData].reverse();
while (stack.length) {
const node = stack.pop();
// 重置高亮状态
node.highlight = false;
result.push(node);
// 如果节点展开且有子节点,将子节点加入栈中
if (node.expanded && node[this.props.children]?.length) {
// 为了保持顺序,需要反转子节点数组
const children = [...node[this.props.children]].reverse();
stack.push(...children);
}
}
this.flattenedTree = result;
},
上面两篇代码是,整个树组件的核心,其余的逻辑处理和数据处理都是根据自己需求可以编写。
下面分享完整版的代码~
完整代码
HTML
<template>
<view>
<view class="tree-container">
<template v-if="flattenedTree.length">
<view v-for="node in flattenedTree" :key="node[nodeKey]" class="tree-node"
:style="{ paddingLeft: 20 + node.level * 24 + 'px' }" @click="handleClickNode(node)">
<view class="node-content">
<view v-if="node[props.isLeaf]" @click="toggleNode(node)">
<image v-if="node.loading" src="./static/loading.svg" class="loading-spinner" />
<image v-else src="./static/zhankai.svg" class="arrow"
:style="{ transform: node.expanded ? 'rotate(90deg)' : 'none' }">
</image>
</view>
<checkbox-group @change="checkboxChange(node, $event)">
<label style="display: flex;align-items: center; justify-content: center;">
<view class="checkBox" v-if="isShowCheckbox && node.level >= showLevel">
<checkbox :value="true" :checked="false" style="transform:scale(0.7)" />
</view>
<view class="node-label" :class="{ highlight: node.highlight }">
{{ node[props.label] }} </view>
</label>
</checkbox-group>
</view>
</view>
</template>
<view v-else class="empty-state">
<i class="mdi mdi-file-tree empty-icon"></i>
<p>没有找到匹配的节点</p>
</view>
</view>
</view>
</template>
js
export default {
name: 'shanTree',
props: {
//数据源
data: Array,
//节点id
nodeKey: {
type: String,
default: 'id'
},
//内容为空时显示
emptyText: {
type: String,
default: '无数据'
},
//配置项
props: {
type: Object,
default() {
return {
children: 'children',
label: 'label',
isLeaf: 'hasChildren'
};
}
},
//是否懒加载
lazy: {
type: Boolean,
default: false
},
//懒加载方法
load: Function,
//节点是否可选
showCheckbox: {
type: Boolean,
default: false
},
//节点是否可选显示层
showLevel: {
type: Number,
default: 0
}
},
data() {
return {
flattenedTree: [],
treeData: [],
selectNode: null, // 当前选中的节点
extendNode: null, // 当前展开的节点
clickNode: null, // 当前点击的节点
}
},
mounted() {
this.treeData = this.data;
this.flattenTree()
},
computed: {
isShowCheckbox() {
return this.showCheckbox && this.showLevel >= 0;
},
},
methods: {
// 将树形数据扁平化
flattenTree() {
const result = [];
// 为了保持顺序,需要反转子节点数组
const stack = [...this.treeData].reverse();
while (stack.length) {
const node = stack.pop();
// 重置高亮状态
node.highlight = false;
result.push(node);
// 如果节点展开且有子节点,将子节点加入栈中
if (node.expanded && node[this.props.children]?.length) {
// 为了保持顺序,需要反转子节点数组
const children = [...node[this.props.children]].reverse();
stack.push(...children);
}
}
this.flattenedTree = result;
},
// 切换节点展开/折叠状态
toggleNode(node) {
this.extendNode = node;
if (node[this.props.children]?.length) {
// 本地节点,直接切换展开状态
node.expanded = !node.expanded;
this.flattenTree();
} else if (node[this.props.isLeaf] && !node[this.props.children]?.length && this.lazy) {
// 需要懒加载的子节点
this.loadFun(node);
}
this.$emit('extendNode', this.extendNode)
},
//选择节点
checkboxChange(node, event) {
if (event.detail.value[0]) {
this.selectNode = node;
} else {
this.selectNode = null;
}
this.$emit('checkboxChange', this.selectNode)
},
// 点击节点
handleClickNode(node) {
this.clickNode = node;
this.$emit('clickNode', node)
},
// 懒加载
loadFun(node) {
this.$nextTick(() => {
this.$set(node, 'loading', true);
this.load(node, (children) => {
this.$set(node, this.props.children, children);
this.$set(node, 'loading', false);
this.$set(node, 'expanded', true);
this.flattenTree();
});
});
},
}
}
结语

作品分享:
shanTree树插件链接:ext.dcloud.net.cn/plugin?id=2...
闪图表情包管理浏览器插件:juejin.cn/post/751636...