企业股权结构图
这个示例渲染自上而下的企业股权结构图:投资方位于焦点公司上方,子公司位于下方。它结合自定义企业卡片节点插槽、上游分支布局后清理、一次性懒加载子公司展开,以及用于画布设置和图片导出的共享悬浮面板。
带有惰性子公司展开的企业股权结构图
此示例构建的内容
这个示例构建的是一个以查看为主的企业所有权结构图。投资方显示在焦点公司上方,运营子公司显示在其下方,并且每条关系都标注了持股比例。
结果比通用树形图示例更具体。节点被渲染为公司卡片,根公司显示为蓝色横幅,特殊公司可以显示控制人或风险标记,画布还使用了重复的分析风格背景。用户可以展开选定的子公司以显示更多分支,打开悬浮设置面板,修改画布交互行为,并将当前图谱导出为图片。
这个示例最值得关注的地方,是在同一个查看器中结合了三种行为:业务化节点渲染、布局完成后对上游分支的清理,以及一次性的惰性分支扩展。
数据是如何组织的
图谱数据是在 initializeGraph() 内部以内联方式组装的,整体是一个 RGJsonData 对象。初始数据声明了 rootId: '0'、11 个节点和 10 条所有权连线。这个结构已经将上游投资方(a-1、a-2、a)与焦点公司(0)以及下游子公司(1 到 5,以及 4 下面的子节点)分开。
有两个子公司在 setJsonData(...) 执行之前被预先设置为惰性加载占位节点。它们的节点数据包含 remoteChilds: true 和 loaded: false,因此即使这些子公司尚未出现在初始数据集中,图谱仍然可以显示展开控件。
这里还有一个重要的加载后预处理步骤。在 relation-graph 完成布局后,代码会从实时节点中读取 node.lot.level,收集上游节点和根节点 id,把父侧展开占位器移动到顶部,隐藏根节点的展开占位器,并调整投资方侧边的连线标签位置。
在真实业务系统中,这种结构同样可以表示股东、控股实体、受控子公司、持股比例,以及诸如实控人或注销状态标记之类的简单公司状态信息。
relation-graph 的使用方式
这个示例包裹在 RGProvider 中,图谱本身通过 RGHooks.useGraphInstance() 驱动。基础图谱选项定义了一个自上而下生长的 tree 布局,保持列居中,并使用 20px 的水平间距和 80px 的垂直间距。
有几个选项共同塑造了这个股权图的展示方式。节点默认使用矩形形状且没有内置边框,连线使用 RGLineShape.SimpleOrthogonal,拐角通过 defaultPolyLineRadius: 5 设为圆角,RGJunctionPoint.tb 让连线固定锚定在顶部和底部连接点上。reLayoutWhenExpandedOrCollapsed 保持启用,以便惰性分支展开时布局仍然一致。
这个示例并不依赖默认节点渲染。RGSlotOnNode 用公司卡片模板替换了节点主体,它通过检查 node.lot.level === 0 来识别根节点,应用全宽的根横幅样式,并根据 node.data 有条件地显示控制人或风险标记。
图实例 API 是整个流程的核心。setJsonData(...) 负责加载初始结构,getNodes() 和 getLines() 暴露计算后的布局元数据,updateNode(...) 和 updateLine(...) 在布局后细化上游展示,而 addNodes(...)、addLines(...) 和 doLayout() 会在惰性分支展开时扩展图谱。moveToCenter() 和 zoomToFit() 则用于完成首次视图定位。
悬浮辅助窗口是一个复用的本地子组件,而不是股权图特有的逻辑,但它依然会影响这个示例的运行时行为。在这个共享组件内部,RGHooks.useGraphStore() 读取当前交互设置,setOptions(...) 切换滚轮和拖拽行为,图片导出流程则使用 prepareForImageGeneration()、getOptions() 和 restoreAfterImageGeneration()。
样式部分通过 SCSS 覆盖完成。样式表添加了平铺的画布背景,重新着色了展开按钮,为百分比标签加上了盒状样式,并定义了公司卡片的比例、标记徽章、选中态样式以及根横幅变体。
关键交互
最重要的交互是分支展开。当用户展开一个标记了 remoteChilds: true 且 loaded: false 的节点时,示例会显示加载状态,等待两秒,追加三个新的子公司及其持股连线,重新执行布局,并将该节点标记为已加载,以避免之后重复添加同一分支。
悬浮辅助窗口增加了第二层交互。用户可以拖动窗口、最小化窗口、打开设置浮层、将滚轮行为在滚动、缩放和无之间切换,将画布拖拽行为在框选、移动和无之间切换,并将当前图谱下载为图片。
这里还有一个 onNodeClick 处理器,但它只是将被点击的节点输出到控制台,因此不会实质性改变面向用户的行为。
关键代码片段
这个片段建立了视觉基础:一个自上而下的树形布局,使用正交连线,并且在展开或收起时自动重新布局。
const graphOptions: RGOptions = {
defaultExpandHolderPosition: 'bottom',
defaultNodeShape: RGNodeShape.rect,
defaultNodeBorderWidth: 0,
defaultLineShape: RGLineShape.SimpleOrthogonal,
defaultPolyLineRadius: 5,
defaultJunctionPoint: RGJunctionPoint.tb,
reLayoutWhenExpandedOrCollapsed: true,
layout: {
layoutName: 'tree',
from: 'top',
treeNodeGapH: 20,
treeNodeGapV: 80
}
};
这个片段表明,初始数据集已经同时包含了固定的所有权连线,以及为后续惰性展开预先准备好的节点。
const myJsonData: RGJsonData = {
rootId: '0',
nodes: [
{ id: 'a-1', text: 'Jack Li', data: { owner: true } },
{ id: 'a-2', text: 'Tom Yang' },
{ id: 'a', text: 'Zeekr Technology Limited' },
{ id: '0', text: 'Zhejiang Zeekr Intelligent Technology Co., Ltd.', width: 300 },
{ id: '1', text: 'Zeekr Automotive (Shanghai) Co., Ltd.', expandHolderPosition: 'bottom', expanded: false, data: { remoteChilds: true, loaded: false } },
{ id: '3', text: 'Zhejiang Zeekr Automotive Sales Co., Ltd.', expandHolderPosition: 'bottom', expanded: false, data: { remoteChilds: true, loaded: false } }
]
};
这个片段证明,该示例会在布局完成后读取计算得到的 node.lot.level 值,并用这些值把上游分支与根节点、下游节点区分开来处理。
graphInstance.getNodes().forEach((node) => {
if (!node.lot) return;
if (node.lot.level < 0) {
if (node.rgChildrenSize > 0) {
graphInstance.updateNode(node.id, {
expandHolderPosition: 'top'
});
}
parentsNodeIds.push(node.id);
} else if (node.lot.level === 0) {
graphInstance.updateNode(node.id, {
expandHolderPosition: 'hide'
});
parentsNodeIds.push(node.id);
}
});
这个片段展示了该清理步骤的第二部分:投资方侧的百分比标签会被移动到正交连线更靠近起点的位置。
graphInstance.getLines().forEach(line => {
if (parentsNodeIds.includes(line.from) && parentsNodeIds.includes(line.to)) {
graphInstance.updateLine(line.id, {
placeText: 'start',
polyLineStartDistance: 60
});
}
});
这个片段展示了惰性展开是如何作为对实时图谱的一次性运行时变更实现的,而不是通过完整重建整个数据集来完成。
if (node.data && node.data.remoteChilds === true && node.data.loaded === false) {
graphInstance.loading('Loading subsidiaries...');
node.data.loaded = true;
setTimeout(() => {
// ... newNodes and newLines are created here ...
graphInstance.addNodes(newNodes);
graphInstance.addLines(newLines);
graphInstance.doLayout();
graphInstance.clearLoading();
}, 2000);
}
这个片段展示了 RGSlotOnNode 如何将默认节点转换为具有根节点、控制人和风险变体的业务卡片。
<RGSlotOnNode>
{({ node }: RGNodeSlotProps) => {
const isRoot = node.lot && node.lot.level === 0;
return (
<div className={`my-industy-node ${isRoot ? 'my-root' : ''}`}>
{!isRoot && node.data?.owner && <div className="my-card-tag my-card-tag-owner">Controller</div>}
{!isRoot && node.data?.risk && <div className="my-card-tag my-card-tag-risk">Cancelled</div>}
<div className="my-card-body">{node.text}</div>
</div>
);
}}
</RGSlotOnNode>
这个片段展示了共享设置面板如何在不修改股权数据本身的情况下改变运行时的画布行为。
<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 }); }}
/>
这个示例的独特之处
与附近的 investment-penetration、industry-chain、layout-tree、expand-button 和 bothway-tree2 等示例相比,这个示例是固定自上而下股权结构且明确显示持股比例的最清晰参考。它的核心启发并不是通用树形布局、类型化分支导航,或孤立存在的惰性加载,而是业务化所有权视图与选择性分支扩展的结合。
对比数据也支撑了一个更具体的区别:这个示例会在布局完成后检查 node.lot.level,把上游展开占位器移动到顶部,隐藏根节点展开占位器,并将投资方到根节点的标签移向这些连线的起始端。与更简单的惰性展开和基础布局类邻近示例相比,这是一个更专业化的清理步骤。
它最少见、也最值得复用的组合包括:公司卡片节点插槽、盒状百分比标签、带图案的画布、父侧标签清理,以及同一查看器中的一次性惰性子公司加载。因此,与通用树图或纯技术性的展开按钮示例相比,它更适合作为所有权分析类界面的起点。
这种模式还适用于哪些场景
这种模式很适合迁移到企业股权穿透图、投资组合结构图、多实体控制关系图,以及需要以某个核心公司为中心、同时呈现上游所有者和下游子公司的合规审查界面。
当上游一侧需要与下游一侧不同的标签位置或展开控件行为时,同样的方法也可以适配到非金融层级结构中。例如特许经营所有权结构、合作伙伴网络控制图,以及带状态标记的监管实体树。
如果产品后续需要接入真实后端数据,占位式的 remoteChilds 模式可以替换为真正的 API 驱动展开,同时保留相同的插槽渲染、布局后清理和导出流程。