ReactJS基于antd将对象数组转为无限层级可折叠树节点
无限层级可折叠树节点实现方案
需求规则
- 基于扁平对象数组的
Level(节点层级,从0开始计数,支持无限嵌套)、SubField(节点路径)字段构建嵌套树结构 - 节点展示文本取
ServiceHeaderBlockTitle字段值 - 每个节点支持三类操作:编辑标题、新增子层级、删除当前层级
- 当节点
IsServiceBlock属性为true时,禁止在该节点下新增子层级 - 所有带下级节点的父节点支持展开/折叠交互
预期层级结构示例
/* -Level0(SubField("/") --Level1(SubField"/1/") --Level1(SubField"/2/") ----Level2(SubField"/2/1") ------Level3(SubField"/2/1/1") ------Level3(SubField"/2/1/2") --------Level3(SubField"/2/1/2/1") --Level1(SubField"/3/") ----Level2(SubField"/3/1") ----Level2(SubField"/3/2") ----Level2(SubField"/3/3") --Level1(SubField"/4/") ----Level2(SubField"/4/1") ----Level2(SubField"/4/2") ... and so on */
第一步:扁平数组转嵌套树工具方法
核心逻辑通过SubField路径匹配父节点,无需额外依赖parentId字段:
function buildTree(flatList) { // 按Level升序排序,保证父节点优先处理 const sortedList = [...flatList].sort((a, b) => a.Level - b.Level); const tree = []; // 哈希表存储所有节点,key为SubField,实现O(1)查找 const nodeMap = new Map(); sortedList.forEach(item => { const node = { ...item, children: [], expanded: true // 默认展开状态,可按需修改 }; nodeMap.set(item.SubField, node); if (item.Level === 0) { // 根节点直接入树 tree.push(node); } else { // 计算父节点路径:移除当前路径最后一段,例如/2/1/的父路径为/2/ const pathParts = item.SubField.split('/').filter(Boolean); pathParts.pop(); const parentPath = '/' + (pathParts.length ? pathParts.join('/') + '/' : ''); const parentNode = nodeMap.get(parentPath); parentNode?.children.push(node); } }); return tree; }
第二步:可折叠树核心交互逻辑(原生JS实现,框架可直接迁移逻辑)
// 递归渲染单个节点 function renderNode(node) { const nodeEl = document.createElement('div'); nodeEl.className = 'tree-node'; nodeEl.style.paddingLeft = `${node.Level * 20}px`; // 折叠/展开按钮 const toggleBtn = document.createElement('span'); toggleBtn.className = 'toggle-btn'; if (node.children.length > 0) { toggleBtn.textContent = node.expanded ? '▼' : '▶'; toggleBtn.addEventListener('click', () => { node.expanded = !node.expanded; renderTree(); }); } else { toggleBtn.textContent = '○'; } nodeEl.appendChild(toggleBtn); // 节点标题 const titleEl = document.createElement('span'); titleEl.className = 'node-title'; titleEl.textContent = node.ServiceHeaderBlockTitle; nodeEl.appendChild(titleEl); // 操作按钮组 const actionGroup = document.createElement('span'); actionGroup.className = 'action-group'; // 编辑按钮 const editBtn = document.createElement('button'); editBtn.textContent = '编辑'; editBtn.addEventListener('click', () => { const newTitle = prompt('请输入新标题', node.ServiceHeaderBlockTitle); if (newTitle?.trim()) { node.ServiceHeaderBlockTitle = newTitle.trim(); renderTree(); } }); actionGroup.appendChild(editBtn); // 新增子节点按钮:IsServiceBlock为true时不渲染 if (!node.IsServiceBlock) { const addBtn = document.createElement('button'); addBtn.textContent = '新增子级'; addBtn.addEventListener('click', () => { const newTitle = prompt('请输入新节点标题'); if (newTitle?.trim()) { // 生成新节点的路径、层级属性 const childIndex = node.children.length + 1; const newSubField = `${node.SubField}${childIndex}/`; const newNode = { ServiceHeaderBlockId: Date.now(), // 临时ID,对接后端时替换为接口返回值 ServiceHeaderBlockTitle: newTitle.trim(), SubField: newSubField, Level: node.Level + 1, IsServiceBlock: false, ResourceDescriptionId: null, IsActive: true, children: [], expanded: true }; node.children.push(newNode); renderTree(); } }); actionGroup.appendChild(addBtn); } // 删除按钮:根节点不允许删除 if (node.Level !== 0) { const delBtn = document.createElement('button'); delBtn.textContent = '删除'; delBtn.addEventListener('click', () => { if (confirm(`确定要删除节点「${node.ServiceHeaderBlockTitle}」及其所有子节点吗?`)) { const pathParts = node.SubField.split('/').filter(Boolean); pathParts.pop(); const parentPath = '/' + (pathParts.length ? pathParts.join('/') + '/' : ''); const parentNode = findNodeByPath(treeData, parentPath); if (parentNode) { parentNode.children = parentNode.children.filter(child => child.ServiceHeaderBlockId !== node.ServiceHeaderBlockId); renderTree(); } } }); actionGroup.appendChild(delBtn); } nodeEl.appendChild(actionGroup); // 节点展开时递归渲染子节点 const childrenContainer = document.createElement('div'); childrenContainer.className = 'children-container'; if (node.expanded && node.children.length > 0) { node.children.forEach(child => { childrenContainer.appendChild(renderNode(child)); }); } nodeEl.appendChild(childrenContainer); return nodeEl; } // 辅助方法:根据路径查找对应节点 function findNodeByPath(nodes, path) { for (const node of nodes) { if (node.SubField === path) return node; if (node.children.length) { const found = findNodeByPath(node.children, path); if (found) return found; } } return null; } // 全局树数据、渲染入口 let treeData = []; function renderTree(containerSelector = '#tree-container') { const container = document.querySelector(containerSelector); container.innerHTML = ''; treeData.forEach(rootNode => { container.appendChild(renderNode(rootNode)); }); } // 初始化调用:传入原始扁平数组即可生成可交互树 // treeData = buildTree(yourRawDataArray); // renderTree();
附:原始参考数据集
[ { "ServiceHeaderBlockId": 1, "ServiceHeaderBlockTitle": "Header", "SubField": "/", "Level": 0, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 57, "ServiceHeaderBlockTitle": "Project Administration", "SubField": "/1/", "Level": 1, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 58, "ServiceHeaderBlockTitle": "Innovation", "SubField": "/2/", "Level": 1, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 76, "ServiceHeaderBlockTitle": "Commercialization", "SubField": "/3/", "Level": 1, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 77, "ServiceHeaderBlockTitle": "Quality/Food Safety Audit", "SubField": "/4/", "Level": 1, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 78, "ServiceHeaderBlockTitle": "Co-Man sourcing", "SubField": "/3/1/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 79, "ServiceHeaderBlockTitle": "Co-Man Vetting", "SubField": "/3/2/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 80, "ServiceHeaderBlockTitle": "Line Trail", "SubField": "/3/3/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 81, "ServiceHeaderBlockTitle": "Gap Analysis", "SubField": "/4/1/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 82, "ServiceHeaderBlockTitle": "HACCP", "SubField": "/4/2/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 84, "ServiceHeaderBlockTitle": "Product Development", "SubField": "/2/2/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 59, "ServiceHeaderBlockTitle": "Project Charter", "SubField": "/1/1/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 60, "ServiceHeaderBlockTitle": "Internal Ideation", "SubField": "/1/2/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 61, "ServiceHeaderBlockTitle": "White Space Opportunity", "SubField": "/2/1/", "Level": 2, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 62, "ServiceHeaderBlockTitle": "Territory Mapping/Platform Design", "SubField": "/2/1/1/", "Level": 3, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 70, "ServiceHeaderBlockTitle": "Creative Council Session", "SubField": "/2/1/2/", "Level": 3, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 71, "ServiceHeaderBlockTitle": "Trends Report", "SubField": "/2/1/3/", "Level": 3, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 85, "ServiceHeaderBlockTitle": "Product Development Administration", "SubField": "/2/2/1/", "Level": 3, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 103, "ServiceHeaderBlockTitle": "Brand/Project Immersion", "SubField": "/1/1/1/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 10, "IsActive": true }, { "ServiceHeaderBlockId": 104, "ServiceHeaderBlockTitle": "Project Charter Preparation", "SubField": "/1/1/2/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 23, "IsActive": true }, { "ServiceHeaderBlockId": 105, "ServiceHeaderBlockTitle": "Timeline", "SubField": "/1/1/3/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 23, "IsActive": true }, { "ServiceHeaderBlockId": 106, "ServiceHeaderBlockTitle": "Project Organization-Data Collection", "SubField": "/1/1/4/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 38, "IsActive": true }, { "ServiceHeaderBlockId": 107, "ServiceHeaderBlockTitle": "Brand/Project Immersion", "SubField": "/1/2/1/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 10, "IsActive": true }, { "ServiceHeaderBlockId": 108, "ServiceHeaderBlockTitle": "Meeting-Internal", "SubField": "/1/2/2/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 23, "IsActive": true }, { "ServiceHeaderBlockId": 109, "ServiceHeaderBlockTitle": "Meeting Report", "SubField": "/1/2/3/", "Level": 3, "IsServiceBlock": true, "ResourceDescriptionId": 26, "IsActive": true }, { "ServiceHeaderBlockId": 110, "ServiceHeaderBlockTitle": "Brand/Project Immersion", "SubField": "/2/1/1/1/", "Level": 4, "IsServiceBlock": true, "ResourceDescriptionId": 10, "IsActive": true }, { "ServiceHeaderBlockId": 111, "ServiceHeaderBlockTitle": "Meeting-Internal", "SubField": "/2/1/1/2/", "Level": 4, "IsServiceBlock": true, "ResourceDescriptionId": 10, "IsActive": true }, { "ServiceHeaderBlockId": 115, "ServiceHeaderBlockTitle": "Creative Council Brief", "SubField": "/2/1/3/1/", "Level": 4, "IsServiceBlock": true, "ResourceDescriptionId": 10, "IsActive": true }, { "ServiceHeaderBlockId": 86, "ServiceHeaderBlockTitle": "Process Technology Assessment ", "SubField": "/2/2/1/1/", "Level": 4, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 87, "ServiceHeaderBlockTitle": "Analogue Cutting-Internal", "SubField": "/2/2/1/2/", "Level": 4, "IsServiceBlock": false, "ResourceDescriptionId": null, "IsActive": true }, { "ServiceHeaderBlockId": 114, "ServiceHeaderBlockTitle": "
相关产品推荐
相关产品推荐

