JavaScript is required

组织机构树 Folder 布局

这个示例使用 relation-graph 的 folder 布局和自定义企业卡片节点渲染一棵左到右组织层级树。它先把嵌套树数据预处理为 RGJsonData,再允许用户在运行时调节连接器偏移与 folder 间距,同时在共享悬浮工具窗中保留画布设置与图片导出。

文件夹布局中的组织树

这个示例构建了什么

这个示例使用 relation-graph 的 folder 布局构建了一个从左到右的组织层级查看器。节点不再是纯文本行,而是被渲染为公司卡片,包含彩色侧边栏、标签 chips,以及一行截断显示的位置文本。

用户可以在图运行时调整展示效果。浮动辅助窗口提供了一个连接线起点偏移滑块、两个用于控制文件夹水平和垂直间距的滑块,以及一个用于设置画布滚轮模式、拖拽模式和导出图片的设置面板。最关键的一点是,这个示例在不改动 relation-graph 底层布局引擎的前提下,把文件夹风格的层级布局转换成了面向业务的组织树。

数据是如何组织的

源数据在 data.ts 中以嵌套字面量的形式开始。每一项都包含 idtextdata 载荷,以及可选的 children,其中 data 对象承载了之后由节点插槽渲染的卡片元数据。

在图加载之前,示例会显式地将这棵树拍平成包含 rootIdnodeslinesRGJsonData。这个预处理发生在 flattenTreeData(...) 中,因此该示例并不依赖 setJsonData(...) 自动发现并拍平 children。在初始化过程中,代码还会先为每条线设置显式的连接点参数和初始的 fromJunctionPointOffsetX 值,然后再调用 setJsonData(...)

在真实产品中,同样的结构可以表示部门、子公司、加盟分支、区域团队,或任何其他层级结构,只要每个节点既需要标签,也需要紧凑的业务元数据。

relation-graph 是如何使用的

这个示例挂载在 RGProvider 中,然后渲染一个单独的 RelationGraph 实例。它的核心图配置将布局固定为 folder,并设置 from: 'left',同时把 treeNodeGapHtreeNodeGapV 绑定到 React state 上,从而支持在运行时调整间距。

图的样式是有明确倾向性的。节点使用 RGNodeShape.rect,并设置 defaultNodeBorderWidth: 0;连线使用 RGLineShape.StandardOrthogonal4 像素折线圆角半径,以及蓝色默认颜色。defaultExpandHolderPosition: 'right'reLayoutWhenExpandedOrCollapsed: true 让分支切换控件始终与 folder 布局从左到右的阅读方向保持一致。

这个示例依赖 RGHooks.useGraphInstance(),而不是 ref。这个 graph 实例驱动了整个生命周期:挂载时调用 setJsonData(...),间距变化时调用 updateOptions(...)doLayout(),连接线偏移滑块变化时调用 getLines()updateLine(...),以及在布局完成后调用 moveToCenter()zoomToFit()

节点渲染通过 RGSlotOnNode 被替换掉,使每个图节点都变成自定义卡片。my-relation-graph.scss 中的 SCSS 则进一步完成了展示层样式,包括卡片外壳、展开控件以及已选中连线状态。浮动的 DraggableWindow 是一个共享辅助组件,但在这个示例中它依然很关键,因为它提供了可移动的控制面板、画布设置面板,以及截图导出流程。

关键交互

主要交互是运行时展示调节。移动连接线起点偏移滑块时,会在不重建数据集的情况下,更新所有已加载连线的 fromJunctionPointOffsetX

水平和垂直距离滑块会更新 folder 布局选项,触发 doLayout(),然后重新居中并重新适配图视图,从而让间距变化后仍然易于阅读。

浮动辅助窗口可以拖拽、最小化,并切换到设置覆盖层。在这个覆盖层中,用户可以修改滚轮行为、修改画布拖拽行为,并把当前图视图导出为图片。

分支展开和折叠也是体验的一部分,因为图被配置为在分支可见性变化时重新布局。节点点击和连线点击处理器只是在控制台输出对象,因此它们并不是这个示例行为的核心。

关键代码片段

这段代码展示了示例从嵌套层级数据开始,并显式将其拍平成 RGJsonData

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [],
    lines: []
};

flattenTreeData(rootNodeJson, null, myJsonData.nodes, myJsonData.lines);
return myJsonData;

这个递归辅助函数证明了,在图加载之前,父子关系就已经被转换成扁平的节点数组和连线数组。

treeNodes.forEach((nodeData) => {
    const node: JsonNode = {
        id: nodeData.id,
        text: nodeData.text,
        data: nodeData.data
    };
    nodes.push(node);

    if (parent) {
        const line: JsonLine = {
            from: parent.id,
            to: nodeData.id
        };
        lines.push(line);
    }

这段选项配置确定了 folder 布局的方向,以及那些让该层级读起来更像组织树而不是普通文件树的视觉默认值。

const graphOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'folder',
        from: 'left',
        treeNodeGapH: rangeHorizontal,
        treeNodeGapV: rangeVertical
    },
    defaultExpandHolderPosition: 'right',
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.StandardOrthogonal,

这一步初始化会在数据交给 relation-graph 之前,为每条已加载的连线统一设置自底向左的路由方式。

myJsonData.lines.forEach((line, index) => {
    if (!line.id) {
        line.id = `l${index + 1}`;
    }
    line.fromJunctionPoint = RGJunctionPoint.bottom;
    line.fromJunctionPointOffsetX = fromOffsetX;
    line.toJunctionPoint = RGJunctionPoint.left;
});
await graphInstance.setJsonData(myJsonData);

这一对更新函数展示了两条运行时调节路径:一条是针对间距变化重新布局,另一条是不改布局、直接原地修改连线偏移。

graphInstance.updateOptions({
    layout: {
        layoutName: 'folder',
        from: 'left',
        treeNodeGapH: rangeHorizontal,
        treeNodeGapV: rangeVertical
    }
});
await graphInstance.doLayout();
graphInstance.moveToCenter();
graphInstance.zoomToFit();
graphInstance.getLines().forEach(line => {
    graphInstance.updateLine(line, {
        fromJunctionPointOffsetX: fromOffsetX
    });
});

这个插槽片段展示了 relation-graph 节点如何被替换为卡片式的业务内容。

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div className="my-card-node">
            <div className="card-left" />
            <div className="card-right">
                <div className="card-name">{node.text}</div>
                <div className="card-tags">
                    {node.data?.tags.map((tag: string) => (
                        <div key={tag} className="card-tag">

这个共享辅助组件片段提供了示例中的画布设置和图片导出控制。

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

这个示例的独特之处

根据对比数据,这个示例与 layout-folder 最接近,但它把这种模式又向前推进了两步:它把通用的文件夹式节点替换成公司卡片,并在共享的连接线偏移控制模式之上,增加了实时的水平和垂直间距控制。因此,当目标是构建业务层级查看器而不是通用文件树时,它会是一个更强的起点。

ever-changing-treeio-tree-layout 相比,这里的运行时调节范围更窄,也更贴合当前场景。这些控件不会把图变成一个通用布局实验台。相反,它们保持固定的从左到右 folder 结构,并把注意力集中在这个组织化展示真正重要的少数几个调整项上。

investment 相比,这个示例更简单,也更静态。它不会讲解懒加载分支、所有权语义或布局后的清理规则。它的独特组合在于:显式的层级拍平、自定义公司卡片、蓝色正交 folder 连接线,以及在同一个查看器中提供实时间距和连接线偏移调节。浮动工具窗口有所帮助,但比较记录也清楚表明,共享窗口本身并不是独特之处。

这种模式还适用于哪里

这种模式很适合内部组织架构图、区域办公室树、子公司地图、合作伙伴层级以及加盟体系结构,在这些场景中,每个节点都需要紧凑的元数据,而不只是一个标签。

它也适用于分类树、审批链和服务归属图,尤其是在团队需要先调整间距以提升可读性,然后再截图或将图嵌入文档时。

更一般地说,只要源数据是嵌套的业务树,而最终查看器需要显式预处理、卡片式节点渲染,以及一个轻量的运行时控制界面而不是完整编辑器,这个示例就是一个很好的参考。