移动端树形多选列表组件解析

本文最后更新于:2025年10月9日 下午

引言

在现代Web应用开发中,处理复杂的树形数据结构并提供良好的用户体验是一项常见需求。本文将深入解析一个移动端树形多选列表组件,该组件具有虚拟滚动、搜索功能和多级节点管理等特性,适用于处理大量树形数据的场景。

整体概述

这个移动端树形多选列表组件是一个完整的解决方案,用于在移动设备上展示和操作复杂的树形数据结构。它支持多级节点展开/收起、节点选择、搜索功能、面包屑导航和已选项管理等特性。

组件主要包含以下几个核心功能:

  1. 树形数据展示与导航

  2. 多级节点选择(支持全选/取消全选子节点)

  3. 虚拟滚动优化(处理大量数据)

  4. 搜索功能

  5. 面包屑导航

  6. 已选项弹窗管理

核心部分解析

1. HTML结构

组件的HTML结构相对简洁,主要包含以下几个部分:

  • 搜索框(.search-box):用于搜索节点

  • 面包屑导航(.breadcrumb):显示当前路径

  • 结果树容器(.tree-container):展示树形结构

  • 已选项计数器(.selected-count):显示已选项目数量

  • 已选项弹窗(.modal):展示和管理已选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="container">
<div class="search-box">
<input type="text" class="search-input" id="searchInput" placeholder="输入名称搜索...">
</div>

<div class="breadcrumb" id="breadcrumb"></div>

<div class="result-info" id="resultInfo"></div>

<div class="tree-container" id="treeContainer">
<div class="loading" id="loading">加载中...</div>
</div>
</div>

<div class="selected-count" id="selectedCount">0</div>

2. CSS样式设计

CSS样式采用了现代化的设计风格,特别针对移动设备进行了优化:

响应式设计

1
2
3
4
5
6
7
8
9
10
11
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f5f5;
padding: 10px;
}

虚拟滚动样式

为了配合虚拟滚动功能,节点元素采用了绝对定位:

1
2
3
4
5
6
7
8
9
10
.tree-node {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #eee;
transition: background-color 0.2s;
position: absolute; /* 虚拟滚动关键 */
width: 100%;
box-sizing: border-box;
}

不同层级的缩进

通过预定义的CSS类实现不同层级的缩进效果:

1
2
3
4
.tree-node.indent-1 { padding-left: 30px; }
.tree-node.indent-2 { padding-left: 45px; }
.tree-node.indent-3 { padding-left: 60px; }
/* ... 更多层级 */

3. JavaScript核心实现

JavaScript部分是整个组件的核心,采用了ES6的Class语法来组织代码。

树节点数据结构

1
2
3
4
5
6
7
8
9
10
class TreeNode {
constructor(id, name, parentId = null) {
this.id = id;
this.name = name;
this.parentId = parentId;
this.children = [];
this.checked = false;
this.indeterminate = false;
}
}

树形选择器主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class TreeSelector {
constructor(container, rootNode) {
this.container = container;
this.rootNode = rootNode;
this.currentNode = rootNode;
this.path = [rootNode];
this.selectedNodes = new Set();
this.searchResults = new Set();
this.searchMode = false;
this.searchTerm = '';

// 虚拟滚动相关
this.nodeHeight = 50; // 每个节点的高度
this.buffer = 15; // 缓冲区节点数
this.scrollTop = 0;
this.containerHeight = 0;

// 创建节点ID到节点的映射以提高查找性能
this.nodeMap = new Map();
this.buildNodeMap(rootNode);

// 构建父节点映射以提高查找性能
this.parentMap = new Map();
this.buildParentMap(rootNode);

// 构建节点路径映射
this.nodePathMap = new Map();
this.buildNodePathMap(rootNode, []);

// 初始化虚拟滚动
this.initVirtualScroll();

this.render();
this.bindEvents();
this.updateSelectedCount();
this.initModal();
}
}

虚拟滚动实现

虚拟滚动是处理大量数据的关键技术,通过只渲染可见区域的节点来提高性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 初始化虚拟滚动
initVirtualScroll() {
let ticking = false;

const handleScroll = () => {
this.scrollTop = this.container.scrollTop;
if (!ticking) {
requestAnimationFrame(() => {
this.render();
ticking = false;
});
ticking = true;
}
};

this.container.addEventListener('scroll', handleScroll);

// 获取容器高度
this.containerHeight = this.container.clientHeight;
}

// 渲染普通树结构(虚拟滚动)
renderTree() {
// 获取当前路径下的所有节点
const allNodes = this.flattenTree(this.currentNode, 0);

// 计算可见节点范围
const startIndex = Math.max(0, Math.floor(this.scrollTop / this.nodeHeight) - this.buffer);
const endIndex = Math.min(
allNodes.length,
Math.ceil((this.scrollTop + this.containerHeight) / this.nodeHeight) + this.buffer
);

// 获取可见节点
const visibleNodes = allNodes.slice(startIndex, endIndex);

// 创建或获取虚拟容器
let virtualContainer = this.container.querySelector('.virtual-container');
if (!virtualContainer) {
virtualContainer = document.createElement('div');
virtualContainer.className = 'virtual-container';
this.container.innerHTML = '';
this.container.appendChild(virtualContainer);
}

// 设置虚拟容器高度
virtualContainer.style.height = `${allNodes.length * this.nodeHeight}px`;

// 获取所有现有的节点元素
const existingNodes = Array.from(virtualContainer.querySelectorAll('.tree-node'));

// 创建一个映射来跟踪哪些节点已经存在
const existingNodeMap = new Map();
existingNodes.forEach(nodeEl => {
existingNodeMap.set(nodeEl.dataset.id, nodeEl);
});

// 移除不再需要的节点
const visibleNodeIds = new Set(visibleNodes.map(item => item.node.id));
existingNodes.forEach(nodeEl => {
if (!visibleNodeIds.has(nodeEl.dataset.id)) {
nodeEl.remove();
}
});

// 更新或创建可见节点
visibleNodes.forEach((item, index) => {
const nodeId = item.node.id;
let nodeElement = existingNodeMap.get(nodeId);

if (!nodeElement) {
// 创建新节点
nodeElement = this.createNodeElement(item.node, item.depth);
virtualContainer.appendChild(nodeElement);
}

// 更新节点位置
nodeElement.style.top = `${(index + startIndex) * this.nodeHeight}px`;
});
}

搜索功能实现

搜索功能允许用户快速定位特定节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 搜索功能 - 优化版本
search(term) {
console.time('搜索耗时');
this.searchTerm = term.trim().toLowerCase();
this.searchMode = this.searchTerm !== '';

if (!this.searchMode) {
this.searchResults.clear();
this.scrollTop = 0;
this.container.scrollTop = 0;
this.render();
console.timeEnd('搜索耗时');
return;
}

this.searchResults.clear();

// 使用优化的搜索算法
// 遍历所有节点查找匹配项
for (const [id, node] of this.nodeMap) {
// 检查节点名称是否匹配
if (node.name.toLowerCase().includes(this.searchTerm)) {
this.searchResults.add(node);
}
}

this.scrollTop = 0;
this.container.scrollTop = 0;
console.timeEnd('搜索耗时');
this.render();
}

节点选择与状态管理

组件支持节点的多选功能,并能正确处理父子节点之间的状态联动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 切换复选框状态
toggleCheckbox(node) {
node.checked = !node.checked;
node.indeterminate = false;

// 更新子节点状态
this.updateChildren(node, node.checked);

// 更新父节点状态
this.updateParents(node);

// 更新选中计数
this.updateSelectedCount();

// 更新UI
this.updateCheckboxUI(node);
}

// 更新子节点状态
updateChildren(node, checked) {
if (node.children) {
node.children.forEach(child => {
child.checked = checked;
child.indeterminate = false;
this.updateChildren(child, checked);
});
}
}

// 更新父节点状态
updateParents(node) {
const parent = this.parentMap.get(node.id);
if (!parent) return;

// 检查是否所有子节点都被选中
const allChecked = parent.children.every(child => child.checked);
const someChecked = parent.children.some(child => child.checked || child.indeterminate);

parent.checked = allChecked;
parent.indeterminate = !allChecked && someChecked;

// 更新UI
this.updateCheckboxUI(parent);

// 递归更新祖先节点
this.updateParents(parent);
}

性能优化要点

1. 虚拟滚动

通过虚拟滚动技术,即使面对数万个节点,也能保持流畅的用户体验。

2. 数据结构优化

使用Map来存储节点映射关系,提高节点查找效率:

1
2
3
this.nodeMap = new Map(); // 节点ID到节点的映射
this.parentMap = new Map(); // 子节点ID到父节点的映射
this.nodePathMap = new Map(); // 节点ID到路径的映射

3. DOM操作优化

通过复用DOM元素和精确控制重绘区域,减少不必要的DOM操作。

总结

这个移动端树形多选列表组件展示了如何在移动设备上高效处理大量树形数据。通过虚拟滚动、合理的数据结构设计和精心优化的UI交互,它能够提供流畅的用户体验,即使在数据量很大的情况下也能保持高性能。

该组件的实现具有以下亮点:

  1. 完整的树形数据操作功能

  2. 高性能的虚拟滚动实现

  3. 直观的UI设计和交互

  4. 强大的搜索功能

  5. 灵活的节点选择和状态管理

这些技术和设计模式可以应用到其他需要处理复杂数据结构的前端项目中,具有很高的参考价值。

完整代码


移动端树形多选列表组件解析
https://blog.xuven.xyz/post/MobileTreeStructured/
作者
Xuven Li
发布于
2025年10月9日
许可协议