班级成员与兴趣小组关系图
这个示例构建的是一个学校活动关系看板,而不是传统的节点连线图。半透明的学校地图、可点击的位置图钉、左侧的兴趣小组列表以及右侧的班级成员列表,都渲染在同一个 relation-graph canvas 中。
使用 Canvas-Slot DOM 锚点进行双向兴趣小组探索
这个示例构建了什么
这个示例构建的是一个学校活动关系看板,而不是传统的节点连线图。半透明的学校地图、可点击的位置图钉、左侧的兴趣小组列表以及右侧的班级成员列表,都渲染在同一个 relation-graph canvas 中。
用户可以点击小组卡片或其地图图钉,以高亮匹配的成员,并绘制一条更粗、带动画效果的连线回到小组位置。他们也可以点击成员卡片,重新绘制从该成员到其加入的每个小组的连接层。这里的核心点在于,relation-graph 扮演的是自定义 HTML 界面的连接引擎,而不是可见节点的渲染器。
数据如何组织
数据位于两个内联数组中,并由 loadDataFromRemote() 加载到 React state。interestGroups 保存 groupId、显示名称以及地图图钉的 { x, y } 坐标,而 classMembers 保存成员名称以及由多个 group id 组成的 joinedGroups 数组。
这个示例中并没有真实的远程获取过程,也没有单独的数据规范化步骤。真正重要的预处理发生在 UI 状态被转换为 fake-line 定义时:activeGroupId 和 activeMemberName 决定了 updateMyLines() 是构建成员到小组的连线加上一条小组到位置的连线,还是构建成员到多个小组的连线。在生产系统中,同样的数据形态可以表示学生与社团、员工与团队、志愿者与活动,或任何其他带有可选空间锚点的双侧成员关系表。
relation-graph 的使用方式
index.tsx 使用 RGProvider 包裹整个示例,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 获取实时 graph 实例。图本身运行时配置为 debug: false、defaultJunctionPoint: RGJunctionPoint.lr,以及 layout.layoutName = 'fixed',这非常适合由绝对定位 HTML 组成、而不是由自动布局图节点构成的场景。
实际界面通过 RGSlotOnCanvas 渲染。在这个 slot 内,示例放置了背景地图、地图图钉、小组列表和成员列表。每个可交互的 HTML 界面元素都包裹在 RGConnectTarget 中,并分配了一个稳定的目标 id,分别属于三类命名空间之一:location-*、group-* 或 member-*。当活动选择发生变化时,这些 id 会被 addFakeLines() 复用。
graph 实例 API 在这里被用作渲染生命周期工具。启动时,示例会加载内联数据,预先选中 music 小组,然后调用 moveToCenter() 和 zoomToFit()。每次选择变化时,它都会先通过 sleep(100) 等待,清除之前的图内容,添加一个不可见的 fake-root 节点,再重新绘制 fake lines。共享的浮动窗口使用 RGHooks.useGraphStore(),并配合 setOptions()、prepareForImageGeneration()、getOptions() 和 restoreAfterImageGeneration() 在运行时切换滚轮或拖拽行为,并将当前 canvas 导出为图片。视觉定制来自 my-relation-graph.scss,它为小组提供紫色卡片、为成员提供蓝色卡片,并为地图图钉添加脉冲光晕效果。
关键交互
- 点击小组卡片或地图图钉会选中该小组,清除当前活动成员,通过重新生成的蓝色曲线高亮匹配成员,并添加一条从小组卡片到地图位置的带动画紫色曲线。
- 点击成员卡片会选中该成员,清除当前活动小组,并重新绘制从该成员到其加入的每个小组的紫色曲线。
- 浮动说明窗口可以拖动、最小化,并切换到设置覆盖层。
- 设置覆盖层可以将滚轮行为切换为滚动、缩放或无操作,将 canvas 拖拽行为切换为选择、移动或无操作,并将当前图导出为图片。
关键代码片段
这个片段展示了固定布局的 graph 配置,它让 canvas 可以承载一个使用绝对定位的 HTML 场景。
const graphOptions: RGOptions = {
debug: false,
defaultJunctionPoint: RGJunctionPoint.lr,
layout: {
layoutName: 'fixed'
}
};
这个片段展示了如何把当前选中的实体转换为 fake-line 定义,而不是在 graph JSON 中存储静态边。
if (activeGroupId) {
classMembers.forEach((member, index) => {
if (member.joinedGroups.includes(activeGroupId)) {
myFakeLines.push({
id: `line-member-${index}`,
from: 'member-' + member.name,
to: 'group-' + activeGroupId,
color: 'rgba(29,169,245,0.76)',
lineShape: RGLineShape.StandardCurve
});
}
});
}
这个片段展示了重绘周期:等待 slot DOM 出现,清除之前的连接器,添加一个占位节点,并注入新的 fake lines。
await graphInstance.sleep(100);
// ...
graphInstance.clearGraph();
graphInstance.addNodes([{ id: 'fake-root', text: '', opacity: 0, x: 0, y: 0 }]);
graphInstance.addFakeLines(myFakeLines);
这个片段展示了 RGSlotOnCanvas 中的普通 HTML 如何通过 RGConnectTarget 成为 graph 端点。
<RGConnectTarget
key={group.groupId}
targetId={`group-${group.groupId}`}
junctionPoint={RGJunctionPoint.lr}
disableDrag={true}
disableDrop={true}
>
<div
className={`w-64 pointer-events-auto c-i-group cursor-point ${activeGroupId === group.groupId ? 'c-i-group-checked' : ''}`}
onClick={() => onGroupClick(group.groupId)}
>
这个片段展示了运行时 canvas 控制和图片导出来自共享辅助代码,而不是主示例组件本身。
<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 }); }}
/>
这个示例的独特之处
对比数据将 inventory-structure-diagram、element-connect-to-node、element-lines 和 scene-network-use-canvas-slot 识别为它最接近的邻居。这里真正独特的一步是双向的多对多浏览:选中一个小组时,会从匹配成员一侧汇聚连线,并额外添加一条小组到位置的连线;选中一个成员时,则会向多个小组发散连线。这比单向详情面板或一对一端点挂接示例更进一步。
这个示例还在同一个组合场景中接入了三类端点:location-*、group-* 和 member-*。与 element-lines 和 element-connect-to-node 相比,重点从证明 DOM 端点挂接转移为使用一个选择模型协同三个可见界面。与 scene-network-use-canvas-slot 相比,这里的连接层不是在挂载时准备好的静态看板,而是根据当前选择状态反复重建。浮动工具窗口是整体组合的一部分,但它属于共享基础设施,而不是这个示例独有的特性。
这种模式还适用于哪里
这种模式非常适合那些已经拥有自定义 HTML 卡片或地图标记、只需要 relation-graph 提供连接层的界面。比如员工与项目看板、志愿者与活动匹配界面、仓库与产品成员关系视图、客户与细分群体浏览器,或需要列表到位置高亮联动的校园与楼层地图。
对于那些希望通过一次选择同步多个面板、又不想把整个界面转换成标准图节点的仪表盘,这也是一个很强的起点。可复用的核心思路,是固定 canvas 组合、稳定的 RGConnectTarget id,以及由选择驱动的 fake-line 重新生成。