JavaScript is required

工业链卡片锚点混合布局

这是一个偏查看器的 relation-graph 示例,用于工业价值链看板:外层图使用固定布局,内部采用自定义混合布局管线。普通画布插槽卡片行通过 `RGConnectTarget` 与 `addFakeLines(...)` 转为图锚点,同时按组树布局器和依赖引用可在展开、收起及按层批量展开时保持附属子树同步。

用于工业价值链仪表板的卡片锚定混合布局

这个示例构建了什么

这个示例构建了一个面向查看场景的工业价值链仪表板:三张大型阶段卡片位于图谱画布内部,每一行卡片都锚定一棵独立的 relation-graph 树。用户会在背景中看到上游、中游和下游三个面板,看到将这些行连接到隐藏图根节点的正交桥接连线,以及围绕这些卡片分布的多个树形簇,而不是单一的一棵层级树。

用户可以展开或折叠分支,可以通过浮动辅助窗口将所有分组展开到指定深度,可以点击空白画布清除选中和编辑状态,可以拖动或最小化辅助窗口,还可以打开一个次级设置浮层来调整滚轮模式、拖拽模式和图片导出。本示例的重点是这种组合方式:RGSlotOnCanvas 中的普通 HTML 会成为可见的锚定层,而真实的图节点则保留在其后方,并由一个自定义的混合布局控制器重新定位。

数据是如何组织的

源数据来自 getMyAllColumnsData(),它会返回三个列对象:c1c2c3。每一列都包含阶段名称、副标题、图标名称、主题颜色,以及一个由嵌套 JsonNode 树组成的 items 数组。在当前示例提供的数据中,这些树分别描述了上游材料与研发、中游制造,以及下游应用与服务。阶段外壳是静态的,但真正重要的是这套可复用结构:列级元数据用于描述画布中的卡片,而嵌套节点树用于描述将挂接到每个卡片行上的图内容。

在进行任何布局之前,MyMixLayout.loadData(...) 会先把这三列转换成多个分组。analysisDataForColumn3(...) 会将每一列拆分成以组为单位的根节点,添加 refs 以便某些分组依赖于更早或更晚分组的边界,并按列选择不同的布局规则。随后,flatNodeData(...) 会把每棵树拍平成 nodeslines,补充 hasChildrendeep 元数据;buildMyGroup(...) 则会给每个节点打上 myGroupId,移除 children,并把真实根节点收缩成一个微小的隐藏锚点节点。在生产系统中,同样的模式可以表示供应链阶段、服务旅程阶段、产品生命周期分段、平台能力列,或任何需要让每张卡片行挂接一棵说明树的分阶段仪表板。

relation-graph 是如何使用的

index.tsx 使用 RGProvider 包裹整个示例,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 以命令式方式驱动图谱。外层图被配置为 layoutName: 'fixed',因此 relation-graph 不会对整张画布执行一次性的自动布局。取而代之的是,MyMixLayout 会通过 createLayout(...) 为每个分组创建独立的树布局器,用 fixedRootNode = true 将根节点锁定在原位,并结合 RGConnectTarget 坐标、列专属偏移量、依赖 refs 以及画布卡片容器的实际渲染高度来定位每个分组。

图谱选项使这种组合在视觉上保持一致:矩形节点、正交连线、圆角折线拐点、无默认节点边框,以及左右方向的连接点行为。initializeGraph() 会启用 XY 动画、加载列数据、推导出所有分组、等待 canvas slot 的 DOM 完成渲染、通过 addFakeLines(...) 将卡片行连接到隐藏根节点、应用全部分组布局,然后再通过 zoomToFit() 适配视口。

在这个示例中,slot 的重要性高于默认节点渲染。RGSlotOnCanvas 负责渲染整个工业仪表板,每一行都暴露一个 RGConnectTarget 端点,使非节点 DOM 也能参与图连接。RGSlotOnNode 将可见图节点简化为居中的文本标签,而真正的分组根节点则被保持为微小且空白的状态。这个示例没有实现创作型工作流,但它复用了共享的查看器工具 DraggableWindowCanvasSettingsPanel 通过 RGHooks.useGraphStore() 读取实时选项,使用 setOptions(...) 更新滚轮和拖拽行为,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 导出图像。本地 SCSS 文件主要只包含少量外层包装样式覆盖,因此大多数行为与视觉表现都来自图谱选项、slot 内容以及各分组样式,而不是大量的 CSS 定制。

关键交互

  • 点击内建的展开控制点会展开或折叠单个分支,然后 relayoutIfNeed(...) 会重新执行该分组的布局,并递归重新布局 refs 中列出的依赖分组。
  • 点击 SimpleUISelect 中任意一个深度按钮,会依据 node.data.deep 展开或折叠所有分组,刷新节点可见性,重新执行所有分组布局,并重新适配视口。
  • 点击空白画布会清除选中状态,并清空编辑中节点列表,从而在不改变底层数据集的前提下重置查看器。
  • 拖动辅助窗口会重新定位这个浮动控制面板,最小化后会隐藏说明面板,但图谱仍保持可见。
  • 打开设置浮层后,可以切换滚轮行为、切换画布拖拽行为,并可通过共享截图辅助功能将当前图谱导出为图片。

关键代码片段

下面这段代码表明,外层图保持固定布局,并统一了这种混合组合所使用的节点和连线默认配置:

const graphOptions: RGOptions = {
    debug: false,
    layout: { layoutName: 'fixed' },
    defaultPolyLineRadius: 10,
    defaultNodeBorderWidth: 0,
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.SimpleOrthogonal,
    defaultJunctionPoint: RGJunctionPoint.lr
};

下面这段代码展示了普通仪表板行如何在 canvas slot 中变成图连接端点:

<RGConnectTarget
    targetId={'col-item-ct-' + item.id}
    junctionPoint={RGJunctionPoint.lr}
    disableDrag={true}
    disableDrop={true}
>
    <div className={`w-1.5 h-1.5 rounded-full ${themeColor.dot}`} />
</RGConnectTarget>

下面这段代码展示了这些 HTML 端点如何通过伪造的正交连线桥接到隐藏根节点:

const fakeLine: JsonLine = {
    id: 'ct-line-' + group.rootNodeId,
    fromType: RGInnerConnectTargetType.CanvasPoint,
    from: connectTargetId,
    toType: RGInnerConnectTargetType.Node,
    to: group.rootNodeId,
    lineShape: RGLineShape.SimpleOrthogonal,
    color: group.groupStyles.lineStyles.color || 'rgba(0,0,0,0.1)',
    showEndArrow: false
};

下面这段代码展示了分组布局如何从 connect-target 坐标出发,再创建一个固定根节点的专用树布局器:

const connectTarget: RGConnectTargetData = this.graphInstance.getConnectTargetById(connectTargetId);
const groupRootNodeXy = {
    x: connectTarget.offsetX,
    y: connectTarget.offsetY
};
// ... column-specific offsets and dependency adjustments
const myGroupLayout = this.graphInstance.createLayout(layoutOptions as RGLayoutOptions);
myGroupLayout.isMainLayouer = false;
myGroupLayout.layoutOptions.fixedRootNode = true;
myGroupLayout.placeNodes(groupNodes, groupRootNode);

下面这段代码展示了预处理步骤如何给每个拍平后的节点打上可复用元数据,用于控制展开与分组:

const nodeJson = {
    id: thisOrignNode.id,
    text: thisOrignNode.text,
    data: {
        ...orignData,
        hasChildren,
        deep
    }
};

下面这段代码解释了为什么在一棵树中展开或折叠,也可能导致其他树重新定位:

relayoutAffectedGroups(group: MyGroup) {
    const affectedGroupsList = this.allGroups.filter(g => g.refs.includes(group.groupId));
    for (const affectedGroup of affectedGroupsList) {
        this.layoutGroup(affectedGroup);
        this.relayoutAffectedGroups(affectedGroup);
    }
}

这个示例的独特之处

对比数据表明,mix-layout-tree 是与之最接近的分组布局示例,但它传达的经验点并不相同。mix-layout-tree 更适合作为“围绕一个可见共享根节点拆分单棵层级树”的简洁参考。而 mix-layout-2 更进一步,它把 canvas slot 中的卡片行变成图锚点,隐藏真实的分组根节点,并通过显式的跨组依赖传播重新布局。因此,当产品需要让仪表板内容与图结构保持同步时,这个示例会更有参考价值。

mix-layout-editor 相比,这个示例仍属于同一类卡片锚定混合布局家族,但重点从创作转向展示。它移除了编辑浮层、插入与删除工作流,以及 heavily 依赖 minimap 的工具,从而让可复用的核心思路更容易被抽离出来:一种面向查看端的组合方式,具备批量深度控制与自动的依赖式重新布局能力。

对比文件还明确了一个重要边界。DOM 锚点连线并不是这个示例独有的,因为 inventory-structure-diagram 以及其他一些 demo 也会同时使用 RGConnectTarget 和 fake-line 连接。这里真正突出的,是这组特性的组合方式:工业仪表板背景、隐藏根节点的分阶段树簇、按列定义的生长方向、无箭头的正交桥接连线,以及当某个分组尺寸变化时触发的递归重新布局。这种组合在整个示例集中相对少见,因此它非常适合作为“同步仪表板 + 树结构视图”场景的起点,而不只是一个普通的多树查看器。

这个模式还适用于哪里

这种模式很适合迁移到供应链总览、产品生命周期仪表板、服务旅程面板、平台能力地图,以及合规或风险界面等场景中,在这些场景里,每一张可见阶段卡片都需要挂接一棵说明树。同样的方法也适用于运维类仪表板,在这类场景中,HTML 摘要应该始终是主视觉层,而图谱则作为其后方的结构化说明层存在。

它也非常适合作为分阶段流程工具的扩展点,尤其是那些既需要按需展开信息、又不能丢失空间上下文的工具。团队可以复用分组预处理、connect-target 锚定以及基于依赖的重新布局逻辑,用于 onboarding 流程、制造流程图、采购链路、合作伙伴生态,或任何一种多列叙事界面,只要其中某个展开的子树需要推动其相关簇向上或向下移动。