JavaScript is required

贸易伙伴关系探查

这个示例围绕一个焦点公司构建贸易关系查看器,根节点上方是入向类别,下方是出向类别。展开类别时会懒加载企业节点;被选中的企业标签可打开伙伴卡片,根节点控制项还可独立隐藏上/下两组分支。

支持按需分支展开的贸易伙伴关系浏览器

这个示例构建了什么

这个示例围绕一个核心公司构建了一个面向贸易的关系查看器。中心卡片位于入向类别与出向类别之间,入向类别包括 Purchased Products、Supplier 和 Country of Origin,出向类别包括 Supply Products、Buyer 和 Country of Destination。

用户可以缩放和拖动画布,展开类别节点以按需加载合作公司,从中心节点开始隐藏图的上半部分或下半部分,并通过一个锚定的合作方卡片查看已加载公司。这个示例的主要价值不在于通用树渲染,而在于把分支增长、分支过滤和节点级检查结合到一个紧凑的业务查看器中。

数据是如何组织的

初始图数据直接内联写在 initializeGraph() 中,由两个数组构成:7 个节点和 6 条连线。一个中心节点在 data.expandedUpSidedata.expandedDownSide 中保存 UI 状态,而 6 个类别节点声明了 type、固定宽度以及 expandHolderPosition,因此图一开始就是一个分为入向与出向的树形结构。

懒加载数据来自 fetchMockData(componyId),它返回一个包含 nodeslinesRGJsonData 片段。每个异步加载出来的子节点都是一个带有 c-company 类名的 company 节点,展开处理器会在插入前把父节点的 xy 位置复制到每个新子节点上,这样下一次布局计算时就能从展开分支的位置向外做动画展开。

在真实业务系统中,同样的数据结构可以由供应商列表、买家关系、国家级贸易维度、商品目录或海关记录支撑。这个示例中的 mock API 刻意保持了较小的载荷,但这种“种子数据加片段增量”的模式,已经对应了真实后端中的渐进式加载方式。

relation-graph 是如何使用的

页面挂载在 RGProvider 内部,RGHooks.useGraphInstance() 负责驱动所有图操作。图使用 tree 布局,并配置 from: 'top'treeNodeGapH: 10treeNodeGapV: 120、正交连线以及上下连接点。这个配置会生成一个以中心为轴的纵向结构,核心节点上方是入向类别,下方是出向类别。

RelationGraph 绑定了 onNodeExpand,这样类别节点就可以通过 addNodes(...)addLines(...)doLayout() 追加新的图数据片段。同一个 graph instance API 也负责首次渲染的初始化流程,包括 addNodes(...)addLines(...)doLayout()moveToCenter()zoomToFit(),随后再通过 loading('Loading Data...')sleep(400)clearLoading() 管理懒加载过程中的反馈。

主要的自定义点是 RGSlotOnNode。它替换了默认节点主体,因此中心节点可以渲染两个自定义可见性切换按钮,类别节点可以渲染成带样式的业务卡片,而懒加载插入的公司节点则可以通过 MyComponyDetail 渲染。随后,本地 SCSS 文件又把默认展开按钮重新着色为蓝色,并减小了 .c-company 节点的文字尺寸。

关键交互

  • 展开任意一个预置的类别节点,都会触发一次性的异步加载。界面会出现 loading 遮罩,生成 5 个公司节点,并在插入后重新布局图。
  • 中心节点分别带有顶部和底部切换按钮。它们不会删除数据,而是通过对 getNodeRelatedNodes(...) 的结果按负或正的 lot.level 值进行过滤,从而隐藏相关节点。
  • 鼠标滚轮用于缩放画布,拖拽用于移动画布,因此这个查看器更像一个探索式仪表板,而不是静态图示。
  • 当某个公司节点进入插槽中的 checked 状态时,PartnerCard 会显示在该节点下方,并带有默认的合作方指标和产品标签。

关键代码片段

这个片段展示了用于塑造入向/出向树形结构的布局与画布配置。

const graphOptions: RGOptions = {
  layout: {
    layoutName: 'tree',
    from: 'top',
    treeNodeGapH: 10,
    treeNodeGapV: 120,
  },
  defaultLineShape: RGLineShape.StandardOrthogonal,
  defaultJunctionPoint: RGJunctionPoint.tb,
  wheelEventAction: 'zoom',
  dragEventAction: 'move',
};

这个片段展示了该示例如何在首次布局前预置一个中心公司节点,以及分别位于上方和下方的类别节点。

const nodes = [
  {
    id: 'center', text: 'HONGKONG SLKH CASTING CO LTD',
    color: '#1a73e8', fontColor: '#ffffff', borderColor: '#1a73e8',
    width: 300, height: 50,
    data: { expandedUpSide: true, expandedDownSide: true }
  },
  { id: 'in1', text: 'Purchased Products', width: 200, expandHolderPosition: 'top', expanded: false, borderColor: '#f59e0b', color: '#ffffff', fontColor: '#334155', type: 'input' },
  { id: 'in2', text: 'Supplier', width: 200, expandHolderPosition: 'top', expanded: false, borderColor: '#3b82f6', color: '#ffffff', fontColor: '#334155', type: 'input' },
  { id: 'out1', text: 'Supply Products', width: 200, expandHolderPosition: 'bottom', expanded: false, borderColor: '#f59e0b', color: '#ffffff', fontColor: '#334155', type: 'output' },
  { id: 'out2', text: 'Buyer', width: 200, expandHolderPosition: 'bottom', expanded: false, borderColor: '#3b82f6', color: '#ffffff', fontColor: '#334155', type: 'output' },
];

这个片段展示了一次性懒展开的流程,包括 loading 反馈、位置预设、增量插入和重新布局。

const onNodeExpand = async (node: RGNode) => {
  if (!node.data.dataLoaded) {
    graphInstance.loading('Loading Data...');
    node.data.dataLoaded = true;
    const newNodeAndLines = await fetchMockData(node.id);
    newNodeAndLines.nodes.forEach((n: JsonNode) => {
      n.x = node.x;
      n.y = node.y;
    });
    graphInstance.addNodes(newNodeAndLines.nodes);
    graphInstance.addLines(newNodeAndLines.lines);
    await graphInstance.sleep(400);
    graphInstance.clearLoading();
    await graphInstance.doLayout();
  }
}

这个片段展示了中心节点如何通过更新相关节点,而不是重建整份数据集,来隐藏图的一侧。

const toggleRootUp = (node: RGNode) => {
  const newExpanded = !node.data.expandedUpSide;
  graphInstance.updateNodeData(node, {
    expandedUpSide: newExpanded
  });
  const relatedNodes = graphInstance.getNodeRelatedNodes(node);
  const leftNodes = relatedNodes.filter(n => n.lot.level < 0);
  leftNodes.forEach(node => {
    graphInstance.updateNode(node, {
      hidden: newExpanded === false
    });
  });
}

这个片段展示了自定义节点插槽如何把公司节点同时变成一个紧凑的信息块,以及浮动合作方详情层的触发器。

const MyComponyDetail: React.FC<{ node: RGNode, checked?: boolean }> = ({ node, checked }) => {
  const partnerInfo = node.data.info || {
    name: "JINAN MEIDE CASTING CO LTD",
    country: "China",
    yearsActive: 8,
  };
  return (
    <div className="flex items-center justify-center px-4 py-2 text-sm font-medium transition-all gap-2">
      <HotelIcon className="text-blue-500 shrink-0" size={16} />
      <div className="rg-node-text">{node.text}</div>
      <div className="absolute top-[60px]">
        {checked && <PartnerCard partner={partnerInfo} />}
      </div>
    </div>
  )
}

这个示例的独特之处

对比数据并不支持把这里的某一个机制单独视为独特能力。其他示例同样展示了懒展开、自定义节点渲染或根节点侧分支控制。真正突出的地方在于这些能力的组合方式。

expand-button 相比,这个示例把懒展开放进了一个具备贸易语境的仪表板场景中,而不是把这种机制单独拿出来展示。与 multiple-expand-buttons 相比,它在根节点级分支切换的基础上,又加入了异步图增长以及选中节点后的详情卡片。与 investmentinvestment-penetration 相比,这里的重点不再是所有权关系或导航流转,而转向贸易维度探索和合作伙伴检查。

因此,当需求是“中心为一个核心公司,周围围绕若干领域类别,分支延迟加载,并为所选合作伙伴提供更丰富的画布内详情视图”时,这个示例就是一个很强的起点。

这种模式还适用于哪里

这种模式很适合用于供应链探索、采购审查工具、进出口调查界面,以及 B2B 合作伙伴尽调仪表板。同样的分裂式根节点结构可以表示上游与下游关系、国内与海外市场,或者入向与出向物流类别。

当完整关系图过大而无法预加载时,它也是一个很有用的模板。在这种情况下,可以沿用当前结构接入真实 API,让每个分支只在分析人员展开某个类别时才加载,而 checked 节点上的覆盖层则可以作为一个紧凑区域,用来展示合作伙伴指标、合规说明或产品摘要。