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">×</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