JavaScript is required

选中连线自定义工具栏

这个示例展示如何把自定义悬浮工具栏附着到当前选中的 relation-graph 连线上。工具栏会跟随连线实时几何位置,可修改线色、线形、线宽或删除连线;外围辅助窗还提供画布设置与图片导出。

自定义选中连线工具栏

这个示例构建了什么

这个示例构建了一个小型的 relation-graph 编辑工作区:当用户点击一条已有连线时,会在该连线上方打开一个浮动工具栏。用户可以通过这个工具栏修改当前选中连线的颜色、形状和宽度,也可以直接删除这条连线。

整体视觉效果刻意更偏向编辑器,而不是某个特定业务场景。点状画布背景、辅助窗口以及附着在图谱上的覆盖层,共同让这个示例更像一个聚焦于自定义连线维护的参考实现。这里的关键点在于,连线工具栏并不是固定在屏幕上的 UI;当图谱移动或缩放时,它会跟随当前选中连线的实时几何位置一起移动。

数据是如何组织的

图数据以内联方式组织成一个 RGJsonData 对象,其中包含 rootId、一个较小的 nodes 数组以及一个 lines 数组。大多数连线都继承图谱的默认配置,而连线 l4 则预先设置为正交形状,并带有显式的连接点,因此画布初始状态同时展示了默认的曲线样式,以及一个预配置好的例外连线。

在调用 setJsonData() 之前,没有额外的预处理流程。唯一有意义的准备工作,就是内联定义这份示例数据集,以及对 l4 做逐条连线覆盖。在生产环境图谱中,同样的数据结构可以用来表示工作流分支、依赖关系、审批路径,或任何以边为核心、且用户工作流中包含连线样式调整或删除操作的数据集。

relation-graph 是如何使用的

该示例在入口层使用 RGProvider,这样嵌套组件就可以消费 relation-graph 的 hooks。在 MyGraph 内部,RGHooks.useGraphInstance() 驱动完整生命周期:通过 setJsonData() 加载内联 JSON,使用 zoomToFit() 适配视图,借助 updateLine() 更新连线属性,调用 removeLine() 删除连线,并通过 setEditingNodes()setEditingLine() 协调当前编辑目标。

图谱配置很少,但都是有意为之。示例将 defaultLineShape 设置为 RGLineShape.StandardCurve,将 defaultJunctionPoint 设置为 RGJunctionPoint.ltrb,并将 defaultLineColor 设置为 #00a63e,从而建立起绿色曲线这一基线样式,之后工具栏再按需覆盖这些属性。

这里最重要的 relation-graph 技术点是自定义覆盖层模式。RGSlotOnView 在图谱表面挂载了两个组件:RGEditingConnectController 和自定义的 MyLineToolbarMyLineToolbar 通过 RGHooks.useEditingLine() 读取当前选中的连线及其实时起止点,再基于中点计算平移变换,这样工具栏就能附着在该连线上,而不是固定在浏览器视口上。

该示例还自定义了周边工作区。SCSS 文件添加了一个会随图谱变换变量同步缩放的点状编辑背景,共享的 DraggableWindow 则通过 RGHooks.useGraphStore() 读取图谱配置,使用 setOptions() 在运行时暴露滚轮与拖拽行为的调整能力,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 将图谱导出为图片。

关键交互

点击一条连线后,这条连线会成为当前激活的编辑目标,随后自定义工具栏会显示在它的上方。从这一刻起,这个工具栏就成为连线维护的主要操作界面。

点击颜色色块、形状选项或宽度选项后,会对当前激活的连线应用一次有针对性的 updateLine() 更新。这让编辑模型保持得很简单:只处理一条选中连线、一个浮动操作面板,以及立即可见的视觉反馈。

点击删除按钮会移除当前激活的连线,显示一条警告风格的全局消息,清除当前编辑中的连线,并重置选中状态。因此,删除流程同时处理了图谱数据变更和相关 UI 清理。

节点点击和画布手势负责管理编辑目标的切换。普通节点点击会替换当前编辑节点集合,带修饰键的节点点击会切换节点是否属于集合,而点击空白画布则会同时清除节点和连线的编辑状态。当画布处于框选模式时,框选结束会用选区视图内的节点替换当前编辑节点集合。

关键代码片段

下面这个片段展示了内联图数据,以及为 l4 预设的逐条连线例外配置。

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'Border color' },
        { id: 'a1', text: 'No border' },
        { id: 'a2', text: 'Plain' },
        { id: 'a1-1', text: 'Text Node' },
        { id: 'a1-4', text: 'XXX' },
        { id: 'b', text: 'Font color' },
        // ...
    ],
    lines: [
        { id: 'l1', text: 'Line1 Text', from: 'a', to: 'b' },
        { id: 'l4', text: 'Line4 Text', from: 'a', to: 'a2', lineShape: RGLineShape.StandardOrthogonal, fromJunctionPoint: RGJunctionPoint.left, toJunctionPoint: RGJunctionPoint.right },
        // ...
    ]
};

下面这个片段展示了点击与选择事件如何在节点、连线和空白画布之间交接编辑状态。

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 onLineClick = (lineObject: RGLine, linkObject: RGLink) => {
    graphInstance.setEditingLine(lineObject);
};

下面这个片段展示了自定义覆盖层如何直接挂载到图谱视图中,而不是放在单独的侧边栏里。

<RelationGraph
    options={graphOptions}
    onCanvasSelectionEnd={onCanvasSelectionEnd}
    onCanvasClick={onCanvasClick}
    onNodeClick={onNodeClick}
    onLineClick={onLineClick}
>
    <RGSlotOnView>
        <RGEditingConnectController />
        <MyLineToolbar onLinePropChange={onLinePropChange} onRemoveLine={onRemoveLine} />
    </RGSlotOnView>
</RelationGraph>

下面这个片段展示了为什么工具栏能够跟随选中连线的几何位置,并直接应用属性修改。

const editingLine = RGHooks.useEditingLine();
const toolbarXyOnCanvas = {
    x: (editingLine.startPoint.x + editingLine.endPoint.x) / 2,
    y: Math.min(editingLine.startPoint.y, editingLine.endPoint.y),
};

return (
    editingLine.line ? <div
        style={{
            transform: `translate(-50%, -100%) translate(${toolbarXyOnCanvas.x}px, ${toolbarXyOnCanvas.y - 20}px)`
        }}
    >

下面这个片段展示了连线操作界面本身:属性选项通过 onLinePropChange(...) 调用,删除操作则通过 onRemoveLine(...) 触发。

{[
    { value: RGLineShape.StandardStraight, text: 'Straight' },
    { value: RGLineShape.StandardOrthogonal, text: 'Orthogonal' },
    { value: RGLineShape.StandardCurve, text: 'Bezier' }
].map((shape) => (
    <div
        key={shape.value}
        onClick={() => onLinePropChange(editingLine.line!, 'lineShape', shape.value)}
    >
        {shape.text}
    </div>
))}

这个示例的独特之处

与附近的 change-line-pathchange-line-verticeschange-line-text 等示例相比,这个示例改变的是“选中连线的控制界面”,而不是“选中连线的编辑目标”。那些示例会保留内置的 RGEditingLineController,用于直接操作路径、端点或标签。而这个示例没有在渲染后的图谱中显示该控制器,而是通过 RGSlotOnView 配合 RGHooks.useEditingLine(),把一个自定义浮动操作卡片附着到当前激活的连线上。

它还组合了几个在示例集中相对少见的特性:由选择驱动的编辑目标管理、通过 updateLine()removeLine() 实现运行时连线变更、基于实时连线几何信息的中点覆盖层定位,以及一个暴露画布设置与导出操作的共享辅助窗口。点状画布和辅助窗口本身并不独特,但“自定义选中连线工具栏”这一模式才是这里真正的核心。

editor-button-on-line 相比,这个示例更窄、更聚焦。它整体承担的编辑器工作更少,但在选中连线属性编辑上走得更深:通过一个专用覆盖层同时提供颜色、形状、宽度和删除操作。与 line-vertex-on-node 相比,它关注的是在连线被选中之后如何维护一条已有连线,而不是从一个选中节点创建新的连接。

这个模式还适用于哪里

这种模式非常适合那些边本身承载工作流含义,并且需要轻量级内联操作的图谱工具。例如审批路径编辑器、依赖关系图、API 调用图、网络拓扑维护工具,以及那些用户需要在不打开模态框的情况下,直接重设样式、停用或移除某条连接的流程图。

同样的做法也适用于需要品牌化或业务定制边控件的产品。团队可以继续使用 relation-graph 提供布局、视口行为和编辑状态管理,但将默认的选中连线 UI 替换为业务专属操作,例如修改优先级、分配负责人、切换路由策略,或打开一个自定义的边详情面板。