实现可折叠展开的树形菜单 + 复选框联动功能
在开发企业级管理系统或权限管理模块时,常常需要展示具有层级关系的数据,如部门-员工结构、权限组等。这时候我们通常会使用“树形结构”来展示数据。本文将手把手教你如何在微信小程序中实现一个支持展开/收起 、多级复选框联动 的树形结构组件。
一、最终效果预览
我们将实现如下功能:
支持多级节点
点击箭头展开/收起子节点
点击复选框选择节点
一级节点选中时自动选中所有子节点
获取所有选中的叶子节点信息
二、项目结构说明
本项目包含两个主要部分:
- 主页面 Page
- 树形组件 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"
}
}