当前位置: 首页 > news >正文

微信小程序:实现树形结构组件

实现可折叠展开的树形菜单 + 复选框联动功能

在开发企业级管理系统或权限管理模块时,常常需要展示具有层级关系的数据,如部门-员工结构、权限组等。这时候我们通常会使用“树形结构”来展示数据。本文将手把手教你如何在微信小程序中实现一个支持展开/收起 多级复选框联动 的树形结构组件。


一、最终效果预览

我们将实现如下功能:

支持多级节点
点击箭头展开/收起子节点
点击复选框选择节点
一级节点选中时自动选中所有子节点
获取所有选中的叶子节点信息


二、项目结构说明

本项目包含两个主要部分:

  1. 主页面 Page
  2. 树形组件 TreeNode

组件化设计使得树形结构更易维护、复用。


三、主页面实现

1. WXML 结构

<view class="tree-container"><block wx:for="{{treeData}}" wx:key="id"><tree-node data="{{item}}" index="{{index}}" indent="0"expandedNodes="{{expandedNodes}}"selectedNodes="{{selectedNodes}}"bindtoggle="toggleNode" bindselect="selectNode"isTopLevel="{{true}}"></tree-node></block>
</view>
<!-- <button bind:tap="getSelData"></button> -->

2. JS 数据与方法

通过children标记为子项,如果有数据标识为有子信息就会再次执行树形组件将子项进行展示

getSelData为视图层中提交按钮的方法,可以直接获取叶子节点的信息

Page({data: {treeData: [{id: 1,name: "员工组1",children: []},{id: 2,name: "员工组2",children: []},{id: 3,name: "员工组3",children: [{id: 31,name: "员工组3-1",// children: [//   {//     id: 311,//     name: "员工组3-1-1",//     children: []//   },//   {//     id: 312,//     name: "员工组3-1-2",//     children: []//   }// ]},{id: 32,name: "员工组3-2",children: []}]},{id: 4,name: "员工组4(空组)",children: []}],expandedNodes: [], //默认展开项selectedNodes: []},toggleNode(e) {const {id} = e.detail;const {expandedNodes} = this.data;const newExpandedNodes = new Set(expandedNodes);if (newExpandedNodes.has(id)) {newExpandedNodes.delete(id);} else {newExpandedNodes.add(id);}this.setData({expandedNodes: [...newExpandedNodes] // 转为数组});},selectNode(e) {const {id} = e.detail;const {treeData,selectedNodes} = this.data;// 查找被点击的节点const findNode = (nodes, targetId) => {for (const node of nodes) {if (node.id === targetId) return node;if (node.children) {const found = findNode(node.children, targetId);if (found) return found;}}};const targetNode = findNode(treeData, id);// 判断是否是一级节点(根据您的数据结构)const isTopLevel = treeData.some(item => item.id === id);let newSelectedNodes = [...selectedNodes];if (isTopLevel) {// 一级节点:选中/取消所有子节点const childIds = this.getAllChildIds(targetNode);if (newSelectedNodes.includes(id)) {// 取消选中(移除当前节点和所有子节点)newSelectedNodes = newSelectedNodes.filter(nodeId => nodeId !== id && !childIds.includes(nodeId));} else {// 选中(添加当前节点和所有子节点)newSelectedNodes = [...newSelectedNodes, id, ...childIds];}} else {// 非一级节点保持原逻辑if (newSelectedNodes.includes(id)) {newSelectedNodes = newSelectedNodes.filter(nodeId => nodeId !== id);} else {newSelectedNodes = [...newSelectedNodes, id];}}this.setData({selectedNodes: newSelectedNodes});},// 获取所有子节点IDgetAllChildIds(node) {if (!node.children || node.children.length === 0) return [];return node.children.reduce((acc, child) => {return [...acc, child.id, this.getAllChildIds(child)];}, []);},//查询选中的数据getSelectedNodeDetails() {//获取全部数据// const { treeData, selectedNodes } = this.data;// const result = [];// // 递归查找匹配的节点// const findNodeById = (nodes) => {//   for (const node of nodes) {//     if (selectedNodes.includes(node.id)) {//       result.push({ id: node.id, name: node.name });//     }//     if (node.children && node.children.length > 0) {//       findNodeById(node.children);//     }//   }// };// findNodeById(treeData);// return result;//获取叶子节点const {treeData,selectedNodes} = this.data;const result = [];const isTopLevel = (id) => {return treeData.some(node => node.id === id);};const findLeafNodes = (nodes) => {for (const node of nodes) {if (selectedNodes.includes(node.id)) {// 排除一级节点 && 必须是叶子节点if (!isTopLevel(node.id) && (!node.children || node.children.length === 0)) {result.push({id: node.id,name: node.name});}}if (node.children && node.children.length > 0) {findLeafNodes(node.children);}}};findLeafNodes(treeData);return result;},//获取全部数据getSelData() {console.log(this.getSelectedNodeDetails())}
});

3. JSON 配置引入组件

{"usingComponents": {"tree-node":"/components/TreeNode/index"},"navigationBarTitleText": "测试","navigationBarBackgroundColor": "#f5f5f5","navigationBarTextStyle":"black"
}

四、树形组件实现(TreeNode)

1. WXML 结构

<view class="tree-node" style="margin-left: {{indent}}px;"><view class="tree-line"><!-- 箭头图标 --><view class="toggle-icon {{hasChildren ? '' : 'hidden'}}" bindtap="toggle">{{isExpanded ? '▼' : '▶'}}</view><!-- 复选框 --><view class="checkbox" bindtap="select" style="background: {{isSelected ? '#07c160' : '#fff'}}"><text class="checkbox-icon">{{isSelected ? '✓' : ''}}</text></view><!-- 节点名称 --><text class="node-name">{{data.name}}</text></view><!-- 子节点 --><view class="children-container" wx:if="{{isExpanded && hasChildren}}"><block wx:for="{{data.children}}" wx:key="id"><!-- 修改这里为绝对路径 --><tree-node data="{{item}}" indent="{{indent + 15}}" expandedNodes="{{expandedNodes}}" selectedNodes="{{selectedNodes}}" bindtoggle="onChildToggle" bindselect="onChildSelect"></tree-node></block></view>
</view>

2. Component JS 逻辑

Component({properties: {data: Object,indent: {type: Number,value: 0},isTopLevel: {  // 新增属性type: Boolean,value: false},expandedNodes: Array,selectedNodes: Array},data: {isExpanded: false,isSelected: false,hasChildren: false},attached() {// console.log('Node data:', this.data.data);// console.log('Has children:', this.data.hasChildren);// console.log('Is expanded:', this.data.isExpanded);},observers: {'data.children': function(children) {this.setData({hasChildren: !!(children && children.length) // 确保布尔值});},'expandedNodes': function(expandedNodes) {this.setData({isExpanded: expandedNodes.includes(this.properties.data.id)});},'selectedNodes': function(selectedNodes) {this.setData({isSelected: selectedNodes.includes(this.properties.data.id)});}},methods: {toggle() {if (!this.data.hasChildren) return;this.triggerEvent('toggle', { id: this.properties.data.id });},select() {this.triggerEvent('select', { id: this.properties.data.id });},onChildToggle(e) {this.triggerEvent('toggle', e.detail);},onChildSelect(e) {this.triggerEvent('select', e.detail);},}
});

3. CSS 样式

.tree-node {display: flex;font-size: 16px;flex-direction: column;font-size: 90%;justify-content: flex-start; /* 垂直对齐方式 */
}.toggle-icon {width: 15px;height: 15px;margin:0 1px;display: flex;align-items: center;justify-content: center;font-size: 12px;color: rgb(68, 68, 68);
}.toggle-icon.hidden {visibility: hidden;
}.checkbox {width: 13px;height: 13px;margin-right: 4px;border-radius: 4px;display: flex;align-items: center;justify-content: center;
}.checkbox-icon {font-size: 10px;color: #fff;
}.node-name {flex: 1;
}.children-container {border-left: 1px dashed #eee;
}.tree-line{display: flex;justify-content: center;align-items: center;padding:5px 0;
}/* 禁用状态样式 */
.checkbox.disabled {background-color: #f5f5f5;border-color: #ddd !important;
}.checkbox.disabled .icon {color: rgb(133, 133, 133);
}/* 保持图片中的箭头样式 */
.toggle-icon {opacity: 0.5; /* 箭头也变灰 */
}

4. JSON 声明组件

需要再次引用改组件,因为这里使用的是递归的方式,将二级、三级标题进行循环展示

{"component": true,"usingComponents": {"tree-node":"/components/TreeNode/index"}
}

相关文章:

  • P27:RNN实现阿尔茨海默病诊断
  • Spring,Spring MVC,Spring Boot 之间什么关系?
  • Linux信号机制:从入门到精通
  • vscode把less文件生成css文件配置,设置生成自定义文件名称和路径
  • 移动端测试——如何解决iOS端无法打开弹窗式网页(Webkit)
  • 七、Python高级特性:迭代器、生成器与装饰器
  • 智能实验室革命:Deepoc大模型驱动全自动化科研新生态
  • 前端 E2E 测试实践:打造稳定 Web 应用的利器!
  • echarts柱状图要给柱子顶部加一个顶的写法方案
  • 在反向代理环境下精准获取客户端真实 IP 的最佳实践
  • 每日八股文补充2网络篇
  • GESP C++ 五级真题(2024年12月)题解
  • Shell 流程控制
  • 什么是Sanity Testing?和冒烟测试的区别?
  • Kotlin中协程挂起函数的本质
  • 数据结构学习——二叉树
  • PCIE中基于地址的路由
  • IPV6概述
  • 【Android知识点】面试版
  • 1. 配置OSPF智能定时器