JavaScript is required

渐变连线图编辑器

这个示例把一个小型 relation-graph 画布变成轻量编辑器,支持基于调色板创建节点、上下文创建连线,以及按端点着色的渐变边。它还展示了自定义连线插槽如何与选择、删除、画布设置和图片导出共存。

构建一个由调色板驱动的渐变线图编辑器

这个示例构建的内容

这个示例构建的是一个紧凑的图编辑器,而不是只读的关系视图。初始画布会显示一个小型、类似工作流的路由图,其中包含圆角图标节点、与节点主体分离的文本标签,以及颜色从源节点过渡到目标节点的曲线连线。

用户可以选择节点和连线、框选多个节点、从顶部中央的调色板拖拽图标模板来创建新节点、通过节点旁的工具栏创建新的出边、删除已选节点、删除已选连线、调整节点尺寸、在浮动设置面板中修改画布滚轮与拖拽行为,并将当前图导出为图片。最重要的一点是,这个示例在保留 relation-graph 编辑模型的同时,替换了节点和连线两者的可视化表现层。

数据是如何组织的

初始图数据在 MyGraph.tsx 中以内联的 RGJsonData 形式声明。每个节点存储 idtextcolor,以及一个 data.myIcon 字段,节点插槽会使用这个字段来选择 Lucide 图标。每条连线只存储 idfromtotext,因此图一开始采用的是一种简单、偏业务友好的数据形状。

在调用 setJsonData(...) 之前几乎没有做预处理。图会被直接加载,然后在加载后的一个遍历过程中,对 graphInstance.getNodes() 返回的每个节点重新赋值颜色,颜色来自一个固定调色板中的随机值。运行时创建的节点也遵循同样的结构:顶部调色板提供图标名称,随机分配一个颜色,回调中再补上 data.myIcon 和生成的节点 id。

在生产系统中,这种结构可以表示支持渠道、审批步骤、服务依赖或工作流状态。节点的 data 字段是主要的扩展点,可以在不改变核心图结构的前提下承载图标 key、业务类型元数据或仅供编辑器使用的状态。

relation-graph 是如何使用的

这个 demo 包裹在 RGProvider 中,然后 MyGraph 通过 RGHooks.useGraphInstance() 作为核心控制入口。图本身运行在树形布局下,具有固定的水平和垂直间距、曲线连线、左右连接点默认值、默认 3 的线宽、鼠标滚轮缩放以及拖拽移动画布行为。

三个插槽定义了大部分自定义 UI。RGSlotOnNode 用图标卡片和渲染在节点下方的标题替换默认节点主体。RGSlotOnLineCustomLineContent 替换默认边渲染,但仍然将路径和标签几何的计算委托给 generateLinePath(...)generateLineTextStyle(...),因此自定义视觉仍能与 relation-graph 的内部机制保持对齐。RGSlotOnView 增加了固定在视口上的 UI,这些元素不会随着画布缩放而移动,包括顶部创建调色板、对齐参考线、缩放手柄、节点工具栏、连线控制器和连接控制器。

这个示例还直接使用了 relation-graph 的编辑 API。setJsonData(...)zoomToFit()getNodes()updateNode(...) 用于初始化并重新设置图样式。setEditingNodes(...)toggleEditingNode(...)setEditingLine(...)getNodesInSelectionView(...)clearChecked() 用于在节点点击、连线点击、矩形选择和空白画布点击之间同步选择状态。startCreatingNodePlot(...) 为顶部调色板提供拖拽创建节点的能力,而 startCreatingLinePlot(...) 则为选中节点提供上下文式的连线创建能力。removeNode(...)removeLine(...) 会直接修改运行中的图数据,共享的浮动窗口则通过 setOptions(...)prepareForImageGeneration()restoreAfterImageGeneration() 来改变画布行为并导出图片。

样式通过本地 SCSS 处理,而不是通过额外的图数据字段来完成。样式表为画布提供了分层的径向渐变背景,重设了内置工具栏颜色,为节点添加白色边框和圆角,为连线标签应用渐变文字效果,并将已选连线改为带粉色高亮的删除状态。

关键交互

  • 点击节点会将其选中并进入编辑状态。按住 ShiftCtrlMeta 时,同样的操作会变为切换选择,因此这个示例支持借助修饰键进行多选。
  • 拖拽顶部某个图标卡片会启动 startCreatingNodePlot(...)。在画布上释放后,会在落点附近插入一个新节点,并立刻将其设为当前的编辑节点。
  • 完成一次矩形选择后,编辑中的节点集合会被替换为 getNodesInSelectionView(...) 返回的节点。
  • 当且仅当有一个节点处于编辑状态时,MyNodeToolbar 会显示在其周围。它的侧边和底部按钮会启动出边创建,顶部按钮则用于删除该节点。
  • 点击一条连线会将其设为当前的编辑连线。在这种状态下,自定义连线标签会从文本切换为内联删除按钮。
  • RGEditingLineController 挂载时使用了 textEditable={false}pathEditable={false}。在这个示例中,它充当的是一个紧凑的连线端点编辑辅助工具,而不是完整的路径塑形或文本编辑工具。
  • 浮动辅助窗口包含手动重新着色按钮、用于配置滚轮和拖拽模式的设置面板,以及图片导出操作。

关键代码片段

这段代码定义了编辑器画布的基础图行为。

const graphOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'tree',
        treeNodeGapH: 200,
        treeNodeGapV: 40
    },
    defaultLineShape: RGLineShape.StandardCurve,
    defaultJunctionPoint: RGJunctionPoint.lr,
    defaultLineWidth: 3,
    wheelEventAction: 'zoom',
    dragEventAction: 'move',
};

这个回调把顶部调色板变成了一个支持拖拽放置的节点模板来源。

graphInstance.startCreatingNodePlot(e.nativeEvent, {
    templateNode: {
        text: iconName,
        color: randomColor,
        data: {
            myIcon: iconName
        }
    },
    onCreateNode: (x, y, nodeTemplate) => {
        const newNode = {
            ...nodeTemplate,
            id: `N-${graphInstance.generateNewNodeId()}`,

这里的部分代码会把新放下的节点持久化到当前图中,并立即将其交给编辑状态管理。

        const newNode = {
            ...nodeTemplate,
            id: `N-${graphInstance.generateNewNodeId()}`,
            x: x - 20,
            y: y - 20
        };
        graphInstance.addNodes([newNode]);
        graphInstance.setEditingNodes([graphInstance.getNodeById(newNode.id)]);
    }
});

这个回调让节点旁的工具栏创建的是真实边,而不只是临时引导线。

graphInstance.startCreatingLinePlot(e.nativeEvent, {
    template: { ...lineTemplate },
    fromNode: node,
    onCreateLine: (from, to, finalTemplate) => {
        if ('id' in to) {
            graphInstance.addLines([{
                ...finalTemplate,
                from: (from as RGNode).id,
                to: (to as RGNode).id,
                text: 'New Line'
            }]);
        }
    }
});

连线插槽中的这部分代码保留了 relation-graph 的路径计算,同时根据当前端点颜色生成自定义的 SVG 渐变。

const linePathInfo = useMemo<RGLinePathInfo>(
    () => graphInstance.generateLinePath(lineConfig),
    [lineConfig]
);
const textStyle = graphInstance.generateLineTextStyle(lineConfig, linePathInfo);
const fromNodeColor = lineConfig.from.color || '#666666';
const toNodeColor = lineConfig.to.color || '#666666';
const linearGradientId = 'gradient-for-' + lineConfig.line.id;

这个条件分支会在连线被选中时,把普通标签切换为内联删除操作。

{checked ?
    <button
        className="cursor-pointer h-8 w-8 bg-pink-400 text-white rounded flex place-items-center justify-center"
        onClick={() => {
            onMyRemoveIconClick(lineConfig.line);
        }}
    >
        <Trash2Icon size={18} />
    </button>

这个辅助面板中的代码展示了该示例如何复用图 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();
};

这个示例的独特之处

与相邻的示例 create-line-from-nodeline-vertex-on-node 相比,这个 demo 覆盖的范围更广。它并不止于从现有节点出发进行上下文式连线创建;它还允许用户通过顶部调色板创建全新的节点,然后在同一画布中继续编辑这些节点。

customize-line-toolbar 相比,这里的选中连线体验更深地嵌入在线本身之中。同一个 RGSlotOnLine 实现同时负责渐变绘制、标签渲染、点击透传以及内联删除,因此与选中连线相关的操作不需要移动到单独的浮动工具栏中。

change-line-verticesgee-node-alignment-guides 相比,这些内置编辑辅助能力在这里是更完整创作界面中的组成部分。对齐参考线、缩放手柄、连线控制器和连接控制器会与自定义节点和连线渲染一起出现,这使它成为一个异常密集的参考示例,适合用于理解轻量级编辑器组合方式,而不是单独演示某一个控制器。

最值得关注的是这种少见的组合:由调色板驱动的节点创建、基于节点上下文的连线创建、带选择感知的内联边删除、基于端点颜色的自定义边渲染,以及用于重新着色、画布设置和图片导出的共享辅助 UI。这个组合使该示例成为一个很强的起点,适合那些希望在不先构建完整应用外壳的情况下实现紧凑图编辑器的团队。

这种模式还能用在哪里

这种模式很适合用于那些既需要全局方式来添加新元素,又需要为现有元素提供上下文工具的场景。示例包括支持路由设计器、审批流构建器、服务交接地图、事件响应预案以及轻量级架构画布。

当团队希望使用附着在图上的编辑控件,而不是侧边面板时,它也很有价值。顶部调色板可以表示可复用的节点模板,节点工具栏可以暴露上下文相关操作,自定义连线插槽则可以在不放弃 relation-graph 选择模型的前提下,通过视觉方式表达状态或归属。

最后,这也是一个构建品牌化编辑器的实用参考。这个示例表明,视觉层可以通过插槽和本地 CSS 进行高度定制,同时仍然依赖 relation-graph 来处理布局、选择、几何计算、控制器叠加层以及导出准备工作。