JavaScript is required

节点环绕快捷操作栏

这个示例构建了一个轻量级图编辑工作区,在当前处于 relation-graph 编辑状态的节点周围挂载了自定义快捷操作托盘。页面展示了一个全高的点状画布、一个浮动辅助窗口,以及位于编辑节点上方、下方、左侧、右侧和四个角落的白色操作组。

编辑节点周围的八方向快捷操作

这个示例构建了什么

这个示例构建了一个轻量级图编辑工作区,在当前处于 relation-graph 编辑状态的节点周围挂载了自定义快捷操作托盘。页面展示了一个全高的点状画布、一个浮动辅助窗口,以及位于编辑节点上方、下方、左侧、右侧和四个角落的白色操作组。

用户可以点击节点,通过组合键把更多节点切换进编辑集合,完成框选后重新定位覆盖层,并点击空白画布区域清除当前编辑状态。这个示例的重点不是按钮背后的业务逻辑,而是精确演示如何在一个或多个节点周围放置上下文控制项。

数据是如何组织的

图数据以内联方式声明为一个 RGJsonData 对象,包含一个 rootId、一个扁平的 nodes 数组和一个扁平的 lines 数组。在这个示例中,数据集很小:11 个节点和 11 条连线,字段仅包含简单的 idtextfromto

在调用 setJsonData(...) 之前,没有任何外部获取或转换流程。唯一的数据准备步骤发生在 initializeGraph() 内部:示例构建 JSON 对象,将其加载到图实例中,使视口自适应,然后解析节点 da,从而让编辑覆盖层在首次渲染时就立即可见。

在真实产品中,同样的结构可以表示人员与汇报关系、工作流步骤与状态迁移、服务与依赖关系,或任何需要在选中节点周围提供上下文操作的实体图。

relation-graph 是如何使用的

入口组件用 RGProvider 包裹页面,MyGraph 通过 RGHooks.useGraphInstance() 读取 provider 作用域内的实例。该实例负责初始数据加载、视口自适应、编辑节点更新、编辑连线重置、选择框结果解析以及 checked 状态清除。

这个示例没有定义自定义布局对象。它依赖 relation-graph 在 setJsonData(...) 期间的常规布局处理,然后调用 zoomToFit(),让初始图形填满视口。

RelationGraph 被配置为 showToolBar: false,这会移除内置工具栏,并把页面的交互外壳交给自定义 UI。组件注册了 onNodeClickonCanvasSelectionEndonCanvasClick,这三个回调都会通过实例 API 更新同一套编辑状态模型,例如 toggleEditingNode(...)setEditingNodes(...)getNodesInSelectionView(...)setEditingLine(null)

覆盖层本身是这个示例里最关键的 relation-graph 技巧。RGSlotOnView 挂载 RGEditingNodeController,而在这个控制器内部,示例使用绝对定位 class 渲染自定义 HTML 托盘。这些托盘不是 relation-graph 的内置预设;它们只是普通的 <div> 块,通过继承控制器提供的位置参考,再利用 transform 放置在编辑节点的不同侧边。

共享的 DraggableWindow 提供了第二层集成。它的设置面板通过 RGHooks.useGraphStore() 反映当前的滚轮模式和拖拽模式,通过 setOptions(...) 更新这些模式,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 导出画布。这个辅助窗口很实用,但相较于节点周围覆盖层这一模式,它属于次要部分。

关键交互

  • 初始化后会将两个节点预先放入编辑状态,因此快捷操作托盘会立即可见。
  • 不带组合键点击节点时,会用该节点替换当前编辑节点集合,并清除任何处于激活状态的编辑连线。
  • 按住 ShiftCtrlMeta 点击节点时,会在编辑选择中切换该节点,而不是替换整个集合。
  • 当选择框操作完成时,示例通过 getNodesInSelectionView(...) 解析被框住的节点,并将该列表作为新的编辑节点集合。
  • 点击空白画布区域时,会清除编辑节点、清除当前激活的编辑连线,并移除 checked 状态。
  • 浮动辅助窗口可以被拖动,可以打开设置面板,可以在运行时切换滚轮和拖拽行为,还可以把图导出为图片。
  • 快捷操作托盘只是占位示例。它们展示的是放置方式和目标绑定,而不是真正的变更命令。

关键代码片段

下面这个片段展示了示例以内联方式构建图数据、使视口自适应,并预置两个编辑节点,从而让覆盖层在加载时出现。

const initializeGraph = async () => {
    const myJsonData: RGJsonData = {
        rootId: 'a',
        nodes: [
            { id: 'a', text: 'Border color' },
            // ...
        ],
        lines: [
            { id: 'l1', from: 'a', to: 'b' },
            // ...
        ]
    };
    await graphInstance.setJsonData(myJsonData);
    graphInstance.zoomToFit();

    const nodeD = graphInstance.getNodeById('d');
    const nodeA = graphInstance.getNodeById('a');
    if (nodeD && nodeA) {
        graphInstance.setEditingNodes([nodeD, nodeA]);
    }
};

下面这个片段展示了普通点击、组合键点击、选择框完成以及画布重置,都会更新同一套编辑节点状态。

const onNodeClick = (nodeObject: RGNode, $event: RGUserEvent) => {
    if ($event.shiftKey || $event.ctrlKey || ($event.metaKey && !$event.altKey)) {
        graphInstance.toggleEditingNode(nodeObject);
    } else {
        graphInstance.setEditingNodes([nodeObject]);
    }
    graphInstance.setEditingLine(null);
};

const onCanvasSelectionEnd = (selectionView: RGSelectionView) => {
    const willSelectedNodes: RGNode[] = graphInstance.getNodesInSelectionView(selectionView) || [];
    graphInstance.setEditingNodes(willSelectedNodes);
};

下面这个片段展示了图配置如何关闭内置工具栏,并在视图插槽中挂载自定义覆盖层。

const graphOptions: RGOptions = {
    showToolBar: false
};

<RelationGraph
    options={graphOptions}
    onCanvasSelectionEnd={onCanvasSelectionEnd}
    onCanvasClick={onCanvasClick}
    onNodeClick={onNodeClick}
>
    <RGSlotOnView>
        <RGEditingNodeController>

下面这个片段展示了其中一个自定义托盘。相同的模式也被重复用于其他侧边和角落。

{/* top */}
<div className="pointer-events-auto absolute left-0 top-0 transform translate-y-[-40px]">
    <div className="w-fit flex gap-1 flex-nowrap whitespace-nowrap bg-white border border-gray-300 p-1 rounded shadow">
        <div className="bg-blue-500 text-white text-xs hover:bg-blue-700 px-1 py-0.5 rounded">top1</div>
        <div className="bg-blue-500 text-white text-xs hover:bg-blue-700 px-1 py-0.5 rounded">top2</div>
    </div>
</div>
{/* ... similar blocks follow for bottom, left, right, and all four corners ... */}

下面这个片段展示了共享辅助窗口如何通过 relation-graph API 导出图像,并改变运行时画布行为。

const downloadImage = async () => {
    const canvasDom = await graphInstance.prepareForImageGeneration();
    let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
    if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
        graphBackgroundColor = '#ffffff';
    }
    const imageBlob = await domToImageByModernScreenshot(canvasDom, {
        backgroundColor: graphBackgroundColor
    });
    await graphInstance.restoreAfterImageGeneration();
};

下面这个片段展示了样式表如何把画布变成类似编辑器的点状表面,并随着图视口一起缩放。

.relation-graph {
    --rg-canvas-scale: 1;
    --rg-canvas-offset-x: 0px;
    --rg-canvas-offset-y: 0px;
    background-position: var(--rg-canvas-offset-x) var(--rg-canvas-offset-y);
    background-size: calc(var(--rg-canvas-scale) * 15px) calc(var(--rg-canvas-scale) * 15px);
    background-image: radial-gradient(circle, rgb(197, 197, 197) calc(var(--rg-canvas-scale) * 1px), transparent 0);
}

这个示例的独特之处

与附近的其他编辑示例相比,这个示例的突出点在于它主要把 RGEditingNodeController 当作位置参考,而不是变更工具。相较于 create-line-from-nodeline-vertex-on-node,这里的覆盖层并不是连线编排工作流,而是一种视图层模式,用于把上下文控制项停靠在当前编辑节点或多个节点周围。

相比 batch-operations-on-nodes,这里的重点也不是连接诸如重新着色、调整尺寸或删除之类的命令。这个示例真正不同的地方在于空间覆盖:它展示了顶部、底部、左侧、右侧和四个角落的托盘布局,并让这种布局模式兼容预置选择、基于组合键的多选以及框选后的重新定位。

相比 gee-node-resizegee-node-alignment-guides,这里可复用的价值也不是内置缩放手柄或拖拽辅助,而是点状编辑画布、浮动工具窗口、showToolBar: false 以及跟随当前编辑节点集合的自定义 HTML 覆盖层这一组合。

这一模式还适用于哪些场景

这一模式非常适合工作流和流程编辑器,在这些场景中,被选中的步骤需要就近显示诸如分支、批准、拒绝、分配或查看之类的操作。

它也适用于服务拓扑工具、依赖关系图以及知识图谱整理界面。在这些场景里,用户通常已经知道自己要执行什么命令,但需要这些命令出现在当前目标节点附近,而不是远处的全局工具栏中。

在内部管理工具中,同样的方法还可以用于组织架构图、审核队列或资产关系视图,让同一套选择模型同时驱动单节点和多节点的上下文控制项。