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

发布于:2025-06-28 ⋅ 阅读:(18) ⋅ 点赞:(0)

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

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


一、最终效果预览

我们将实现如下功能:

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


二、项目结构说明

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

  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
    });
  },
  // 获取所有子节点ID
  getAllChildIds(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"
  }
}


网站公告

今日签到

点亮在社区的每一天
去签到