JavaScript is required

节点悬停详情提示框

这个示例展示如何在自定义 `RGSlotOnNode` 渲染器中,于节点悬停时打开不可交互的详情提示卡,并在 `RGSlotOnView` 中渲染。它使用相对容器的指针坐标、悬停进入/离开处理,以及内联图标节点数据,在保持图谱只读的同时暴露节点元信息。

构建仅在悬停时显示的节点详情提示卡

这个示例构建了什么

这个示例构建了一个只读的 relation graph,当鼠标悬停在自定义节点上时,会在指针附近打开一个小型详情卡片。可见场景被刻意压缩得较为紧凑:绿色的圆形图标节点、渲染在节点下方的短标签,以及一个始终停留在图谱视图内部的白色浮动 tooltip。

用户可以通过将指针移到节点上来查看节点详情,并在离开该节点时关闭卡片。节点点击和连线点击也都存在,但它们只会把选中的对象输出到控制台,因此这里的核心价值是被动查看,而不是编辑或执行命令。

这个示例最有用的思路,是把职责拆分到 node slot 和 view slot:node slot 负责悬停触发,view slot 负责覆盖层 UI。

数据是如何组织的

图数据是在 initializeGraph() 内部以内联方式创建的,一个 RGJsonData 对象直接定义了这些数据。它包含 rootId: '2'、一个 nodes 数组,以及一个带有明确连线 id 和标签的 lines 数组。每个节点还携带 data.myicon,自定义节点渲染器会用它来选择一个 Lucide 图标。

在调用 setJsonData() 之前没有任何预处理步骤。组件直接在代码中构建最终 payload,并原样加载,然后将视口居中并适配显示。在业务应用中,这种结构同样可以表示产品、服务、设备、部门或依赖项,而 data.myicon 也可以替换为类型、状态或分类元数据,用来选择对应的自定义视觉样式。

relation-graph 是如何使用的

index.tsx 通过 RGProvider 包裹这个示例,而 MyGraph.tsx 使用 RGHooks.useGraphInstance() 获取实时图实例。一个 useEffect() hook 会在实例可用后执行 initializeGraph(),通过 setJsonData() 加载内联数据,然后调用 moveToCenter()zoomToFit()

图配置非常少,而且主要聚焦视觉层面。defaultNodeColor 提供了绿色填充,自定义 slot 会通过 node.color 读取它;defaultNodeShape 被设置为 RGNodeShape.circledefaultJunctionPoint 被设置为 RGJunctionPoint.border。从当前审阅的源码来看,并没有显式设置布局选项,因此这个数据集中的节点排列方式依赖 relation-graph 的默认行为。

主要定制发生在两个 slot 上。RGSlotOnNode 用圆形图标渲染器替换了内置节点主体,并直接在节点 DOM 上绑定 onMouseEnteronMouseLeaveRGSlotOnView 则在图场景内部以覆盖层 HTML 的方式渲染提示卡。提示卡的位置不是通过图事件回调计算的,而是根据相对于一个包装元素的指针坐标来计算。

图级别的点击事件被有意弱化。onNodeClickonLineClick 都绑定在 RelationGraph 上,但两个处理器都只是在控制台打印对象。这让示例保持在 viewer 模式下,也使悬停覆盖层成为唯一真正有意义的图交互。

本地样式较轻,且目标明确。SCSS 文件通过 .c-my-rg-node 定义了圆形节点的尺寸和居中方式,而提示卡中的各行则通过 .c-node-menu-item 控制间距和排版。覆盖层本身还设置了 pointerEvents: 'none',这样就不会把卡片变成交互式菜单,而是保留 slotted node 上的悬停流程。

关键交互

  • 悬停节点时,会在当前指针位置附近打开一个详情提示卡。
  • 将指针移出节点后,提示卡会立即关闭。
  • 提示卡保持为非交互状态,因为它的覆盖层容器禁用了 pointer events。
  • 点击节点会输出节点对象,但不会改变图状态。
  • 点击连线会输出连线对象,但不会打开任何额外 UI。

关键代码片段

这个 options 配置块说明,这个示例主要把 relation-graph 当作 viewer 使用,只定制了基础的节点形状、颜色和连线连接点模式。

const graphOptions: RGOptions = {
    defaultNodeColor: 'rgba(66,187,66,1)',
    defaultNodeShape: RGNodeShape.circle,
    defaultJunctionPoint: RGJunctionPoint.border,
};

这个内联 payload 说明,图标选择由每个节点自身的元数据驱动,而图内容则直接在组件代码中组装完成。

const myJsonData: RGJsonData = {
    rootId: '2',
    nodes: [
        { id: '1', text: 'Node-1', data: { myicon: 'star' } },
        { id: '2', text: 'Node-2', data: { myicon: 'settings' } },
        // ... more icon-tagged nodes ...
        { id: '5', text: 'Node-5', data: { myicon: 'gift' } }
    ],
    lines: [
        { id: 'l1', from: '7', to: '71', text: 'Investment' },
        // ... more labeled lines ...
    ]
};

这段初始化流程证明,图实例是通过标准的基于 hook 的启动流程加载的。

await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();

这个悬停处理器是核心实现细节,它通过相对于包装元素的指针坐标来定位覆盖层。

const showNodeTips = (nodeObject: RGNode, $event: React.MouseEvent) => {
    setCurrentNode(nodeObject);
    if (myPage.current) {
        const _base_position = myPage.current.getBoundingClientRect();
        setIsShowNodeTipsPanel(true);
        setNodeMenuPanelPosition({
            x: $event.clientX - _base_position.x + 10,
            y: $event.clientY - _base_position.y + 10
        });
    }
};

这个 node slot 片段表明,悬停触发器是挂在自定义节点 DOM 上的,而不是挂在图范围的鼠标移动监听器上。

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div
            className="h-full"
            onMouseEnter={(e) => showNodeTips(node, e)}
            onMouseLeave={() => hideNodeTips()}
        >
            <div className="c-my-rg-node" style={{ backgroundColor: node.color }}>
                <GetLucideIcon name={node.data?.myicon} />
            </div>
        </div>
    )}
</RGSlotOnNode>

这个 view slot 片段证明,提示卡是在图场景内部渲染的,并且有意避免拦截指针输入。

<RGSlotOnView>
    {isShowNodeTipsPanel && currentNode && (
        <div
            className="p-3 bg-white border border-gray-300 shadow-xl absolute rounded-lg"
            style={{
                left: `${nodeMenuPanelPosition.x}px`,
                top: `${nodeMenuPanelPosition.y}px`,
                zIndex: 1000,
                pointerEvents: 'none'
            }}
        >

这个示例的独特之处

对比数据表明,这个示例与 node-menu-2node-menunode-line-tips-contentmenu 比较接近,但它把共享的 slot + overlay 构建方式用于更收敛的目标。与 node-menu-2node-menu 相比,关键区别在于这个示例是被动的、由悬停驱动的:它不会打开可操作菜单,不会在节点、连线和画布目标之间切换,也不需要为交互面板实现点击外部关闭逻辑。

node-line-tips-contentmenu 相比,这里是一个更小巧的、只针对节点的方案。它没有加入连线命中检测、右键菜单、辅助窗口或混合覆盖层类型。对比记录还强调了这里一个相对少见的组合:绿色圆形图标节点、节点下方的徽标式标签、相对于包装元素的指针定位计算、悬停进入与离开处理器,以及在一个只读 viewer 中带有 pointerEvents: 'none' 的白色浮动卡片。

因此,当需求是在图场景内部进行轻量级节点查看,同时又不想引入菜单状态、编辑流程或图变更时,这个示例会是一个更合适的起点。它不应该被描述为唯一的覆盖层示例,但它确实是非交互式节点悬停详情模式中最清晰的参考之一。

这种模式还适用于哪里

这种模式很适合迁移到依赖关系图、服务拓扑查看器、产品关系浏览器、设备网络和组织浏览界面中。在这些场景里,用户需要快速查看只读详情,而不必离开图本身。提示卡内容可以替换为健康摘要、归属字段、状态元数据、风险标签或紧凑指标,同时继续沿用相同的悬停触发和 view overlay 结构。

它同样是一个适合继续扩展的检查型 UI 起点。团队可以先从这个被动卡片开始,后续再加入视口边缘限制、悬停延迟、固定详情面板或适用于触摸设备的替代方案,同时仍然保留“slotted node 触发器 + 场景内覆盖层渲染”这个基础思路。