JavaScript is required

连接列表项与地点节点

这个示例在校园地图之上构建了一个位置分配视图。画布左侧显示固定位置的地图图钉,右侧显示六张兴趣小组卡片。系统始终使用一条带动画的连接线来高亮当前选中的列表项与其匹配地图位置之间的关系。

将列表卡片连接到地图上的固定位置节点

这个示例构建了什么

这个示例在校园地图之上构建了一个位置分配视图。画布左侧显示固定位置的地图图钉,右侧显示六张兴趣小组卡片。系统始终使用一条带动画的连接线来高亮当前选中的列表项与其匹配地图位置之间的关系。

用户可以通过点击卡片或图钉来切换当前激活的关系。场景中还包含一个悬浮辅助窗口,可以拖动、最小化、展开为设置面板,并用于将当前画布导出为图片。

这个示例的重点并不是通用节点编辑,而是一种聚焦的 DOM 到节点连接模式:列表一侧是由 RGConnectTarget 包装的普通 DOM,目标一侧则作为真实节点保留在 relation-graph 模型内部。

数据如何组织

数据起始于 MyGraph.tsx 中的一个小型内联数组。每条记录都有一个 groupId、一个显示名称,以及一个包含明确 xy 坐标的 location 对象。

在渲染任何内容之前,这个数组会被用在两个地方:

  • 它会被复制到 React state 中作为 interestGroups,用于驱动右侧卡片列表。
  • 它会被投影成图节点,节点 id 形如 location-a,并带上 data.myGroupId,然后通过 addNodes(...) 插入图中。

这个示例中没有 setJsonData(...) 调用。图内容是在挂载后以命令式方式组装的,同一份源记录同时用于 DOM 端点和地图侧节点。在真实系统中,这种数据形态同样可以表示校园地图上的部门、活动场馆中的展位、平面图上的设备,或设施中的服务点。

relation-graph 的使用方式

图以固定布局模式运行,因此每个投影出来的节点都会停留在源记录提供的精确坐标上。初始选项还将 defaultJunctionPoint 设为 border,将鼠标滚轮设为缩放模式,并将画布拖拽设为移动模式。

RGProvider 提供图上下文,而 RGHooks.useGraphInstance() 是主要控制入口。组件通过这个实例来添加节点、让视口居中、适配缩放、清除上一条 fake line,并添加新的当前激活 fake line。共享的辅助窗口也会使用图实例来修改运行时选项并执行图片导出流程。

两个插槽定义了可见场景:

  • RGSlotOnNode 用可点击的 MapPin 标记替换默认节点渲染。
  • RGSlotOnCanvas 在同一个画布层中渲染校园地图和 RGConnectTarget 卡片列表。

RGConnectTarget 是列表侧的关键集成点。每张卡片都会获得一个稳定的端点 id,例如 group-a,而当前选中的 group 会通过一条 fake line 连接到与之匹配的图节点,并且该线的目标类型被显式设置为 RGInnerConnectTargetType.Node

悬浮辅助窗口并不是这个示例专属的图逻辑,但它对完整行为很重要。它通过 RGHooks.useGraphStore() 读取当前图选项,用 setOptions(...) 更新 wheelEventActiondragEventAction,并在 prepareForImageGeneration()restoreAfterImageGeneration() 之间导出图像。

关键交互

  • 这个示例会在挂载后不久自动选中 group a,因此第一条连接线无需手动操作就会出现。
  • 点击列表卡片或地图图钉都会进入同一个 onGroupClick(...) 处理函数,更新当前激活的 group id,并只重绘一条 fake line。
  • 辅助文本和节点设置表明,地图侧图钉在运行中的图里是设计为可拖动的。代码不会把变更后的坐标回写到 React state,因此任何重新定位都只是当前画布运行期间的临时状态。
  • 悬浮辅助窗口支持拖动、最小化、打开设置覆盖层、切换滚轮和拖拽行为,以及下载截图。

关键代码片段

这个片段展示了同一组内联记录如何同时变成 UI state 和固定位置的图节点。

const myGroups = [
    { groupId: 'a', groupName: 'Sports Group', location: { x: 260, y: 300 } },
    { groupId: 'b', groupName: 'Music Group', location: { x: 350, y: 100 } },
    // ...
];
setInterestGroups(myGroups);
graphInstance.addNodes(myGroups.map(n => ({
    id: 'location-' + n.groupId,
    x: n.location.x,
    y: n.location.y,
    data: { myGroupId: n.groupId }
})));

这个片段证明,选择操作不会重建整个场景,而只是替换当前激活的一条 fake line。

const myFakeLines: JsonLine[] = [{
    id: `fl-${groupId}`,
    from: 'group-' + groupId,
    to: 'location-' + groupId,
    toType: RGInnerConnectTargetType.Node,
    color: 'rgba(159,23,227,0.65)',
    lineShape: RGLineShape.StandardCurve,
    fromJunctionPoint: RGJunctionPoint.lr,
    toJunctionPoint: RGJunctionPoint.border,
    animation: 2
}];
graphInstance.clearFakeLines();
graphInstance.addFakeLines(myFakeLines);

这个片段展示了目标一侧仍然是真实图节点,即使它被渲染成图钉样式的标记。

<RGSlotOnNode>
    {({ node, checked }: RGNodeSlotProps) => {
        return (
            <div
                className={`pointer-events-auto cursor-point c-i-location ${checked ? 'c-i-location-active' : ''}`}
                onClick={() => onGroupClick(node.data.myGroupId)}
            >
                <MapPin className='transform translate-y-[-25px] translate-x-[-5px]' size={24} />
            </div>
        );
    }}
</RGSlotOnNode>

这个片段展示了列表一侧如何被转换为可连接的 DOM 端点。

<RGConnectTarget
    key={group.groupId}
    targetId={`group-${group.groupId}`}
    junctionPoint={RGJunctionPoint.lr}
    disableDrag={true}
    disableDrop={true}
>
    <div
        className={`w-full pointer-events-auto c-i-group cursor-point ${activeGroupId === group.groupId ? 'c-i-group-checked' : ''}`}
        onClick={() => onGroupClick(group.groupId)}
    >

这个片段展示了共享辅助面板如何改变运行时行为,并将图画布捕获为图片。

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
if (imageBlob) {
    downloadBlob(imageBlob, 'my-image-name');
}
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

对比数据将这个示例定位在纯元素连线示例和基于地图的节点示例之间。它最明显的区别在于,列表一侧保持为普通 DOM,而目标一侧则作为真实的固定位置节点保留在 relation-graph 内部。因此,它是一个聚焦于 DOM 到节点连接的参考示例,而不是 DOM 到 DOM 连线或通用图编辑器。

element-lines 相比,这个示例在目标端需要保留图身份、元数据和内建节点行为时更有价值。与 element-connect-to-node 相比,它更聚焦,也更容易复用,因为它没有把多种策略并排比较。它只保留一张地图、一个列表和一条当前激活的连接线,因此示例本身更专注于“选择并高亮”的工作流。

对比结果还强调了这一特性组合的独特强度:固定布局地图节点、自定义节点渲染、在画布中组合的列表 UI、由图驱动且可拖动的图钉,以及通过 clearFakeLines()addFakeLines() 在每次选择时替换单条动画连接线。这使它成为构建紧凑型分配面板的一个实用起点,在这类界面中,当前关系通常需要保持最突出的视觉表现。

这一模式还适用于哪里

这一模式可以迁移到任何一侧关系属于图模型、另一侧关系属于普通页面 UI 的界面中。

  • 将团队、设备或任务分配到楼层平面图或校园地图中的固定位置。
  • 将侧边栏中的仓储物品或站点连接到存储布局中的空间锚点。
  • 将活动展位、服务台或应急点连接到场馆地图上的位置。
  • 构建审核工具,让操作员从列表中选中一条记录后立即看到它映射到的目标位置。

可复用的核心思路是职责拆分:将有空间意义的锚点保留为图节点,将周边工作流 UI 保留为 DOM,并且只重绘当前相关的那条连接线。