React中使用Treeviz渲染树节点时Popover组件无法正常显示问题
问题根因
你目前的实现本质是将React组件通过renderToString序列化为静态HTML字符串交给Treeviz渲染,所有React组件的事件绑定、交互逻辑、第三方Popover组件的初始化逻辑都会在序列化后完全丢失。Tooltip能正常工作是因为依赖浏览器原生支持的title属性,无需额外JS逻辑,而Popover依赖JS交互逻辑,因此该实现方式天生不支持你的需求。
可行解决方案
采用事件委托+全局Popover组件的方案实现,既可以正常弹出Popover,也能完美支持后续的描述编辑功能,完整实现代码如下:
import React, { useState, useRef, useEffect } from "react"; import { TreevizReact } from "treeviz-react"; const data_1 = [ { id: 1, text_1: "Chaos", description: "Void", father: null, color: "#FF5722" }, { id: 2, text_1: "Tartarus", description: "Abyss", father: 1, color: "#FFC107" }, { id: 3, text_1: "Gaia", description: "Earth", father: 1, color: "#8BC34A" }, { id: 4, text_1: "Eros", description: "Desire", father: 1, color: "#00BCD4" } ]; export const App = () => { // Popover状态管理 const [popoverVisible, setPopoverVisible] = useState(false); const [currentNode, setCurrentNode] = useState(null); const [popoverPos, setPopoverPos] = useState({ x: 0, y: 0 }); const [editDesc, setEditDesc] = useState(""); const treeContainerRef = useRef(null); // 点击非Popover区域关闭 useEffect(() => { const handleClickOutside = (e) => { if (!e.target.closest('.custom-popover') && !e.target.closest('.node-popover-btn')) { setPopoverVisible(false); } }; document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); }, []); // 处理节点按钮点击 const handleNodeBtnClick = (e) => { const btn = e.target.closest('.node-popover-btn'); if (!btn) return; const nodeId = parseInt(btn.dataset.nodeId, 10); const node = data_1.find(item => item.id === nodeId); if (!node) return; // 获取按钮位置设置Popover位置 const rect = btn.getBoundingClientRect(); setCurrentNode(node); setEditDesc(node.description); setPopoverPos({ x: rect.right + 10, y: rect.top }); setPopoverVisible(true); }; // 保存描述 const saveHandler = (e) => { e.preventDefault(); // 这里可以写你的保存逻辑,更新数据源即可 const index = data_1.findIndex(item => item.id === currentNode.id); if (index > -1) { data_1[index].description = editDesc; } setPopoverVisible(false); alert('保存成功'); }; return ( <div style={{ marginLeft: 10, position: 'relative' }} ref={treeContainerRef} onClick={handleNodeBtnClick}> <div style={{ display: "flex" }}> <TreevizReact data={data_1} relationnalField={"father"} nodeWidth={120} nodeHeight={80} areaHeight={500} areaWidth={1000} mainAxisNodeSpacing={2} secondaryAxisNodeSpacing={2} linkShape={"quadraticBeziers"} renderNode={(data) => { const nodeData = data.data; const settings = data.settings; let result = ""; if (data.depth !== 2) { result = `<div class="box" style='cursor:pointer;height:${settings.nodeHeight}px; width:${settings.nodeWidth}px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${nodeData.color};border-radius:5px;'> <div> <strong title="${nodeData.description}">${nodeData.text_1}</strong> <button class="node-popover-btn" data-node-id="${nodeData.id}" style="margin-left:8px;padding:2px 6px;font-size:12px;"> 查看详情 </button> </div></div>`; } else { result = `<div class="box" style='cursor:pointer;height:${ settings.nodeHeight }px; width:${ settings.nodeHeight }px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${ nodeData.color };border-radius:${settings.nodeHeight / 2}px;'><div><strong> ${nodeData.text_1} </strong></div></div>`; } return result; }} duration={600} isHorizontal linkWidth={(node) => 10} /> </div> {/* 全局Popover组件 */} {popoverVisible && currentNode && ( <div className="custom-popover" style={{ position: 'fixed', left: popoverPos.x, top: popoverPos.y, background: '#fff', border: '1px solid #eee', borderRadius: 4, boxShadow: '0 2px 8px rgba(0,0,0,0.15)', padding: 12, width: 240, zIndex: 9999 }} > <h4 style={{margin: '0 0 12px 0'}}>{currentNode.text_1}</h4> <form onSubmit={saveHandler}> <textarea value={editDesc} onChange={(e) => setEditDesc(e.target.value)} style={{width: '100%', minHeight: 60, marginBottom: 12, padding: 4}} /> <div style={{display: 'flex', gap: 8, justifyContent: 'flex-end'}}> <button type="button" onClick={() => setPopoverVisible(false)} style={{padding: '4px 12px'}}>取消</button> <button type="submit" style={{padding: '4px 12px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: 2}}>保存</button> </div> </form> </div> )} </div> ); }; export default App;
方案说明
- 放弃使用
renderToString序列化React组件的方式,直接给节点按钮增加自定义data-node-id属性存储节点ID - 利用事件冒泡机制,在Treeviz容器层监听点击事件,识别节点按钮的点击操作
- 全局维护一个Popover组件,点击按钮时计算按钮位置,把Popover定位到对应位置展示,所有交互逻辑都在React正常的生命周期内执行,完全支持编辑等复杂交互。
内容的提问来源于stack exchange,提问作者Phaki
相关产品推荐
相关产品推荐

