使用插槽实现节点上下文菜单
这是一个紧凑 relation-graph 示例:从自定义插槽节点 DOM 打开节点操作菜单,并通过 `RGSlotOnView` 渲染覆盖层。它演示相对指针定位、原生右键菜单抑制、点击外部关闭,以及在不把图变成编辑器的前提下提供统一成功反馈。
使用 relation-graph Slots 实现节点上下文菜单
这个示例构建了什么
这个示例构建了一个只读 relation graph,其节点不是渲染为默认的节点主体,而是渲染为绿色的圆形图标徽标。在图场景内部点击或右键单击节点时,会在指针位置打开一个浮动操作菜单;选择其中一个固定操作后,会先显示成功消息,然后关闭菜单。
这个示例的核心要点是两个 slot 层之间的职责拆分:RGSlotOnNode 提供可交互的触发表面,RGSlotOnView 负责在图视口内渲染覆盖菜单。
数据如何组织
图数据在 initializeGraph 内以内联方式声明为一个 RGJsonData 对象。它使用 rootId: '2',包含一个带有 id、text 和 data.myicon 的 nodes 数组,以及一个每条记录都显式包含 id、from、to 和 text 字段的 lines 数组。
在调用 setJsonData 之前,这里没有任何预处理步骤,只有在代码中组装这个对象。在生产环境图谱中,data.myicon 可以替换为角色、状态、类型或分类元数据,同时驱动节点外观和可用的节点操作。
relation-graph 是如何使用的
RGProvider 包裹整个 demo,以便 RGHooks.useGraphInstance() 能读取当前激活的图实例。useEffect 会等待该实例可用,然后调用 setJsonData()、moveToCenter() 和 zoomToFit() 来加载示例网络并让其适配视图。
本地的 graphOptions 对象只调整默认展示效果:节点颜色为绿色,节点形状为圆形,连接点在边界上连接。审阅到的文件没有设置显式布局选项,因此最终排布依赖于此示例之外的 relation-graph 默认行为。
RGSlotOnNode 用自定义 DOM 替换默认节点主体。该 slot 会读取 node.data.myicon,渲染一个 Lucide 图标,在节点下方添加一个标题块,并将 onClick 和 onContextMenu 都绑定到同一个打开菜单的处理函数上。
RGSlotOnView 在图视图层内部渲染菜单面板。React state 会跟踪当前激活的节点、面板是否可见,以及相对于包裹元素的菜单坐标。这里注册的 onNodeClick 和 onLineClick 处理器只是次要部分;在审阅到的源码中,它们只会向控制台输出日志。该示例没有实现图编辑或数据变更。
样式分散在 SCSS 和内联样式之间。SCSS 定义圆形节点外壳和白色浮动菜单卡片,内联样式则负责把标签块放在每个节点下方。
关键交互
左键单击自定义节点,会打开与右键单击相同的菜单。
右键单击不会显示浏览器原生上下文菜单,因为节点处理函数调用了 preventDefault()。
菜单位置基于相对于包裹元素的指针坐标计算,因此它会在交互发生的位置出现,并且不会离开图场景。
点击面板外部会关闭它,而点击面板内部不会关闭,因为菜单阻止了事件冒泡,并由包裹元素处理点击空白处关闭的逻辑。
选择某个操作不会改变图数据。它会通过 SimpleGlobalMessage 发送一条成功提示,然后隐藏菜单。
关键代码片段
这个 options 对象表明,该示例改变了节点外观和连接点行为,但没有引入自定义布局配置。
const graphOptions: RGOptions = {
defaultNodeColor: 'rgba(66,187,66,1)',
defaultNodeShape: RGNodeShape.circle,
defaultJunctionPoint: RGJunctionPoint.border
};
这个处理函数证明,菜单位置是根据相对于包裹元素的指针位置计算的,而不是根据图模型坐标计算的。
const showNodeMenus = (node: RGNode, $event: React.MouseEvent<HTMLDivElement>) => {
setCurrentNode(node);
if (myPage.current) {
const _base_position = myPage.current.getBoundingClientRect();
setIsShowNodeMenuPanel(true);
setNodeMenuPanelPosition({
x: $event.clientX - _base_position.x,
y: $event.clientY - _base_position.y
});
}
$event.stopPropagation();
$event.preventDefault();
};
这个节点 slot 才是真正的触发表面:点击和上下文菜单事件都绑定在自定义节点 DOM 上,而不是依赖图级别的上下文菜单钩子。
<RGSlotOnNode>
{({ node }: RGNodeSlotProps) => (
<div
className="c-my-rg-node"
onClick={(event) => showNodeMenus(node, event)}
onContextMenu={(event) => showNodeMenus(node, event)}
>
<NodeIcon name={node.data?.myicon} />
<div>
{node.data?.myicon}
</div>
</div>
)}
</RGSlotOnNode>
这个视图 slot 表明,菜单是在图层内部渲染的,并且只会在本地可见性状态为 true 时存在。
<RGSlotOnView>
{isShowNodeMenuPanel && (
<div
className="pointer-events-auto context-menu-panel"
style={{ left: nodeMenuPanelPosition.x, top: nodeMenuPanelPosition.y }}
onClick={(e) => e.stopPropagation()}
>
<div className="py-1 px-2 text-gray-400 text-xs border-b">
Node Actions:
</div>
{/* menu items */}
</div>
)}
</RGSlotOnView>
这个操作处理函数确认,菜单选择只会发出共享的成功反馈,然后关闭面板。
const doAction = (actionName: string) => {
SimpleGlobalMessage.showMessage({
message: `Performed action ${actionName} on node: ${currentNode?.text}`,
type: 'success'
});
setIsShowNodeMenuPanel(false);
};
这个示例的独特之处
对比数据将这个示例归在 node-menu、node-tips、simple、node-content-lines 和 node 附近,但它的关注点更窄。它最突出的组合是 RGSlotOnNode 加 RGSlotOnView、基于指针位置的菜单状态、对浏览器上下文菜单的抑制、点击外部关闭,以及在一个原本只读的查看器中通过 toast 确认操作。
与 node-menu 相比,这不是一个面向节点、连线和画布的全局上下文菜单系统。只有自定义节点 DOM 会打开菜单,并且同一个触发表面同时支持左键和右键。与 node-tips 相比,这个浮动面板是可操作的而不是信息性的,因为用户可以点击操作并收到反馈,而不是只看到悬浮信息。与 simple 和 node 相比,这里的视图 slot 用于临时性的上下文 UI,而不是持久性的导航或工具组件。
因此,当需求是在一个基本固定的图上为每个节点提供操作,而不是构建更宽泛的编辑器或仪表盘时,这个示例会是更强的起点。
这种模式还适用于哪里
这种模式可以迁移到组织架构图中,在那里一个节点需要打开诸如查看资料、分配负责人或跳转到详情页之类的操作。
它同样适用于服务地图、资产拓扑查看器和知识图谱浏览器,这些场景中的图保持只读,但每个节点都需要轻量级的操作命令。
同样的结构还可以扩展到工作流监控、案件管理或依赖分析工具中,只需把硬编码的操作列表替换为基于权限的命令,并把 data.myicon 替换为领域元数据。