You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 22:06:04