JavaScript is required

力导图图标按钮节点

这个示例展示一个力导布局视图,所有节点都渲染为同一种分层图标按钮,由 `node.data.icon` 同时控制 Lucide 图标与渐变皮肤。它还复用共享悬浮工具窗提供运行时画布设置和图片导出,同时保持图谱本身只读。

力导向布局图标按钮节点

这个示例构建了什么

这个示例在一个力导向 relation-graph 之上构建了一个以样式表现为重点的关系查看器。画布铺满整个屏幕并使用芥末黄色背景,连线是半透明的白色直线,而所有可见节点都被替换成同一种分层图标按钮样式,而不是使用库默认的标签主体。

用户可以直接查看图谱,在悬停或聚焦某个节点时显示其标签,打开一个悬浮设置面板,切换滚轮和拖拽行为,并将画布导出为图片。这个示例的重点不是图编辑,而是展示如何通过一个可复用的节点插槽,把一组通用图数据集转换成一致且具有产品感的图标界面。

数据是如何组织的

图数据以内联方式声明为一个 RGJsonData 对象。它使用简单的 rootId、扁平的 nodes 数组,以及带有 fromto 引用的扁平 lines 数组。每个节点还携带 data.icon,这是本示例中最关键的元数据,因为它同时驱动渲染出的图标和对应的 CSS 修饰类。

在调用 setJsonData(...) 之前没有预处理步骤。示例会直接加载这份字面量数据集,然后让图居中并自适应显示。在真实应用里,同样的数据形态可以表示服务分类、设备分组、产品家族、功能集群,或任何需要让每种节点类型都拥有统一品牌化视觉皮肤的实体网络。

relation-graph 是如何使用的

RGProvider 包裹整个示例,而 RGHooks.useGraphInstance() 提供实时的图实例。本地的 graphOptions 会把布局切换为 force,把 maxLayoutTimes 提高到 80000,让默认节点外观变为透明,保留默认的矩形节点形状,设置背景色,并把内置工具栏水平放置在右下角。

关键的渲染钩子是 RGSlotOnNode。这个示例没有混用多个插槽分支,而是为每个节点统一使用一个插槽模板。这个模板会读取 node.data.icon,把它拼接进按钮类名,并将同一个值传给 MyNodeIcon,由其中的 Lucide 图标映射来选择具体字形。最终效果是通过一个渲染器而不是许多节点专用组件,实现由元数据驱动的主题化渲染。

外围 UI 来自共享辅助组件。DraggableWindow 提供悬浮说明外壳、最小化行为和设置覆盖层。在这个辅助组件内部,CanvasSettingsPanel 会从 store 读取图状态,通过 setOptions(...) 更新运行时选项,为图片生成准备图实例,捕获画布 DOM,然后再恢复图状态。随后通过 SCSS 覆盖样式移除内置节点边框、弱化工具栏外观,并定义分层图标按钮的视觉效果和悬停动效。

关键交互

最显眼的交互发生在节点本身:悬停或通过键盘聚焦时,后层面板会旋转,前层面板会在 3D 空间中向前推出,而图标下方的文本标签会淡入显示。正是这个交互让整张图更像一个基于按钮的查看器,而不是静态网络图。

示例还绑定了 onNodeClickonLineClick,但这些处理函数只是在控制台中记录 id。它们是用于观察的钩子,不是会改变状态的功能。悬浮辅助窗口的功能性更强:它可以被拖动、最小化、展开为设置覆盖层,可用来把滚轮行为切换为滚动、缩放或无,也可用来把画布拖拽行为切换为框选、移动或无,还可以用来下载图的图片。

关键代码片段

下面这段展示了核心插槽渲染器:一个节点模板同时使用 node.data.icon 来控制主题样式和图标选择。

const CustomNodeComponent: React.FC<RGNodeSlotProps> = ({ node }) => {
    return (
        <div>
            <button className={`icon-btn icon-btn--${node.data?.icon}`} type="button">
                <span className="icon-btn__back"></span>
                <span className="icon-btn__front">
                    <MyNodeIcon color="#ffffff" size="30px" name={node.data?.icon} />
                </span>
                <span className="icon-btn__label">{node.text}</span>
            </button>
        </div>
    );
};

下面这段展示了在任何自定义插槽渲染发生之前,如何在库层面配置整张图的视觉外观。

const graphOptions: RGOptions = {
    debug: true,
    defaultLineColor: 'rgba(255, 255, 255, 0.6)',
    defaultNodeColor: 'transparent',
    defaultNodeShape: RGNodeShape.rect,
    backgroundColor: '#d2a904',
    toolBarDirection: 'h',
    toolBarPositionH: 'right',
    toolBarPositionV: 'bottom',
    defaultLineShape: RGLineShape.StandardStraight,
    layout: {
        layoutName: 'force',
        maxLayoutTimes: 80000
    }
};

下面这段展示了该示例如何保持简单的数据模型,并将 data.icon 作为可复用的展示元数据。

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'a', data: { icon: 'football' } },
        { id: 'b', text: 'b', data: { icon: 'fries' } },
        { id: 'b1', text: 'b1', data: { icon: 'delivery_truck' } },
        { id: 'b1-1', text: 'b1-1', data: { icon: 'burger' } }
        // ...
    ],
    lines: [
        { from: 'a', to: 'b', text: '' },
        { from: 'b', to: 'b1', text: '' }
        // ...
    ]
};

下面这段展示了组件挂载时的图初始化流程。

if (graphInstance) {
    console.log('Instance ID:', graphInstance.options.instanceId);

    await graphInstance.setJsonData(myJsonData);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
}

下面这段展示了如何通过 SCSS 规则实现悬停时显示标签和空间纵深效果。

.icon-btn:focus-visible .icon-btn__back, .icon-btn:hover .icon-btn__back {
    transform: rotate(22.5deg)
}

.icon-btn:focus-visible .icon-btn__front, .icon-btn:hover .icon-btn__front {
    transform: translateZ(3em) rotateX(20deg) rotateY(20deg)
}

.icon-btn:focus-visible .icon-btn__label, .icon-btn:hover .icon-btn__label {
    opacity: 1;
    transform: translateY(20%)
}

下面这段展示了共享运行时控件,它们可以切换画布行为并触发图片导出。

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

这个示例的独特之处

根据对比数据,这个示例的突出点不在于引入了很多不同的节点模板,而在于它围绕一种可重复使用的模式保持了足够克制。与 node-style4node-style 相比,它坚持使用同一个图标按钮家族,并让 node.data.icon 控制差异,而不是分支出多个互不相关的插槽设计,或固定不变的 chip 主题。

node-style2 相比,它把相似的查看器外壳从基于内置树节点的 CSS 微调,转向了在力导向布局中通过 RGSlotOnNode 完整替换节点主体。与更宽泛的 node 示例相比,它是一个范围更窄、也更偏产品化的参考:单一的元数据驱动插槽家族、单一的芥末色展示画布、仅在悬停时显示的标签,以及按图标区分的渐变后层面板,而不是一整套样式技巧目录。

共享的悬浮辅助窗口、运行时设置和图片导出在这里当然有用,但它们并不是最能区分这个示例的主张,因为对比数据表明这些工具在附近的示例中也会出现。更稳妥的差异点,是把元数据驱动的图标映射、图标专属渐变、透明化的图谱外观,以及带有 3D 感的悬停动效,组合进同一个连贯的节点渲染器里。

这种模式还能应用到哪里

这种模式非常适合用于那些节点需要少量、但足够容易识别的视觉身份,同时又不希望整张图演变成完整编辑器的查看器。例如服务地图中,不同服务分类各自使用独立图标样式;产品导航图中,每个产品家族使用带品牌感的 chip;餐饮或零售目录中,商品分组需要紧凑的视觉标记;以及设备或能力地图中,仅在用户需要细节时通过悬停显示标签。

它也适合那些希望在数据与设计之间建立可复用渲染约定的团队。如果后端能够提供稳定的元数据键,例如 icon,前端就可以维持一套 RGSlotOnNode 实现,并随着时间推移持续扩展图标映射和 SCSS 主题库,而无需改动图本身的结构。