JavaScript is required

分页懒加载分支节点

这个示例展示如何在左到右企业关系树中对投资与股东分支进行分页懒加载。首批数据在分支展开时加载,后续分页通过图内合成的 `Load More(...)` 节点触发;悬浮辅助窗可调分页大小并提供共享画布设置与图片导出。

通过图内 Load More 节点延迟加载分页分支

这个示例构建了什么

这个示例围绕一个焦点公司构建了一个偏查看器风格的企业关系树。画布上展示了一个大型蓝色根卡片、四个按类型划分的投资与股东分类分支节点,以及只有在用户展开这些分支后才会出现的普通公司节点。

其核心行为是在图内部按分支进行分页。某个分支第一次展开时会拉取第一页,后续页面通过一个作为图节点渲染的合成 Load More(...) 节点来请求,同时还提供了一个浮动辅助窗口,让用户可以调整分页大小,并继续使用共享的画布设置和图片导出工具。

数据是如何组织的

初始图数据是一个内联的 RGJsonData 种子,其中包含一个根节点和四个分支节点。每个分支都通过 data.myType 标识其数据来源,并通过 childrenLoaded 防止第一页被重复拉取。

分页数据并不来自初始图 JSON。mock-data-api.ts 预先生成了四个模块级公司数组,并暴露了 fetchMockDataFromRemoteServer(...),它会针对某一种分支类型返回一个带延迟的分页切片以及该分支的总数。这样可以让示例聚焦于增量加载,而不是完整重建整张图。

在每一页追加数据进行布局之前,loadNextPageData(...) 会将拉取到的数据行转换为一个小型 RGJsonData 片段,为每个公司节点打上 ownerType 标记,并通过新建连线把这些节点挂接到对应的分支节点上。它还会把当前分支节点的 xy 坐标复制到每个追加节点上,这样后续重新布局时就会从分支位置开始,而不是从无关坐标开始。

在真实应用中,同样的结构也可以用于表示分页的投资方、股东、子公司、案件、工单,或任何其他较长的同级列表。在这些场景下,某一个分支应当逐步增长,而不是一次性加载全部内容。

relation-graph 是如何使用的

RGProvider 提供图上下文,RGHooks.useGraphInstance() 则驱动初始化和增量更新。组件挂载时,示例会调用 loading()setJsonData(...)clearLoading()moveToCenter()zoomToFit() 来初始化并定位首屏视图。

图使用从左到右的树形布局,配置包括 layoutName: 'tree'from: 'left'treeNodeGapH: 100treeNodeGapV: 10alignItemsX: 'start'alignParentItemsX: 'end'。再结合 defaultLineShape: RGLineShape.StandardOrthogonaldefaultJunctionPoint: RGJunctionPoint.lr 以及位于右侧的展开控制点,就形成了一个从根节点向右水平展开分支的业务树展示方式。

加载流程结合了 relation-graph 的事件与实例 API。onNodeExpand 会在这些类型化分支节点上检查 childrenLoaded,随后 loadNextPageData(...) 使用 getNodes() 找到正确的分支节点,调用 addNodes(...)addLines(...) 追加下一页内容,使用 sleep(400) 等待节点尺寸测量完成,调用 doLayout() 重新计算位置,并通过 removeNodeById(...) 用新的继续加载节点替换上一个继续节点。

RGSlotOnNode 是表现层的核心。它将根卡片、四个分支按钮、蓝色的 Load More(...) 延续胶囊,以及普通的白色公司卡片渲染为不同的视觉状态,尽管它们本质上都属于同一套图节点系统。局部 SCSS 则进一步覆盖了画布背景、选中态样式、展开按钮样式以及节点外观。

浮动辅助窗口及其设置面板来自共享子组件,并非该示例独有。在这个演示中,它们提供了运行时分页大小输入框、通过 setOptions(...) 切换滚轮与拖拽模式,以及通过 prepareForImageGeneration()getOptions()restoreAfterImageGeneration() 实现图片导出。

关键交互

  • 第一次展开某个类型化分支时,会拉取该分支公司节点的第一页数据。
  • 点击合成的 Load More(...) 节点,会在不重建整张图的情况下,为同一分支追加下一页数据。
  • 修改数字形式的分页大小输入会影响后续请求,因为当前的 pageSize 状态会传入 mock API 调用。
  • 点击画布空白区域会通过 clearChecked() 清除节点和连线的选中状态。
  • 浮动辅助窗口可以拖动、最小化、切换到设置面板,并用于将当前画布导出为图片。
  • 普通节点点击会被记录日志,但在这个演示中不会改变图状态。

关键代码片段

下面这段代码展示了图如何以一个公司根节点和四个支持展开触发加载的类型化分支节点作为起点。

const myJsonData: RGJsonData = {
    rootId: 'root',
    nodes: [
        { id: 'root', text: 'Tian Technology Co., Ltd.', expandHolderPosition: 'hide', data: { myType: 'root' } },
        { id: 'root-invs', text: 'Investment', disablePointEvent: true, expandHolderPosition: 'right', expanded: false, data: { myType: 'investment-button', childrenLoaded: false } },
        { id: 'root-history-invs', text: 'Historical Investment', expandHolderPosition: 'right', expanded: false, data: { myType: 'historical-investment-button', childrenLoaded: false } },
        { id: 'root-sh', text: 'Share Holder', disablePointEvent: true, expandHolderPosition: 'right', expanded: false, data: { myType: 'shareholder-button', childrenLoaded: false } },
        { id: 'root-history-shareholder', text: 'Historical Share Holder', expandHolderPosition: 'right', expanded: false, data: { myType: 'historical-shareholder-button', childrenLoaded: false } }
    ],

下面这段代码展示了分支数据源是一个本地 mock API,它返回的是带延迟的分页切片,而不是一次性返回全部数据行。

export const fetchMockDataFromRemoteServer = async (dataType: string, pageSize: number, fromIndex: number) => {
    return new Promise((resolve) => {
        setTimeout(function () {
            const allItems = mockRemoveDataBase[dataType];
            const total = allItems.length;
            const endIndex = Math.min((fromIndex + pageSize - 1), total);
            const currentPageItems = allItems.slice(fromIndex, endIndex);
            resolve({ currentPageItems, total });

下面这段代码展示了每个类型化分支在第一次展开时只会加载一次,而且第一次请求从索引 0 开始。

const onNodeExpand = async (node: RGNode, e: RGUserEvent) => {
    if (node.data.childrenLoaded) {
        return;
    }
    node.data.childrenLoaded = true;
    const myType = node.data.myType;
    const fromIndex = 0;
    await loadNextPageData(myType, fromIndex);
};

下面这段代码展示了单次拉取的一页数据如何被作为图的增量追加,而不是整体替换现有数据。

for (const entItem of currentPageItems) {
    currentPageGraphJsonData.nodes.push({ id: entItem.companyId, text: entItem.companyName, data: { ownerType: myType } });
    currentPageGraphJsonData.lines.push({ from: typeNodeId, to: entItem.companyId });
}
updateLoadMoreButtonNode(typeNodeId, myType, fromIndex + currentPageItems.length, total, currentPageGraphJsonData);
currentPageGraphJsonData.nodes.forEach((n: JsonNode) => {
    n.x = typeNode.x;
    n.y = typeNode.y;
});
graphInstance.addNodes(currentPageGraphJsonData.nodes);
graphInstance.addLines(currentPageGraphJsonData.lines);

下面这段代码展示了继续加载控件本身也是一个合成图节点,它会保存下一次加载所需的分支类型和偏移量。

const loadNextPageButtonNodeId = `${myType}-next-button`;
graphInstance.removeNodeById(loadNextPageButtonNodeId);
const remainingItemCount = total - fromIndex + 1;
if (remainingItemCount > 0) {
    currentPageGraphJsonData.nodes.push({ id: loadNextPageButtonNodeId, text: `Load More(${remainingItemCount})`, data: { myType: 'more-btn', loadType: myType, fromIndex: (fromIndex + 1) } });
    currentPageGraphJsonData.lines.push({ from: typeNodeId, to: loadNextPageButtonNodeId });
}

下面这段代码展示了 RGSlotOnNode 如何把继续加载节点渲染为图内可点击操作,而不是普通节点文本。

{myType === 'more-btn' && (
  <div className="my-node more-btn px-2" onClick={() => {loadNextPage(node)}}>
    {node.text}
  </div>
)}
{!myType && (
  <div className="my-node">
    {node.text}
  </div>
)}

下面这段代码展示了共享设置面板如何在 relation-graph 准备好渲染状态后导出图画布。

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

对比数据表明,这个示例在企业树演示中属于“按分支分页”的变体,而不是一个通用的延迟展开示例。与 show-more-nodes-front 相比,它不会预先加载分支成员再显示隐藏的溢出部分;它会在展开时拉取第一页,然后通过渲染在图内的重复 Load More(...) 点击,持续扩展同一个分支。

investment-penetrationscene-orginvestment 相比,它的重点也更集中且更容易复用。那些相邻示例同样使用基于追加的方式增长业务树,但这个示例聚焦的是一组固定的根分支,这些分支可以在相同的分支角色下持续加载更多页面,并且通过浮动辅助窗口暴露运行时分页大小控制。

expand-gradually 相比,这里的主要经验并不是折叠或揭示那些初始数据中已经存在的后代节点。这个示例真正有辨识度的模式是图原生的继续加载控制:类型化分支按钮触发第一次拉取,合成的继续节点保留分支上下文,而增量的 addNodes(...)addLines(...)doLayout() 则让实时图在原位持续增长。

稀有度数据进一步强化了这种组合方式。这个示例把类型化根分支按钮、首次展开加载、通过 slot 渲染的继续加载胶囊、运行时分页大小调节、增量追加 API,以及蓝白配色的业务画布组合在一起。当需要让密集的同级列表逐步进入画面而不是替换整张图时,它就是一个很强的起点。

这种模式还适用于哪里

这种模式适用于企业股权查看器、供应商或分销商关系图、案件调查树,以及服务依赖关系浏览器等场景。在这些场景中,某个类别下可能有很多同级节点,且应当按增量方式加载。

它也适用于那些需要让控制界面停留在图内部,而不是移到侧边面板或外部分页器中的图界面。当加载动作需要始终附着在某个分支上,并且应当在受影响节点附近保持可见时,分支级的继续节点就非常有用。

另一个扩展方向是分析人员工具:操作者需要紧凑的首屏视图、可调的批量大小,以及在同一界面完成图片导出。当前演示没有实现特定领域的审核动作,但它的分支级分页模式可以直接复用于这些场景。