tree-selector.html
· 46 KiB · HTML
Исходник
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>移动端树形多选列表</title>
<style>
* {
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;
}
.container {
max-width: 100%;
margin: 0 auto;
}
.search-box {
position: sticky;
top: 0;
background: white;
padding: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 100;
border-radius: 8px;
margin-bottom: 10px;
}
.search-input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 16px;
outline: none;
transition: all 0.3s;
}
.search-input:focus {
border-color: #007AFF;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.result-info {
padding: 10px;
color: #666;
font-size: 14px;
}
.tree-container {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
height: calc(100vh - 150px);
overflow-y: auto;
position: relative;
}
.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;
}
.tree-node:last-child {
border-bottom: none;
}
.tree-node.indent-1 { padding-left: 30px; }
.tree-node.indent-2 { padding-left: 45px; }
.tree-node.indent-3 { padding-left: 60px; }
.tree-node.indent-4 { padding-left: 75px; }
.tree-node.indent-5 { padding-left: 90px; }
.tree-node.indent-6 { padding-left: 105px; }
.tree-node.indent-7 { padding-left: 120px; }
.tree-node.indent-8 { padding-left: 135px; }
.tree-node.indent-9 { padding-left: 150px; }
.tree-node.indent-10 { padding-left: 165px; }
.node-toggle {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
color: #999;
font-size: 18px;
transform: rotate(0deg);
transition: transform 0.2s;
}
.node-toggle.expanded {
transform: rotate(90deg);
}
.node-toggle.empty {
visibility: hidden;
}
.node-content {
flex: 1;
font-size: 16px;
color: #333;
}
.node-checkbox {
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
position: relative;
}
.node-checkbox.checked {
background-color: #007AFF;
border-color: #007AFF;
}
.node-checkbox.indeterminate::after {
content: "";
position: absolute;
width: 10px;
height: 2px;
background-color: white;
}
.node-checkbox.checked::after {
content: "✓";
color: white;
font-size: 14px;
position: absolute;
}
.node-children {
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.search-result {
background-color: #e8f4ff;
}
.no-results {
padding: 20px;
text-align: center;
color: #999;
}
.loading {
padding: 20px;
text-align: center;
color: #999;
}
.selected-count {
position: fixed;
bottom: 20px;
right: 20px;
background: #007AFF;
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
cursor: pointer;
z-index: 1000;
}
.breadcrumb {
display: flex;
padding: 10px 15px;
background: #f9f9f9;
border-bottom: 1px solid #eee;
overflow-x: auto;
white-space: nowrap;
}
.breadcrumb-item {
font-size: 14px;
color: #007AFF;
cursor: pointer;
padding: 0 5px;
}
.breadcrumb-item:not(:last-child)::after {
content: " > ";
color: #999;
}
.breadcrumb-item:last-child {
color: #333;
font-weight: bold;
}
.node-path {
color: #888;
font-size: 12px;
margin-top: 3px;
}
.search-mode .tree-node:not(.search-result) .node-path {
display: none;
}
.virtual-container {
position: relative;
}
.node-icon {
margin-right: 8px;
font-size: 16px;
}
.folder-icon::before {
content: "📁";
}
.file-icon::before {
content: "📄";
}
/* 弹窗样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 10px 20px;
overflow-y: auto;
flex: 1;
}
.selected-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.selected-item:last-child {
border-bottom: none;
}
.item-info {
flex: 1;
}
.item-name {
font-size: 16px;
color: #333;
}
.item-path {
font-size: 12px;
color: #888;
margin-top: 3px;
}
.deselect-btn {
background: #ff3b30;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
}
.clear-all-btn {
background: #ff3b30;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
.confirm-btn {
background: #007AFF;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
.empty-selected {
text-align: center;
padding: 30px;
color: #999;
}
/* 父节点样式调整 */
.parent-node .node-content {
flex: 1;
}
.parent-node .node-checkbox {
display: none;
}
</style>
</head>
<body>
<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>
<!-- 已选项弹窗 -->
<div class="modal" id="selectedModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">已选项</div>
<button class="modal-close" id="modalClose">×</button>
</div>
<div class="modal-body" id="modalBody">
<!-- 已选项将在这里显示 -->
</div>
<div class="modal-footer">
<button class="clear-all-btn" id="clearAllBtn">清空所有</button>
<button class="confirm-btn" id="confirmBtn">确认</button>
</div>
</div>
</div>
<script>
// 模拟大量树形数据
class TreeNode {
constructor(id, name, parentId = null) {
this.id = id;
this.name = name;
this.parentId = parentId;
this.children = [];
this.checked = false;
this.indeterminate = false;
}
}
// 生成大量测试数据,最多5-6级深度
function generateTreeData() {
console.time('数据生成耗时');
const rootNode = new TreeNode('0', '根节点');
const nodeMap = new Map();
nodeMap.set('0', rootNode);
// 生成5-6级深度的树结构
// 第1级: 10个节点
for (let i = 1; i <= 10; i++) {
const parentId = '0';
const nodeId = i.toString();
const nodeName = `一级节点 ${i}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
// 第2级: 每个一级节点下有20个二级节点
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 20; j++) {
const parentId = i.toString();
const nodeId = `${i}-${j}`;
const nodeName = `二级节点 ${i}-${j}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
}
// 第3级: 每个二级节点下有15个三级节点
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 20; j++) {
for (let k = 1; k <= 15; k++) {
const parentId = `${i}-${j}`;
const nodeId = `${i}-${j}-${k}`;
const nodeName = `三级节点 ${i}-${j}-${k}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
}
}
// 第4级: 每个三级节点下有10个四级节点
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 20; j++) {
for (let k = 1; k <= 15; k++) {
for (let l = 1; l <= 10; l++) {
const parentId = `${i}-${j}-${k}`;
const nodeId = `${i}-${j}-${k}-${l}`;
const nodeName = `四级节点 ${i}-${j}-${k}-${l}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
}
}
}
// 第5级: 每个四级节点下有5个五级节点
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 20; j++) {
for (let k = 1; k <= 15; k++) {
for (let l = 1; l <= 10; l++) {
for (let m = 1; m <= 5; m++) {
const parentId = `${i}-${j}-${k}-${l}`;
const nodeId = `${i}-${j}-${k}-${l}-${m}`;
const nodeName = `五级节点 ${i}-${j}-${k}-${l}-${m}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
}
}
}
}
// 第6级: 部分五级节点下有六级节点
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 20; j++) {
for (let k = 1; k <= 15; k++) {
for (let l = 1; l <= 10; l++) {
for (let m = 1; m <= 5; m++) {
// 只为部分节点添加六级节点
if (Math.random() > 0.7) {
for (let n = 1; n <= 3; n++) {
const parentId = `${i}-${j}-${k}-${l}-${m}`;
const nodeId = `${i}-${j}-${k}-${l}-${m}-${n}`;
const nodeName = `六级节点 ${i}-${j}-${k}-${l}-${m}-${n}`;
const node = new TreeNode(nodeId, nodeName, parentId);
nodeMap.set(nodeId, node);
nodeMap.get(parentId).children.push(node);
}
}
}
}
}
}
}
console.timeEnd('数据生成耗时');
console.log(rootNode);
return rootNode;
}
// 树形列表组件
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);
console.log('节点ID到节点数量',this.nodeMap.size)
// 构建父节点映射以提高查找性能
this.parentMap = new Map();
this.buildParentMap(rootNode);
// 构建节点路径映射
this.nodePathMap = new Map();
this.buildNodePathMap(rootNode, []);
// 初始化虚拟滚动
this.initVirtualScroll();
this.render();
this.bindEvents();
this.updateSelectedCount();
this.initModal();
}
// 构建节点路径映射
buildNodePathMap(node, path) {
const currentPath = [...path, node.name];
this.nodePathMap.set(node.id, currentPath);
if (node.children) {
node.children.forEach(child => {
this.buildNodePathMap(child, currentPath);
});
}
}
// 初始化弹窗
initModal() {
const modal = document.getElementById('selectedModal');
const selectedCount = document.getElementById('selectedCount');
const modalClose = document.getElementById('modalClose');
const clearAllBtn = document.getElementById('clearAllBtn');
const confirmBtn = document.getElementById('confirmBtn');
// 点击已选数量按钮打开弹窗
selectedCount.addEventListener('click', () => {
this.showSelectedModal();
});
// 点击关闭按钮关闭弹窗
modalClose.addEventListener('click', () => {
this.closeSelectedModal();
});
// 点击清空所有按钮
clearAllBtn.addEventListener('click', () => {
this.clearAllSelections();
});
// 点击确认按钮
confirmBtn.addEventListener('click', () => {
this.confirmSelections();
});
// 点击遮罩层关闭弹窗
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeSelectedModal();
}
});
}
// 显示已选项弹窗
showSelectedModal() {
const modal = document.getElementById('selectedModal');
const modalBody = document.getElementById('modalBody');
// 获取所有选中的节点
const selectedNodes = [];
for (const [id, node] of this.nodeMap) {
if (node.checked) {
selectedNodes.push(node);
}
}
// 清空弹窗内容
modalBody.innerHTML = '';
// 显示选中项
if (selectedNodes.length === 0) {
modalBody.innerHTML = '<div class="empty-selected">暂无选中项</div>';
} else {
selectedNodes.forEach(node => {
const itemElement = document.createElement('div');
itemElement.className = 'selected-item';
const itemInfo = document.createElement('div');
itemInfo.className = 'item-info';
const itemName = document.createElement('div');
itemName.className = 'item-name';
itemName.textContent = node.name;
const itemPath = document.createElement('div');
itemPath.className = 'item-path';
itemPath.textContent = this.nodePathMap.get(node.id).join(' > ');
itemInfo.appendChild(itemName);
itemInfo.appendChild(itemPath);
const deselectBtn = document.createElement('button');
deselectBtn.className = 'deselect-btn';
deselectBtn.textContent = '取消';
deselectBtn.addEventListener('click', () => {
this.deselectNode(node);
});
itemElement.appendChild(itemInfo);
itemElement.appendChild(deselectBtn);
modalBody.appendChild(itemElement);
});
}
// 显示弹窗
modal.style.display = 'flex';
}
// 关闭已选项弹窗
closeSelectedModal() {
const modal = document.getElementById('selectedModal');
modal.style.display = 'none';
}
// 取消选中节点
deselectNode(node) {
node.checked = false;
node.indeterminate = false;
// 更新子节点状态
this.updateChildren(node, false);
// 更新父节点状态
this.updateParents(node);
// 更新选中计数
this.updateSelectedCount();
// 更新UI
this.updateCheckboxUI(node);
// 重新渲染弹窗
this.showSelectedModal();
}
// 清空所有选择
clearAllSelections() {
for (const [id, node] of this.nodeMap) {
if (node.checked) {
node.checked = false;
node.indeterminate = false;
this.updateCheckboxUI(node);
}
}
// 更新选中计数
this.updateSelectedCount();
// 重新渲染弹窗
this.showSelectedModal();
}
// 确认选择并打印到控制台
confirmSelections() {
const selectedNodes = [];
for (const [id, node] of this.nodeMap) {
if (node.checked) {
selectedNodes.push({
id: node.id,
name: node.name,
path: this.nodePathMap.get(node.id).join(' > ')
});
}
}
console.log('已选项:', selectedNodes);
alert(`已选择 ${selectedNodes.length} 项,详情请查看控制台`);
// 关闭弹窗
this.closeSelectedModal();
}
// 初始化虚拟滚动
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;
}
// 构建节点ID到节点的映射
buildNodeMap(node) {
this.nodeMap.set(node.id, node);
if (node.children) {
node.children.forEach(child => this.buildNodeMap(child));
}
}
// 构建父节点映射
buildParentMap(node) {
if (node.children) {
node.children.forEach(child => {
this.parentMap.set(child.id, node);
this.buildParentMap(child);
});
}
}
// 渲染树形结构
render() {
if (this.searchMode) {
this.renderSearchResults();
} else {
this.renderTree();
}
this.renderBreadcrumb();
this.updateResultInfo();
}
// 渲染普通树结构(虚拟滚动)
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`;
});
}
// 展平树结构
flattenTree(node, depth) {
const result = [{ node, depth }];
if (node.expanded && node.children && node.children.length > 0) {
node.children.forEach(child => {
result.push(...this.flattenTree(child, depth + 1));
});
}
return result;
}
// 创建节点元素
createNodeElement(node, depth) {
// 判断是否为父节点(有子节点)
const isParent = node.children && node.children.length > 0;
const nodeElement = document.createElement('div');
nodeElement.className = `tree-node ${isParent ? 'parent-node' : ''} indent-${Math.min(depth, 10)}`;
nodeElement.dataset.id = node.id;
// 展开/收起按钮
const toggleElement = document.createElement('div');
toggleElement.className = `node-toggle ${!isParent ? 'empty' : ''} ${node.expanded ? 'expanded' : ''}`;
toggleElement.innerHTML = '▶';
nodeElement.appendChild(toggleElement);
// 节点内容
const contentElement = document.createElement('div');
contentElement.className = 'node-content';
contentElement.textContent = node.name;
nodeElement.appendChild(contentElement);
// 只为叶节点添加复选框
if (!isParent) {
const checkboxElement = document.createElement('div');
checkboxElement.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
nodeElement.appendChild(checkboxElement);
}
return nodeElement;
}
// 渲染搜索结果(扁平列表,带虚拟滚动)
renderSearchResults() {
if (this.searchResults.size === 0) {
const noResultsElement = document.createElement('div');
noResultsElement.className = 'no-results';
noResultsElement.textContent = '未找到匹配结果';
this.container.innerHTML = '';
this.container.appendChild(noResultsElement);
return;
}
// 将搜索结果转换为数组并排序
const resultsArray = Array.from(this.searchResults).sort((a, b) => {
return a.name.localeCompare(b.name);
});
// 计算可见节点范围
const startIndex = Math.max(0, Math.floor(this.scrollTop / this.nodeHeight) - this.buffer);
const endIndex = Math.min(
resultsArray.length,
Math.ceil((this.scrollTop + this.containerHeight) / this.nodeHeight) + this.buffer
);
// 获取可见节点
const visibleResults = resultsArray.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 = `${resultsArray.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(visibleResults.map(node => node.id));
existingNodes.forEach(nodeEl => {
if (!visibleNodeIds.has(nodeEl.dataset.id)) {
nodeEl.remove();
}
});
// 更新或创建可见节点
visibleResults.forEach((node, index) => {
let nodeElement = existingNodeMap.get(node.id);
if (!nodeElement) {
// 创建新节点
nodeElement = this.createSearchResultElement(node);
virtualContainer.appendChild(nodeElement);
}
// 更新节点位置
nodeElement.style.top = `${(index + startIndex) * this.nodeHeight}px`;
});
}
// 创建搜索结果元素
createSearchResultElement(node) {
// 判断是否为父节点(有子节点)
const isParent = node.children && node.children.length > 0;
const nodeElement = document.createElement('div');
nodeElement.className = `tree-node search-result ${isParent ? 'parent-node' : ''}`;
nodeElement.dataset.id = node.id;
// 节点图标(文件夹或文件)
const iconElement = document.createElement('div');
iconElement.className = `node-icon ${isParent ? 'folder-icon' : 'file-icon'}`;
nodeElement.appendChild(iconElement);
// 节点内容
const contentElement = document.createElement('div');
contentElement.className = 'node-content';
contentElement.textContent = node.name;
nodeElement.appendChild(contentElement);
// 只为叶节点添加复选框
if (!isParent) {
const checkboxElement = document.createElement('div');
checkboxElement.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
nodeElement.appendChild(checkboxElement);
}
return nodeElement;
}
// 渲染面包屑导航
renderBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
if (!breadcrumb) return;
breadcrumb.innerHTML = '';
this.path.forEach((node, index) => {
const item = document.createElement('div');
item.className = 'breadcrumb-item';
item.textContent = node.name;
item.onclick = () => this.navigateTo(index);
breadcrumb.appendChild(item);
});
}
// 更新结果信息
updateResultInfo() {
const info = document.getElementById('resultInfo');
if (!info) return;
if (this.searchMode) {
info.textContent = `找到 ${this.searchResults.size} 个匹配结果`;
} else {
info.textContent = '';
}
}
// 绑定事件
bindEvents() {
// 搜索事件
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.search(e.target.value);
}, 300);
});
// 节点点击事件代理
this.container.addEventListener('click', (e) => {
const nodeElement = e.target.closest('.tree-node');
if (!nodeElement) return;
const nodeId = nodeElement.dataset.id;
const node = this.nodeMap.get(nodeId);
if (!node) return;
// 在搜索模式下,点击节点行为不同
if (this.searchMode) {
// 点击复选框
if (e.target.classList.contains('node-checkbox')) {
this.toggleCheckbox(node);
return;
}
// 点击节点内容 - 进入浏览模式并导航到该节点
this.enterNodeFromSearch(node);
return;
}
// 在浏览模式下保持原有行为
// 点击展开/收起按钮
if (e.target.classList.contains('node-toggle')) {
this.toggleNode(node);
return;
}
// 点击复选框
if (e.target.classList.contains('node-checkbox')) {
this.toggleCheckbox(node);
return;
}
// 点击节点内容(如果是父节点)
if (e.target.classList.contains('node-content') || e.target.closest('.node-content')) {
if (node.children && node.children.length > 0) {
this.enterNode(node);
} else {
// 点击叶节点时切换选中状态
this.toggleCheckbox(node);
}
return;
}
});
}
// 从搜索结果进入节点浏览
enterNodeFromSearch(node) {
// 构建从根节点到目标节点的路径
const path = [];
let current = node;
// 向上遍历到根节点
while (current) {
path.unshift(current);
current = this.parentMap.get(current.id);
}
// 设置路径并切换到浏览模式
this.path = path;
this.currentNode = node;
this.searchMode = false;
this.searchTerm = '';
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.value = '';
}
// 展开目标节点
node.expanded = true;
this.scrollTop = 0;
this.container.scrollTop = 0;
this.render();
}
// 搜索功能 - 优化版本
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();
}
// 展开/收起节点
toggleNode(node) {
node.expanded = !node.expanded;
this.render();
}
// 进入节点(用于浏览模式)
enterNode(node) {
// 检查节点是否已经在路径中,避免重复添加
const nodeIndex = this.path.findIndex(n => n.id === node.id);
if (nodeIndex !== -1) {
// 如果节点已存在,截断路径到该节点位置
this.path = this.path.slice(0, nodeIndex + 1);
} else {
// 否则添加到路径末尾
this.path.push(node);
}
this.currentNode = node;
this.scrollTop = 0;
this.container.scrollTop = 0;
this.render();
}
// 导航到指定路径层级
navigateTo(index) {
this.path = this.path.slice(0, index + 1);
this.currentNode = this.path[this.path.length - 1];
this.scrollTop = 0;
this.container.scrollTop = 0;
this.render();
}
// 切换复选框状态
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);
}
// 更新复选框UI状态
updateCheckboxUI(node) {
// 更新所有匹配的节点元素(可能在不同视图中存在)
const nodeElements = this.container.querySelectorAll(`.tree-node[data-id="${node.id}"]`);
nodeElements.forEach(nodeElement => {
// 只为叶节点更新复选框
if (!nodeElement.classList.contains('parent-node')) {
const checkbox = nodeElement.querySelector('.node-checkbox');
if (checkbox) {
checkbox.className = `node-checkbox ${node.checked ? 'checked' : ''} ${node.indeterminate && !node.checked ? 'indeterminate' : ''}`;
}
}
});
}
// 更新选中计数
updateSelectedCount() {
const count = document.getElementById('selectedCount');
if (!count) return;
const selectedCount = this.getSelectedCount();
count.textContent = selectedCount;
}
// 获取选中节点数量
getSelectedCount() {
let count = 0;
// 使用Map遍历提高性能
for (const [id, node] of this.nodeMap) {
if (node.checked) {
count++;
}
}
return count;
}
}
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
const treeContainer = document.getElementById('treeContainer');
const loadingElement = document.getElementById('loading');
// 模拟数据加载
setTimeout(() => {
if (loadingElement) {
loadingElement.style.display = 'none';
}
const rootNode = generateTreeData();
const treeSelector = new TreeSelector(treeContainer, rootNode);
// 保存引用以便调试
window.treeSelector = treeSelector;
}, 500);
});
</script>
</body>
</html>
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> |