微信小程序:扁平化的无限级树

在开发公司的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...

相关推荐
Danny_FD23 分钟前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom23 分钟前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试
韦小勇24 分钟前
el-table 父子数据层级嵌套表格
前端
奔赴_向往26 分钟前
为什么 PWA 至今没能「掘进」主流?
前端
小小愿望26 分钟前
微信小程序开发实战:图片转 Base64 全解析
前端·微信小程序
掘金安东尼29 分钟前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
电商API大数据接口开发Cris29 分钟前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
前端·数据挖掘·api
小小愿望30 分钟前
解锁前端新技能:让JavaScript与CSS变量共舞
前端·javascript·css
程序员鱼皮33 分钟前
爆肝2月,我的 AI 代码生成平台上线了!
java·前端·编程·软件开发·项目
天生我材必有用_吴用1 小时前
一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势
前端·vue.js