Last active 1 week ago

xuven revised this gist 1 week ago. Go to revision

1 file changed, 1284 insertions

tree-selector.html(file created)

@@ -0,0 +1,1284 @@
1 + <!DOCTYPE html>
2 + <html lang="zh-CN">
3 + <head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6 + <title>移动端树形多选列表</title>
7 + <style>
8 + * {
9 + margin: 0;
10 + padding: 0;
11 + box-sizing: border-box;
12 + }
13 +
14 + body {
15 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
16 + background-color: #f5f5f5;
17 + padding: 10px;
18 + }
19 +
20 + .container {
21 + max-width: 100%;
22 + margin: 0 auto;
23 + }
24 +
25 + .search-box {
26 + position: sticky;
27 + top: 0;
28 + background: white;
29 + padding: 10px;
30 + box-shadow: 0 2px 4px rgba(0,0,0,0.1);
31 + z-index: 100;
32 + border-radius: 8px;
33 + margin-bottom: 10px;
34 + }
35 +
36 + .search-input {
37 + width: 100%;
38 + padding: 12px 15px;
39 + border: 1px solid #ddd;
40 + border-radius: 20px;
41 + font-size: 16px;
42 + outline: none;
43 + transition: all 0.3s;
44 + }
45 +
46 + .search-input:focus {
47 + border-color: #007AFF;
48 + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
49 + }
50 +
51 + .result-info {
52 + padding: 10px;
53 + color: #666;
54 + font-size: 14px;
55 + }
56 +
57 + .tree-container {
58 + background: white;
59 + border-radius: 8px;
60 + overflow: hidden;
61 + box-shadow: 0 1px 3px rgba(0,0,0,0.1);
62 + height: calc(100vh - 150px);
63 + overflow-y: auto;
64 + position: relative;
65 + }
66 +
67 + .tree-node {
68 + display: flex;
69 + align-items: center;
70 + padding: 12px 15px;
71 + border-bottom: 1px solid #eee;
72 + transition: background-color 0.2s;
73 + position: absolute;
74 + width: 100%;
75 + box-sizing: border-box;
76 + }
77 +
78 + .tree-node:last-child {
79 + border-bottom: none;
80 + }
81 +
82 + .tree-node.indent-1 { padding-left: 30px; }
83 + .tree-node.indent-2 { padding-left: 45px; }
84 + .tree-node.indent-3 { padding-left: 60px; }
85 + .tree-node.indent-4 { padding-left: 75px; }
86 + .tree-node.indent-5 { padding-left: 90px; }
87 + .tree-node.indent-6 { padding-left: 105px; }
88 + .tree-node.indent-7 { padding-left: 120px; }
89 + .tree-node.indent-8 { padding-left: 135px; }
90 + .tree-node.indent-9 { padding-left: 150px; }
91 + .tree-node.indent-10 { padding-left: 165px; }
92 +
93 + .node-toggle {
94 + width: 24px;
95 + height: 24px;
96 + display: flex;
97 + align-items: center;
98 + justify-content: center;
99 + margin-right: 8px;
100 + color: #999;
101 + font-size: 18px;
102 + transform: rotate(0deg);
103 + transition: transform 0.2s;
104 + }
105 +
106 + .node-toggle.expanded {
107 + transform: rotate(90deg);
108 + }
109 +
110 + .node-toggle.empty {
111 + visibility: hidden;
112 + }
113 +
114 + .node-content {
115 + flex: 1;
116 + font-size: 16px;
117 + color: #333;
118 + }
119 +
120 + .node-checkbox {
121 + width: 20px;
122 + height: 20px;
123 + border: 2px solid #ccc;
124 + border-radius: 4px;
125 + display: flex;
126 + align-items: center;
127 + justify-content: center;
128 + margin-left: 10px;
129 + position: relative;
130 + }
131 +
132 + .node-checkbox.checked {
133 + background-color: #007AFF;
134 + border-color: #007AFF;
135 + }
136 +
137 + .node-checkbox.indeterminate::after {
138 + content: "";
139 + position: absolute;
140 + width: 10px;
141 + height: 2px;
142 + background-color: white;
143 + }
144 +
145 + .node-checkbox.checked::after {
146 + content: "✓";
147 + color: white;
148 + font-size: 14px;
149 + position: absolute;
150 + }
151 +
152 + .node-children {
153 + overflow: hidden;
154 + transition: max-height 0.3s ease-out;
155 + }
156 +
157 + .search-result {
158 + background-color: #e8f4ff;
159 + }
160 +
161 + .no-results {
162 + padding: 20px;
163 + text-align: center;
164 + color: #999;
165 + }
166 +
167 + .loading {
168 + padding: 20px;
169 + text-align: center;
170 + color: #999;
171 + }
172 +
173 + .selected-count {
174 + position: fixed;
175 + bottom: 20px;
176 + right: 20px;
177 + background: #007AFF;
178 + color: white;
179 + width: 50px;
180 + height: 50px;
181 + border-radius: 50%;
182 + display: flex;
183 + align-items: center;
184 + justify-content: center;
185 + font-weight: bold;
186 + box-shadow: 0 2px 10px rgba(0,0,0,0.3);
187 + cursor: pointer;
188 + z-index: 1000;
189 + }
190 +
191 + .breadcrumb {
192 + display: flex;
193 + padding: 10px 15px;
194 + background: #f9f9f9;
195 + border-bottom: 1px solid #eee;
196 + overflow-x: auto;
197 + white-space: nowrap;
198 + }
199 +
200 + .breadcrumb-item {
201 + font-size: 14px;
202 + color: #007AFF;
203 + cursor: pointer;
204 + padding: 0 5px;
205 + }
206 +
207 + .breadcrumb-item:not(:last-child)::after {
208 + content: " > ";
209 + color: #999;
210 + }
211 +
212 + .breadcrumb-item:last-child {
213 + color: #333;
214 + font-weight: bold;
215 + }
216 +
217 + .node-path {
218 + color: #888;
219 + font-size: 12px;
220 + margin-top: 3px;
221 + }
222 +
223 + .search-mode .tree-node:not(.search-result) .node-path {
224 + display: none;
225 + }
226 +
227 + .virtual-container {
228 + position: relative;
229 + }
230 +
231 + .node-icon {
232 + margin-right: 8px;
233 + font-size: 16px;
234 + }
235 +
236 + .folder-icon::before {
237 + content: "📁";
238 + }
239 +
240 + .file-icon::before {
241 + content: "📄";
242 + }
243 +
244 + /* 弹窗样式 */
245 + .modal {
246 + display: none;
247 + position: fixed;
248 + top: 0;
249 + left: 0;
250 + width: 100%;
251 + height: 100%;
252 + background-color: rgba(0, 0, 0, 0.5);
253 + z-index: 2000;
254 + justify-content: center;
255 + align-items: center;
256 + }
257 +
258 + .modal-content {
259 + background-color: white;
260 + border-radius: 10px;
261 + width: 90%;
262 + max-width: 500px;
263 + max-height: 80vh;
264 + overflow: hidden;
265 + display: flex;
266 + flex-direction: column;
267 + }
268 +
269 + .modal-header {
270 + padding: 15px 20px;
271 + border-bottom: 1px solid #eee;
272 + display: flex;
273 + justify-content: space-between;
274 + align-items: center;
275 + }
276 +
277 + .modal-title {
278 + font-size: 18px;
279 + font-weight: bold;
280 + color: #333;
281 + }
282 +
283 + .modal-close {
284 + background: none;
285 + border: none;
286 + font-size: 24px;
287 + cursor: pointer;
288 + color: #999;
289 + }
290 +
291 + .modal-body {
292 + padding: 10px 20px;
293 + overflow-y: auto;
294 + flex: 1;
295 + }
296 +
297 + .selected-item {
298 + display: flex;
299 + justify-content: space-between;
300 + align-items: center;
301 + padding: 12px 0;
302 + border-bottom: 1px solid #f0f0f0;
303 + }
304 +
305 + .selected-item:last-child {
306 + border-bottom: none;
307 + }
308 +
309 + .item-info {
310 + flex: 1;
311 + }
312 +
313 + .item-name {
314 + font-size: 16px;
315 + color: #333;
316 + }
317 +
318 + .item-path {
319 + font-size: 12px;
320 + color: #888;
321 + margin-top: 3px;
322 + }
323 +
324 + .deselect-btn {
325 + background: #ff3b30;
326 + color: white;
327 + border: none;
328 + border-radius: 4px;
329 + padding: 6px 12px;
330 + font-size: 14px;
331 + cursor: pointer;
332 + }
333 +
334 + .modal-footer {
335 + padding: 15px 20px;
336 + border-top: 1px solid #eee;
337 + display: flex;
338 + justify-content: space-between;
339 + }
340 +
341 + .clear-all-btn {
342 + background: #ff3b30;
343 + color: white;
344 + border: none;
345 + border-radius: 4px;
346 + padding: 10px 20px;
347 + font-size: 16px;
348 + cursor: pointer;
349 + }
350 +
351 + .confirm-btn {
352 + background: #007AFF;
353 + color: white;
354 + border: none;
355 + border-radius: 4px;
356 + padding: 10px 20px;
357 + font-size: 16px;
358 + cursor: pointer;
359 + }
360 +
361 + .empty-selected {
362 + text-align: center;
363 + padding: 30px;
364 + color: #999;
365 + }
366 +
367 + /* 父节点样式调整 */
368 + .parent-node .node-content {
369 + flex: 1;
370 + }
371 +
372 + .parent-node .node-checkbox {
373 + display: none;
374 + }
375 + </style>
376 + </head>
377 + <body>
378 + <div class="container">
379 + <div class="search-box">
380 + <input type="text" class="search-input" id="searchInput" placeholder="输入名称搜索...">
381 + </div>
382 +
383 + <div class="breadcrumb" id="breadcrumb"></div>
384 +
385 + <div class="result-info" id="resultInfo"></div>
386 +
387 + <div class="tree-container" id="treeContainer">
388 + <div class="loading" id="loading">加载中...</div>
389 + </div>
390 + </div>
391 +
392 + <div class="selected-count" id="selectedCount">0</div>
393 +
394 + <!-- 已选项弹窗 -->
395 + <div class="modal" id="selectedModal">
396 + <div class="modal-content">
397 + <div class="modal-header">
398 + <div class="modal-title">已选项</div>
399 + <button class="modal-close" id="modalClose">&times;</button>
400 + </div>
401 + <div class="modal-body" id="modalBody">
402 + <!-- 已选项将在这里显示 -->
403 + </div>
404 + <div class="modal-footer">
405 + <button class="clear-all-btn" id="clearAllBtn">清空所有</button>
406 + <button class="confirm-btn" id="confirmBtn">确认</button>
407 + </div>
408 + </div>
409 + </div>
410 +
411 + <script>
412 + // 模拟大量树形数据
413 + class TreeNode {
414 + constructor(id, name, parentId = null) {
415 + this.id = id;
416 + this.name = name;
417 + this.parentId = parentId;
418 + this.children = [];
419 + this.checked = false;
420 + this.indeterminate = false;
421 + }
422 + }
423 +
424 + // 生成大量测试数据,最多5-6级深度
425 + function generateTreeData() {
426 + console.time('数据生成耗时');
427 + const rootNode = new TreeNode('0', '根节点');
428 + const nodeMap = new Map();
429 + nodeMap.set('0', rootNode);
430 +
431 + // 生成5-6级深度的树结构
432 + // 第1级: 10个节点
433 + for (let i = 1; i <= 10; i++) {
434 + const parentId = '0';
435 + const nodeId = i.toString();
436 + const nodeName = `一级节点 ${i}`;
437 + const node = new TreeNode(nodeId, nodeName, parentId);
438 + nodeMap.set(nodeId, node);
439 + nodeMap.get(parentId).children.push(node);
440 + }
441 +
442 + // 第2级: 每个一级节点下有20个二级节点
443 + for (let i = 1; i <= 10; i++) {
444 + for (let j = 1; j <= 20; j++) {
445 + const parentId = i.toString();
446 + const nodeId = `${i}-${j}`;
447 + const nodeName = `二级节点 ${i}-${j}`;
448 + const node = new TreeNode(nodeId, nodeName, parentId);
449 + nodeMap.set(nodeId, node);
450 + nodeMap.get(parentId).children.push(node);
451 + }
452 + }
453 +
454 + // 第3级: 每个二级节点下有15个三级节点
455 + for (let i = 1; i <= 10; i++) {
456 + for (let j = 1; j <= 20; j++) {
457 + for (let k = 1; k <= 15; k++) {
458 + const parentId = `${i}-${j}`;
459 + const nodeId = `${i}-${j}-${k}`;
460 + const nodeName = `三级节点 ${i}-${j}-${k}`;
461 + const node = new TreeNode(nodeId, nodeName, parentId);
462 + nodeMap.set(nodeId, node);
463 + nodeMap.get(parentId).children.push(node);
464 + }
465 + }
466 + }
467 +
468 + // 第4级: 每个三级节点下有10个四级节点
469 + for (let i = 1; i <= 10; i++) {
470 + for (let j = 1; j <= 20; j++) {
471 + for (let k = 1; k <= 15; k++) {
472 + for (let l = 1; l <= 10; l++) {
473 + const parentId = `${i}-${j}-${k}`;
474 + const nodeId = `${i}-${j}-${k}-${l}`;
475 + const nodeName = `四级节点 ${i}-${j}-${k}-${l}`;
476 + const node = new TreeNode(nodeId, nodeName, parentId);
477 + nodeMap.set(nodeId, node);
478 + nodeMap.get(parentId).children.push(node);
479 + }
480 + }
481 + }
482 + }
483 +
484 + // 第5级: 每个四级节点下有5个五级节点
485 + for (let i = 1; i <= 10; i++) {
486 + for (let j = 1; j <= 20; j++) {
487 + for (let k = 1; k <= 15; k++) {
488 + for (let l = 1; l <= 10; l++) {
489 + for (let m = 1; m <= 5; m++) {
490 + const parentId = `${i}-${j}-${k}-${l}`;
491 + const nodeId = `${i}-${j}-${k}-${l}-${m}`;
492 + const nodeName = `五级节点 ${i}-${j}-${k}-${l}-${m}`;
493 + const node = new TreeNode(nodeId, nodeName, parentId);
494 + nodeMap.set(nodeId, node);
495 + nodeMap.get(parentId).children.push(node);
496 + }
497 + }
498 + }
499 + }
500 + }
501 +
502 + // 第6级: 部分五级节点下有六级节点
503 + for (let i = 1; i <= 10; i++) {
504 + for (let j = 1; j <= 20; j++) {
505 + for (let k = 1; k <= 15; k++) {
506 + for (let l = 1; l <= 10; l++) {
507 + for (let m = 1; m <= 5; m++) {
508 + // 只为部分节点添加六级节点
509 + if (Math.random() > 0.7) {
510 + for (let n = 1; n <= 3; n++) {
511 + const parentId = `${i}-${j}-${k}-${l}-${m}`;
512 + const nodeId = `${i}-${j}-${k}-${l}-${m}-${n}`;
513 + const nodeName = `六级节点 ${i}-${j}-${k}-${l}-${m}-${n}`;
514 + const node = new TreeNode(nodeId, nodeName, parentId);
515 + nodeMap.set(nodeId, node);
516 + nodeMap.get(parentId).children.push(node);
517 + }
518 + }
519 + }
520 + }
521 + }
522 + }
523 + }
524 +
525 + console.timeEnd('数据生成耗时');
526 + console.log(rootNode);
527 + return rootNode;
528 + }
529 +
530 + // 树形列表组件
531 + class TreeSelector {
532 + constructor(container, rootNode) {
533 + this.container = container;
534 + this.rootNode = rootNode;
535 + this.currentNode = rootNode;
536 + this.path = [rootNode];
537 + this.selectedNodes = new Set();
538 + this.searchResults = new Set();
539 + this.searchMode = false;
540 + this.searchTerm = '';
541 +
542 + // 虚拟滚动相关
543 + this.nodeHeight = 50; // 每个节点的高度
544 + this.buffer = 15; // 缓冲区节点数
545 + this.scrollTop = 0;
546 + this.containerHeight = 0;
547 +
548 + // 创建节点ID到节点的映射以提高查找性能
549 + this.nodeMap = new Map();
550 + this.buildNodeMap(rootNode);
551 + console.log('节点ID到节点数量',this.nodeMap.size)
552 +
553 + // 构建父节点映射以提高查找性能
554 + this.parentMap = new Map();
555 + this.buildParentMap(rootNode);
556 +
557 + // 构建节点路径映射
558 + this.nodePathMap = new Map();
559 + this.buildNodePathMap(rootNode, []);
560 +
561 + // 初始化虚拟滚动
562 + this.initVirtualScroll();
563 +
564 + this.render();
565 + this.bindEvents();
566 + this.updateSelectedCount();
567 + this.initModal();
568 + }
569 +
570 + // 构建节点路径映射
571 + buildNodePathMap(node, path) {
572 + const currentPath = [...path, node.name];
573 + this.nodePathMap.set(node.id, currentPath);
574 +
575 + if (node.children) {
576 + node.children.forEach(child => {
577 + this.buildNodePathMap(child, currentPath);
578 + });
579 + }
580 + }
581 +
582 + // 初始化弹窗
583 + initModal() {
584 + const modal = document.getElementById('selectedModal');
585 + const selectedCount = document.getElementById('selectedCount');
586 + const modalClose = document.getElementById('modalClose');
587 + const clearAllBtn = document.getElementById('clearAllBtn');
588 + const confirmBtn = document.getElementById('confirmBtn');
589 +
590 + // 点击已选数量按钮打开弹窗
591 + selectedCount.addEventListener('click', () => {
592 + this.showSelectedModal();
593 + });
594 +
595 + // 点击关闭按钮关闭弹窗
596 + modalClose.addEventListener('click', () => {
597 + this.closeSelectedModal();
598 + });
599 +
600 + // 点击清空所有按钮
601 + clearAllBtn.addEventListener('click', () => {
602 + this.clearAllSelections();
603 + });
604 +
605 + // 点击确认按钮
606 + confirmBtn.addEventListener('click', () => {
607 + this.confirmSelections();
608 + });
609 +
610 + // 点击遮罩层关闭弹窗
611 + modal.addEventListener('click', (e) => {
612 + if (e.target === modal) {
613 + this.closeSelectedModal();
614 + }
615 + });
616 + }
617 +
618 + // 显示已选项弹窗
619 + showSelectedModal() {
620 + const modal = document.getElementById('selectedModal');
621 + const modalBody = document.getElementById('modalBody');
622 +
623 + // 获取所有选中的节点
624 + const selectedNodes = [];
625 + for (const [id, node] of this.nodeMap) {
626 + if (node.checked) {
627 + selectedNodes.push(node);
628 + }
629 + }
630 +
631 + // 清空弹窗内容
632 + modalBody.innerHTML = '';
633 +
634 + // 显示选中项
635 + if (selectedNodes.length === 0) {
636 + modalBody.innerHTML = '<div class="empty-selected">暂无选中项</div>';
637 + } else {
638 + selectedNodes.forEach(node => {
639 + const itemElement = document.createElement('div');
640 + itemElement.className = 'selected-item';
641 +
642 + const itemInfo = document.createElement('div');
643 + itemInfo.className = 'item-info';
644 +
645 + const itemName = document.createElement('div');
646 + itemName.className = 'item-name';
647 + itemName.textContent = node.name;
648 +
649 + const itemPath = document.createElement('div');
650 + itemPath.className = 'item-path';
651 + itemPath.textContent = this.nodePathMap.get(node.id).join(' > ');
652 +
653 + itemInfo.appendChild(itemName);
654 + itemInfo.appendChild(itemPath);
655 +
656 + const deselectBtn = document.createElement('button');
657 + deselectBtn.className = 'deselect-btn';
658 + deselectBtn.textContent = '取消';
659 + deselectBtn.addEventListener('click', () => {
660 + this.deselectNode(node);
661 + });
662 +
663 + itemElement.appendChild(itemInfo);
664 + itemElement.appendChild(deselectBtn);
665 +
666 + modalBody.appendChild(itemElement);
667 + });
668 + }
669 +
670 + // 显示弹窗
671 + modal.style.display = 'flex';
672 + }
673 +
674 + // 关闭已选项弹窗
675 + closeSelectedModal() {
676 + const modal = document.getElementById('selectedModal');
677 + modal.style.display = 'none';
678 + }
679 +
680 + // 取消选中节点
681 + deselectNode(node) {
682 + node.checked = false;
683 + node.indeterminate = false;
684 +
685 + // 更新子节点状态
686 + this.updateChildren(node, false);
687 +
688 + // 更新父节点状态
689 + this.updateParents(node);
690 +
691 + // 更新选中计数
692 + this.updateSelectedCount();
693 +
694 + // 更新UI
695 + this.updateCheckboxUI(node);
696 +
697 + // 重新渲染弹窗
698 + this.showSelectedModal();
699 + }
700 +
701 + // 清空所有选择
702 + clearAllSelections() {
703 + for (const [id, node] of this.nodeMap) {
704 + if (node.checked) {
705 + node.checked = false;
706 + node.indeterminate = false;
707 + this.updateCheckboxUI(node);
708 + }
709 + }
710 +
711 + // 更新选中计数
712 + this.updateSelectedCount();
713 +
714 + // 重新渲染弹窗
715 + this.showSelectedModal();
716 + }
717 +
718 + // 确认选择并打印到控制台
719 + confirmSelections() {
720 + const selectedNodes = [];
721 + for (const [id, node] of this.nodeMap) {
722 + if (node.checked) {
723 + selectedNodes.push({
724 + id: node.id,
725 + name: node.name,
726 + path: this.nodePathMap.get(node.id).join(' > ')
727 + });
728 + }
729 + }
730 +
731 + console.log('已选项:', selectedNodes);
732 + alert(`已选择 ${selectedNodes.length} 项,详情请查看控制台`);
733 +
734 + // 关闭弹窗
735 + this.closeSelectedModal();
736 + }
737 +
738 + // 初始化虚拟滚动
739 + initVirtualScroll() {
740 + let ticking = false;
741 +
742 + const handleScroll = () => {
743 + this.scrollTop = this.container.scrollTop;
744 + if (!ticking) {
745 + requestAnimationFrame(() => {
746 + this.render();
747 + ticking = false;
748 + });
749 + ticking = true;
750 + }
751 + };
752 +
753 + this.container.addEventListener('scroll', handleScroll);
754 +
755 + // 获取容器高度
756 + this.containerHeight = this.container.clientHeight;
757 + }
758 +
759 + // 构建节点ID到节点的映射
760 + buildNodeMap(node) {
761 + this.nodeMap.set(node.id, node);
762 + if (node.children) {
763 + node.children.forEach(child => this.buildNodeMap(child));
764 + }
765 + }
766 +
767 + // 构建父节点映射
768 + buildParentMap(node) {
769 + if (node.children) {
770 + node.children.forEach(child => {
771 + this.parentMap.set(child.id, node);
772 + this.buildParentMap(child);
773 + });
774 + }
775 + }
776 +
777 + // 渲染树形结构
778 + render() {
779 + if (this.searchMode) {
780 + this.renderSearchResults();
781 + } else {
782 + this.renderTree();
783 + }
784 +
785 + this.renderBreadcrumb();
786 + this.updateResultInfo();
787 + }
788 +
789 + // 渲染普通树结构(虚拟滚动)
790 + renderTree() {
791 + // 获取当前路径下的所有节点
792 + const allNodes = this.flattenTree(this.currentNode, 0);
793 +
794 + // 计算可见节点范围
795 + const startIndex = Math.max(0, Math.floor(this.scrollTop / this.nodeHeight) - this.buffer);
796 + const endIndex = Math.min(
797 + allNodes.length,
798 + Math.ceil((this.scrollTop + this.containerHeight) / this.nodeHeight) + this.buffer
799 + );
800 +
801 + // 获取可见节点
802 + const visibleNodes = allNodes.slice(startIndex, endIndex);
803 +
804 + // 创建或获取虚拟容器
805 + let virtualContainer = this.container.querySelector('.virtual-container');
806 + if (!virtualContainer) {
807 + virtualContainer = document.createElement('div');
808 + virtualContainer.className = 'virtual-container';
809 + this.container.innerHTML = '';
810 + this.container.appendChild(virtualContainer);
811 + }
812 +
813 + // 设置虚拟容器高度
814 + virtualContainer.style.height = `${allNodes.length * this.nodeHeight}px`;
815 +
816 + // 获取所有现有的节点元素
817 + const existingNodes = Array.from(virtualContainer.querySelectorAll('.tree-node'));
818 +
819 + // 创建一个映射来跟踪哪些节点已经存在
820 + const existingNodeMap = new Map();
821 + existingNodes.forEach(nodeEl => {
822 + existingNodeMap.set(nodeEl.dataset.id, nodeEl);
823 + });
824 +
825 + // 移除不再需要的节点
826 + const visibleNodeIds = new Set(visibleNodes.map(item => item.node.id));
827 + existingNodes.forEach(nodeEl => {
828 + if (!visibleNodeIds.has(nodeEl.dataset.id)) {
829 + nodeEl.remove();
830 + }
831 + });
832 +
833 + // 更新或创建可见节点
834 + visibleNodes.forEach((item, index) => {
835 + const nodeId = item.node.id;
836 + let nodeElement = existingNodeMap.get(nodeId);
837 +
838 + if (!nodeElement) {
839 + // 创建新节点
840 + nodeElement = this.createNodeElement(item.node, item.depth);
841 + virtualContainer.appendChild(nodeElement);
842 + }
843 +
844 + // 更新节点位置
845 + nodeElement.style.top = `${(index + startIndex) * this.nodeHeight}px`;
846 + });
847 + }
848 +
849 + // 展平树结构
850 + flattenTree(node, depth) {
851 + const result = [{ node, depth }];
852 +
853 + if (node.expanded && node.children && node.children.length > 0) {
854 + node.children.forEach(child => {
855 + result.push(...this.flattenTree(child, depth + 1));
856 + });
857 + }
858 +
859 + return result;
860 + }
861 +
862 + // 创建节点元素
863 + createNodeElement(node, depth) {
864 + // 判断是否为父节点(有子节点)
865 + const isParent = node.children && node.children.length > 0;
866 +
867 + const nodeElement = document.createElement('div');
868 + nodeElement.className = `tree-node ${isParent ? 'parent-node' : ''} indent-${Math.min(depth, 10)}`;
869 + nodeElement.dataset.id = node.id;
870 +
871 + // 展开/收起按钮
872 + const toggleElement = document.createElement('div');
873 + toggleElement.className = `node-toggle ${!isParent ? 'empty' : ''} ${node.expanded ? 'expanded' : ''}`;
874 + toggleElement.innerHTML = '▶';
875 + nodeElement.appendChild(toggleElement);
876 +
877 + // 节点内容
878 + const contentElement = document.createElement('div');
879 + contentElement.className = 'node-content';
880 + contentElement.textContent = node.name;
881 + nodeElement.appendChild(contentElement);
882 +
883 + // 只为叶节点添加复选框
884 + if (!isParent) {
885 + const checkboxElement = document.createElement('div');
886 + checkboxElement.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
887 + nodeElement.appendChild(checkboxElement);
888 + }
889 +
890 + return nodeElement;
891 + }
892 +
893 + // 渲染搜索结果(扁平列表,带虚拟滚动)
894 + renderSearchResults() {
895 + if (this.searchResults.size === 0) {
896 + const noResultsElement = document.createElement('div');
897 + noResultsElement.className = 'no-results';
898 + noResultsElement.textContent = '未找到匹配结果';
899 + this.container.innerHTML = '';
900 + this.container.appendChild(noResultsElement);
901 + return;
902 + }
903 +
904 + // 将搜索结果转换为数组并排序
905 + const resultsArray = Array.from(this.searchResults).sort((a, b) => {
906 + return a.name.localeCompare(b.name);
907 + });
908 +
909 + // 计算可见节点范围
910 + const startIndex = Math.max(0, Math.floor(this.scrollTop / this.nodeHeight) - this.buffer);
911 + const endIndex = Math.min(
912 + resultsArray.length,
913 + Math.ceil((this.scrollTop + this.containerHeight) / this.nodeHeight) + this.buffer
914 + );
915 +
916 + // 获取可见节点
917 + const visibleResults = resultsArray.slice(startIndex, endIndex);
918 +
919 + // 创建或获取虚拟容器
920 + let virtualContainer = this.container.querySelector('.virtual-container');
921 + if (!virtualContainer) {
922 + virtualContainer = document.createElement('div');
923 + virtualContainer.className = 'virtual-container';
924 + this.container.innerHTML = '';
925 + this.container.appendChild(virtualContainer);
926 + }
927 +
928 + // 设置虚拟容器高度
929 + virtualContainer.style.height = `${resultsArray.length * this.nodeHeight}px`;
930 +
931 + // 获取所有现有的节点元素
932 + const existingNodes = Array.from(virtualContainer.querySelectorAll('.tree-node'));
933 +
934 + // 创建一个映射来跟踪哪些节点已经存在
935 + const existingNodeMap = new Map();
936 + existingNodes.forEach(nodeEl => {
937 + existingNodeMap.set(nodeEl.dataset.id, nodeEl);
938 + });
939 +
940 + // 移除不再需要的节点
941 + const visibleNodeIds = new Set(visibleResults.map(node => node.id));
942 + existingNodes.forEach(nodeEl => {
943 + if (!visibleNodeIds.has(nodeEl.dataset.id)) {
944 + nodeEl.remove();
945 + }
946 + });
947 +
948 + // 更新或创建可见节点
949 + visibleResults.forEach((node, index) => {
950 + let nodeElement = existingNodeMap.get(node.id);
951 +
952 + if (!nodeElement) {
953 + // 创建新节点
954 + nodeElement = this.createSearchResultElement(node);
955 + virtualContainer.appendChild(nodeElement);
956 + }
957 +
958 + // 更新节点位置
959 + nodeElement.style.top = `${(index + startIndex) * this.nodeHeight}px`;
960 + });
961 + }
962 +
963 + // 创建搜索结果元素
964 + createSearchResultElement(node) {
965 + // 判断是否为父节点(有子节点)
966 + const isParent = node.children && node.children.length > 0;
967 +
968 + const nodeElement = document.createElement('div');
969 + nodeElement.className = `tree-node search-result ${isParent ? 'parent-node' : ''}`;
970 + nodeElement.dataset.id = node.id;
971 +
972 + // 节点图标(文件夹或文件)
973 + const iconElement = document.createElement('div');
974 + iconElement.className = `node-icon ${isParent ? 'folder-icon' : 'file-icon'}`;
975 + nodeElement.appendChild(iconElement);
976 +
977 + // 节点内容
978 + const contentElement = document.createElement('div');
979 + contentElement.className = 'node-content';
980 + contentElement.textContent = node.name;
981 + nodeElement.appendChild(contentElement);
982 +
983 + // 只为叶节点添加复选框
984 + if (!isParent) {
985 + const checkboxElement = document.createElement('div');
986 + checkboxElement.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
987 + nodeElement.appendChild(checkboxElement);
988 + }
989 +
990 + return nodeElement;
991 + }
992 +
993 + // 渲染面包屑导航
994 + renderBreadcrumb() {
995 + const breadcrumb = document.getElementById('breadcrumb');
996 + if (!breadcrumb) return;
997 +
998 + breadcrumb.innerHTML = '';
999 +
1000 + this.path.forEach((node, index) => {
1001 + const item = document.createElement('div');
1002 + item.className = 'breadcrumb-item';
1003 + item.textContent = node.name;
1004 + item.onclick = () => this.navigateTo(index);
1005 + breadcrumb.appendChild(item);
1006 + });
1007 + }
1008 +
1009 + // 更新结果信息
1010 + updateResultInfo() {
1011 + const info = document.getElementById('resultInfo');
1012 + if (!info) return;
1013 +
1014 + if (this.searchMode) {
1015 + info.textContent = `找到 ${this.searchResults.size} 个匹配结果`;
1016 + } else {
1017 + info.textContent = '';
1018 + }
1019 + }
1020 +
1021 + // 绑定事件
1022 + bindEvents() {
1023 + // 搜索事件
1024 + const searchInput = document.getElementById('searchInput');
1025 + if (!searchInput) return;
1026 +
1027 + let searchTimeout;
1028 + searchInput.addEventListener('input', (e) => {
1029 + clearTimeout(searchTimeout);
1030 + searchTimeout = setTimeout(() => {
1031 + this.search(e.target.value);
1032 + }, 300);
1033 + });
1034 +
1035 + // 节点点击事件代理
1036 + this.container.addEventListener('click', (e) => {
1037 + const nodeElement = e.target.closest('.tree-node');
1038 + if (!nodeElement) return;
1039 +
1040 + const nodeId = nodeElement.dataset.id;
1041 + const node = this.nodeMap.get(nodeId);
1042 + if (!node) return;
1043 +
1044 + // 在搜索模式下,点击节点行为不同
1045 + if (this.searchMode) {
1046 + // 点击复选框
1047 + if (e.target.classList.contains('node-checkbox')) {
1048 + this.toggleCheckbox(node);
1049 + return;
1050 + }
1051 +
1052 + // 点击节点内容 - 进入浏览模式并导航到该节点
1053 + this.enterNodeFromSearch(node);
1054 + return;
1055 + }
1056 +
1057 + // 在浏览模式下保持原有行为
1058 + // 点击展开/收起按钮
1059 + if (e.target.classList.contains('node-toggle')) {
1060 + this.toggleNode(node);
1061 + return;
1062 + }
1063 +
1064 + // 点击复选框
1065 + if (e.target.classList.contains('node-checkbox')) {
1066 + this.toggleCheckbox(node);
1067 + return;
1068 + }
1069 +
1070 + // 点击节点内容(如果是父节点)
1071 + if (e.target.classList.contains('node-content') || e.target.closest('.node-content')) {
1072 + if (node.children && node.children.length > 0) {
1073 + this.enterNode(node);
1074 + } else {
1075 + // 点击叶节点时切换选中状态
1076 + this.toggleCheckbox(node);
1077 + }
1078 + return;
1079 + }
1080 + });
1081 + }
1082 +
1083 + // 从搜索结果进入节点浏览
1084 + enterNodeFromSearch(node) {
1085 + // 构建从根节点到目标节点的路径
1086 + const path = [];
1087 + let current = node;
1088 +
1089 + // 向上遍历到根节点
1090 + while (current) {
1091 + path.unshift(current);
1092 + current = this.parentMap.get(current.id);
1093 + }
1094 +
1095 + // 设置路径并切换到浏览模式
1096 + this.path = path;
1097 + this.currentNode = node;
1098 + this.searchMode = false;
1099 + this.searchTerm = '';
1100 + const searchInput = document.getElementById('searchInput');
1101 + if (searchInput) {
1102 + searchInput.value = '';
1103 + }
1104 +
1105 + // 展开目标节点
1106 + node.expanded = true;
1107 +
1108 + this.scrollTop = 0;
1109 + this.container.scrollTop = 0;
1110 + this.render();
1111 + }
1112 +
1113 + // 搜索功能 - 优化版本
1114 + search(term) {
1115 + console.time('搜索耗时');
1116 + this.searchTerm = term.trim().toLowerCase();
1117 + this.searchMode = this.searchTerm !== '';
1118 +
1119 + if (!this.searchMode) {
1120 + this.searchResults.clear();
1121 + this.scrollTop = 0;
1122 + this.container.scrollTop = 0;
1123 + this.render();
1124 + console.timeEnd('搜索耗时');
1125 + return;
1126 + }
1127 +
1128 + this.searchResults.clear();
1129 +
1130 + // 使用优化的搜索算法
1131 + // 遍历所有节点查找匹配项
1132 + for (const [id, node] of this.nodeMap) {
1133 + // 检查节点名称是否匹配
1134 + if (node.name.toLowerCase().includes(this.searchTerm)) {
1135 + this.searchResults.add(node);
1136 + }
1137 + }
1138 +
1139 + this.scrollTop = 0;
1140 + this.container.scrollTop = 0;
1141 + console.timeEnd('搜索耗时');
1142 + this.render();
1143 + }
1144 +
1145 + // 展开/收起节点
1146 + toggleNode(node) {
1147 + node.expanded = !node.expanded;
1148 + this.render();
1149 + }
1150 +
1151 + // 进入节点(用于浏览模式)
1152 + enterNode(node) {
1153 + // 检查节点是否已经在路径中,避免重复添加
1154 + const nodeIndex = this.path.findIndex(n => n.id === node.id);
1155 + if (nodeIndex !== -1) {
1156 + // 如果节点已存在,截断路径到该节点位置
1157 + this.path = this.path.slice(0, nodeIndex + 1);
1158 + } else {
1159 + // 否则添加到路径末尾
1160 + this.path.push(node);
1161 + }
1162 + this.currentNode = node;
1163 + this.scrollTop = 0;
1164 + this.container.scrollTop = 0;
1165 + this.render();
1166 + }
1167 +
1168 + // 导航到指定路径层级
1169 + navigateTo(index) {
1170 + this.path = this.path.slice(0, index + 1);
1171 + this.currentNode = this.path[this.path.length - 1];
1172 + this.scrollTop = 0;
1173 + this.container.scrollTop = 0;
1174 + this.render();
1175 + }
1176 +
1177 + // 切换复选框状态
1178 + toggleCheckbox(node) {
1179 + node.checked = !node.checked;
1180 + node.indeterminate = false;
1181 +
1182 + // 更新子节点状态
1183 + this.updateChildren(node, node.checked);
1184 +
1185 + // 更新父节点状态
1186 + this.updateParents(node);
1187 +
1188 + // 更新选中计数
1189 + this.updateSelectedCount();
1190 +
1191 + // 更新UI
1192 + this.updateCheckboxUI(node);
1193 + }
1194 +
1195 + // 更新子节点状态
1196 + updateChildren(node, checked) {
1197 + if (node.children) {
1198 + node.children.forEach(child => {
1199 + child.checked = checked;
1200 + child.indeterminate = false;
1201 + this.updateChildren(child, checked);
1202 + });
1203 + }
1204 + }
1205 +
1206 + // 更新父节点状态
1207 + updateParents(node) {
1208 + const parent = this.parentMap.get(node.id);
1209 + if (!parent) return;
1210 +
1211 + // 检查是否所有子节点都被选中
1212 + const allChecked = parent.children.every(child => child.checked);
1213 + const someChecked = parent.children.some(child => child.checked || child.indeterminate);
1214 +
1215 + parent.checked = allChecked;
1216 + parent.indeterminate = !allChecked && someChecked;
1217 +
1218 + // 更新UI
1219 + this.updateCheckboxUI(parent);
1220 +
1221 + // 递归更新祖先节点
1222 + this.updateParents(parent);
1223 + }
1224 +
1225 + // 更新复选框UI状态
1226 + updateCheckboxUI(node) {
1227 + // 更新所有匹配的节点元素(可能在不同视图中存在)
1228 + const nodeElements = this.container.querySelectorAll(`.tree-node[data-id="${node.id}"]`);
1229 + nodeElements.forEach(nodeElement => {
1230 + // 只为叶节点更新复选框
1231 + if (!nodeElement.classList.contains('parent-node')) {
1232 + const checkbox = nodeElement.querySelector('.node-checkbox');
1233 + if (checkbox) {
1234 + checkbox.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
1235 + }
1236 + }
1237 + });
1238 + }
1239 +
1240 + // 更新选中计数
1241 + updateSelectedCount() {
1242 + const count = document.getElementById('selectedCount');
1243 + if (!count) return;
1244 +
1245 + const selectedCount = this.getSelectedCount();
1246 + count.textContent = selectedCount;
1247 + }
1248 +
1249 + // 获取选中节点数量
1250 + getSelectedCount() {
1251 + let count = 0;
1252 +
1253 + // 使用Map遍历提高性能
1254 + for (const [id, node] of this.nodeMap) {
1255 + if (node.checked) {
1256 + count++;
1257 + }
1258 + }
1259 +
1260 + return count;
1261 + }
1262 + }
1263 +
1264 + // 初始化应用
1265 + document.addEventListener('DOMContentLoaded', () => {
1266 + const treeContainer = document.getElementById('treeContainer');
1267 + const loadingElement = document.getElementById('loading');
1268 +
1269 + // 模拟数据加载
1270 + setTimeout(() => {
1271 + if (loadingElement) {
1272 + loadingElement.style.display = 'none';
1273 + }
1274 +
1275 + const rootNode = generateTreeData();
1276 + const treeSelector = new TreeSelector(treeContainer, rootNode);
1277 +
1278 + // 保存引用以便调试
1279 + window.treeSelector = treeSelector;
1280 + }, 500);
1281 + });
1282 + </script>
1283 + </body>
1284 + </html>
Newer Older