JavaScript is required

节点局部拖拽手柄

这个示例加载固定布局图,禁用整节点拖拽,仅允许从带 `rg-node-drag-handler` 标记的插槽子区域拖动。它通过节点元数据混合“标题条拖拽”和“移动按钮拖拽”两种手柄样式,并在共享悬浮辅助面板中提供滚轮模式、画布拖拽模式和图片导出。

将固定布局节点移动限制在显式拖拽手柄上

这个示例构建了什么

这个示例构建了一个全高的 relation-graph 工作区,并且有意限制了节点移动。用户会看到一个放置在点状画布上的小型固定位置图谱、选中节点内部的琥珀色手柄区域、右侧工具栏,以及图谱上方悬浮的辅助窗口。

用户只能重新定位部分节点,而且只能从每个节点内部渲染出来的特定手柄区域开始拖动。有些节点把整条头部区域作为手柄,有些节点则在左上角提供一个小型移动徽标作为手柄,没有这两种标记的节点则保持不可拖动。这种选择性移动模式是本示例的核心。

页面还包含一些围绕图谱的共享工具行为。辅助窗口可以拖动或最小化,它的设置面板可以切换画布滚轮和拖动画布模式,当前图谱也可以导出为图片。

数据是如何组织的

图数据直接在 initializeGraph() 内以内联方式声明为一个 RGJsonData 对象。它包含 rootIdnodes 数组和 lines 数组。由于本示例使用的是固定布局而不是自动布局引擎,因此每个节点都使用显式的 xy 坐标进行定义。

这里重要的预处理思路是轻量元数据,而不是结构变换。代码把 hasHeaderhasMoveButton 标记存放在 node.data 中,节点插槽稍后会读取这些标记,以决定渲染哪一种拖拽交互提示。还有一个节点单独覆盖了 widthheight,因此这个示例图同时混合了不同的节点尺寸和形状。

在调用 setJsonData(...) 之前,没有单独的规范化处理过程。代码在本地构建这个 JSON 对象,直接把它传给图实例,然后再让视口适配。在真实产品中,同样的结构可以表示工作流卡片、访问控制实体、拓扑项或架构模块,其中某些节点类型只应允许从指定区域移动。

relation-graph 的使用方式

index.tsx 使用 RGProvider 包裹整个演示,这样 MyGraph 和共享的悬浮辅助组件都可以访问同一个当前激活的图实例。在 MyGraph 内部,RGHooks.useGraphInstance() 被用来通过 setJsonData(...) 加载内联数据集,随后调用 zoomToFit(),这样手工摆放的场景在挂载后就能立即完整可见。

图配置定义了交互模型。layout.layoutName 被设为 fixeddisableDragNode 被设为 true,默认节点和连线颜色也设置为与手柄区域一致的琥珀色加白色主题。defaultJunctionPoint 被设为 RGJunctionPoint.border,内置工具栏则被移动到了画布右侧。

主要扩展点是 RGSlotOnNode。这个示例没有使用默认的节点文本渲染,而是通过插槽为每个节点创建自定义 HTML,并有选择地把 rg-node-drag-handler 类应用到头部条带或移动图标区域上。这就是在全局禁用整节点拖动之后,只在允许的局部子区域重新启用拖动的实现机制。

这个示例没有使用视图级或画布级插槽。画布样式来自 my-relation-graph.scss,它使用 --rg-canvas-scale 和偏移变量,在图谱后面绘制出一个带点阵的工作台背景。

悬浮的 DraggableWindowCanvasSettingsPanel 是共享辅助组件,并不是这个示例独有的逻辑。它们通过 RGHooks.useGraphStore() 反映当前交互设置,使用 graphInstance.setOptions(...) 在运行时修改滚轮和画布拖动模式,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 支持图片导出。因此,这个示例更像一个混合工具:用户可以在会话期间重新摆放现有节点,但并不提供 CRUD 编辑或坐标持久化。

包装层上的 onMouseMove 处理函数还会使用 graphInstance.isLine(...)getLinkByLine(...) 检测鼠标悬停的连线,但它只是把结果写到控制台。这是实现细节,不是面向用户的主要学习点。

关键交互

最重要的交互是限定在手柄区域内的节点拖动。用户不能从整个节点主体拖动节点,必须从琥珀色头部条带或琥珀色移动徽标开始拖动,具体取决于该节点的元数据。

悬浮辅助窗口可以通过标题栏拖动,并且在图谱需要更多空间时可以最小化。它的设置按钮会在同一个窗口上方打开一个覆盖面板。

在这个设置面板中,用户可以把鼠标滚轮行为切换为滚动、缩放或无动作。他们也可以在不重建图谱的情况下,把画布拖动模式切换为框选、移动或无动作。

同一个面板里还包含一个下载操作,用于把当前图谱画布导出为图片。虽然这是共享脚手架的一部分,但它仍然会影响这个演示在运行时的实际体验。

关键代码片段

这段配置代码表明,该示例使用固定布局,并且在重新添加局部拖拽手柄之前,先禁用了整节点拖动。

const graphOptions: RGOptions = {
    debug: true,
    defaultJunctionPoint: RGJunctionPoint.border,
    defaultNodeColor: '#ffffff',
    defaultNodeBorderWidth: 1,
    defaultNodeBorderColor: '#f39930',
    defaultLineColor: '#f39930',
    defaultLineWidth: 2,
    toolBarPositionH: 'right',
    disableDragNode: true,
    layout: {
        layoutName: 'fixed'
    }
};

这段数据片段展示了,每个节点获得哪种拖拽交互提示,是由节点级元数据控制的。

const myJsonData: RGJsonData = {
    rootId: 'SYS_ROLE',
    nodes: [
        { id: 'SYS_USER', text: 'SYS_USER', nodeShape: RGNodeShape.rect, x: -32, y: -427, data: { hasHeader: true } },
        { id: 'SYS_DEPT', text: 'SYS_DEPT', nodeShape: RGNodeShape.circle, x: -244, y: -283, data: { hasMoveButton: true } },
        { id: 'SYS_ROLE', text: 'SYS_ROLE', nodeShape: RGNodeShape.rect, x: 0, y: 0, width: 300, height: 200, data: { hasHeader: true } },
        { id: 'SYS_USER_ROLE', text: 'SYS_USER_ROLE', nodeShape: RGNodeShape.rect, x: 405, y: -174, data: { hasMoveButton: true } },
        { id: 'SYS_RESOURCE', text: 'SYS_RESOURCE', nodeShape: RGNodeShape.rect, x: -246, y: -80, data: { hasHeader: true } },
        { id: 'SYS_ROLE_RESOURCE', text: 'SYS_ROLE_RESOURCE', nodeShape: RGNodeShape.rect, x: 338, y: -100 }
    ],
    // ...
};

这个初始化函数表明,图谱是直接从内联 JSON 加载的,然后再让视口适配到合适范围。

const initializeGraph = async () => {
    const myJsonData: RGJsonData = {
        // ...
    };
    await graphInstance.setJsonData(myJsonData);
    graphInstance.zoomToFit();
};

这段插槽代码是核心实现:rg-node-drag-handler 类只会附加到每个自定义节点内部允许拖动的区域上。

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div className="h-full">
            {node.data.hasHeader && <div className="rg-node-drag-handler px-3" style={{ backgroundColor: '#f39930', color: 'white'}}>
                {node.text}
            </div>}
            {node.data.hasMoveButton && <div className="rg-node-drag-handler cursor-move absolute top-0 left-0 h-5 w-5 rounded bg-gray-200 flex place-items-center justify-center" style={{ backgroundColor: '#f39930', color: 'white'}}>
                <MoveIcon />
            </div>}
        </div>
    )}
</RGSlotOnNode>

这段共享设置代码说明,滚轮行为和画布拖动行为是在运行中的图实例上动态修改的。

<SettingRow
    label="Wheel Event:"
    options={[
        { label: 'Scroll', value: 'scroll' },
        { label: 'Zoom', value: 'zoom' },
        { label: 'None', value: 'none' },
    ]}
    value={wheelMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>

这段 SCSS 代码说明,这种编辑器风格的背景并不是图片资源,而是由 relation-graph 画布变量生成出来的。

.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);
}

这个示例的独特之处

对比数据表明,这个示例并不是因为使用了 RGSlotOnNode、固定布局或共享悬浮辅助窗口而显得独特。像 area-setcss-themecanvas-bg2node-style3 这些相邻示例,也分别覆盖了其中一部分内容。

真正突出的地方在于它的交互约定。这个演示先在全局关闭默认的节点拖动,再只在通过插槽渲染且带有 rg-node-drag-handler 标记的子区域里恢复移动能力。因此,局部节点拖动才是这个示例的主要教学点,而不是附带的小修饰。

它还把这个思路推进到了不止一种手柄样式。拖拽交互提示是通过 node.data 按节点选择的,因此同一个图谱里可以同时混用完整头部手柄、紧凑型移动按钮手柄,以及完全不暴露拖拽手柄的节点。

area-set 相比,关键差异不在于画布分区,而在于节点主体内部的移动范围。与 css-themecanvas-bg2 相比,可复用的价值不在于主题切换或包装层样式。与 node-style3 相比,这里的插槽更多是用来控制行为,而不是控制外观。正因如此,当某个图谱需要有选择地重新摆放节点、又不希望整个节点表面都变成拖拽目标时,这个示例会是更好的起点。

这一模式还适用于哪里

这种模式非常适合工作流面板或运维看板:卡片内部可能包含按钮、标签或嵌入式控件,这些内容需要保持可点击,而一个小而明确的允许区域仍然可以支持重新定位。相同思路也适用于拓扑图、架构图和访问控制视图,在这些场景里,某些节点应当允许调整位置,但它们的主体内容不应意外触发拖动。

它也适用于那些需要临时手工整理、但又不提供完整编辑能力的审阅工具。团队可以把固定布局数据作为基线,只在选定的节点类型上暴露拖拽手柄,并避免“节点内部每个像素都能被拖动”所带来的可用性问题。