[{"data":1,"prerenderedAt":7},["ShallowReactive",2],{"example-markdown-content:zh:metric-rollup-graph-editor":3},{"markdown":4,"isPaidExample":5,"isTruncated":5,"charLimit":6},"# 构建一个基于图的指标汇总编辑器\n\n## 本示例实现的内容\n\n这个示例构建了一个紧凑的 relation-graph 编辑器，用于处理指标或成本汇总。界面初始状态包含一个铺满全屏的点阵画布、一个紫色汇总节点、两个蓝色数值节点、一组橙色编辑工具栏，以及位于图上方的浮动辅助窗口。\n\n用户可以将新的数值节点或汇总节点拖到画布上，点击线条工具把输入连接到汇总节点，双击数值和名称进行编辑，删除当前选中的节点或连线，切换到树形布局，并打开共享设置或导出面板。这个示例的重点在于，这张图不仅仅是一个图示：进入某个节点的连线定义了汇总总值该如何重新计算。\n\n## 数据是如何组织的\n\n初始数据以内联方式声明为一个 `RGJsonData` 对象，其中包含 `nodes` 和 `lines`。每个初始节点都已经带有固定的 `x` 和 `y` 坐标，以及存放在 `data` 中的自定义元数据，尤其是 `myNodeType` 和 `numberValue`。这让图同时具备可视化和语义化特征：节点类型控制行为，而 `numberValue` 则是用于聚合的字段。\n\n在执行 `setJsonData(...)` 之前，代码会对每一条初始连线做规范化处理，确保它们具备 `id`，将线条形状切换为 `Curve7`，并设置明确的拐点。运行时创建则使用第二层数据定义：工具栏中包含 `value-node` 和 `sum-node` 两种可复用的节点模板，以及一个用于汇总连线的线条模板。在真实系统中，同样的结构可以表示 KPI 汇总、预算拆解、项目成本模型、人员总量统计，或者任何上游数值输入会驱动派生汇总结果的依赖图。\n\n## relation-graph 的使用方式\n\n`RGProvider` 包裹了整个示例，这样 relation-graph 的 hooks 就能在图组件、工具栏和浮动设置面板之间解析当前激活的图实例。在 `MyGraph` 中，`RGHooks.useGraphInstance()` 会通过 `setJsonData(...)` 加载初始数据集，重新计算汇总节点，然后调用 `moveToCenter()` 和 `zoomToFit()` 让画面重新居中并适配视图。\n\n图配置保持了一个简洁、面向编辑器的基础场景。图初始使用 `fixed` 布局，采用基于边框的连线连接点，使用 `StandardCurve` 绘制连线，并把内置工具栏移到右侧。后续有一个独立的工具栏操作，会通过 `updateOptions(...)` 和 `doLayout()` 将其切换为自上而下的树形布局。\n\n两个 relation-graph 插槽定义了自定义 UI。`RGSlotOnNode` 替换了默认节点主体，因此数值节点会渲染内联数字编辑器，汇总节点会显示计算后的总值，并且每个节点都会在卡片下方显示一个可编辑的名称标签。`RGSlotOnView` 则把橙色的 `SummarizerToolbar` 直接注入图视口，而不是依赖外部页面侧栏。\n\n编辑流程依赖的是图实例 API，而不是外部有状态布局代码。工具栏通过 `startCreatingNodePlot(...)` 启动节点创建，使用 `generateNewUUID()` 生成 id，通过 `addNodes(...)` 插入新项，通过 `startCreatingLinePlot(...)` 启动连线创建，在调用 `addLines(...)` 之前校验目标节点，并通过 `removeNode(...)` 或 `removeLink(...)` 删除当前选中项。`RGHooks.useCreatingNode()` 和 `RGHooks.useCreatingLine()` 会暴露当前创建模式，使工具栏能够直观高亮当前激活的工具。\n\n浮动的 `DraggableWindow` 是一个共享辅助组件，而不是这个示例独有的逻辑，但它仍然是完整工作流中的重要一环。它的设置面板通过 `RGHooks.useGraphStore()` 读取图状态，使用 `setOptions(...)` 修改滚轮和拖拽行为，并通过调用 `prepareForImageGeneration()`、`domToImageByModernScreenshot(...)` 和 `restoreAfterImageGeneration()` 导出当前画布。局部 SCSS 则完善了编辑器外观，包括点阵背景、放置在节点外部的名称标签，以及用于线条标签的更强烈的选中态样式。\n\n## 关键交互\n\n拖拽任一工具栏模板都会启动 relation-graph 的节点落点创建流程，并在拖拽结束的位置插入一个新的类型化节点。新节点会继承模板中的颜色、形状和 `myNodeType`，因此同一组工具栏既能控制可编辑的数值节点，也能控制计算型汇总节点。\n\n点击线条工具会启动引导式连线创建。代码禁止把数值节点作为目标节点，因此新建连线会被视为进入汇总节点的输入，而不是任意连接。添加有效连线后，示例会立即重新执行聚合逻辑。\n\n双击数值节点中的数字会打开一个内联输入框。提交编辑后，会通过 `updateNodeData(...)` 写回新的 `numberValue`，并重新计算所有汇总节点。双击名称标签使用的是同样的内联编辑模式，但它通过 `updateNode(...)` 更新 `node.text`，而不会触发数值重算。\n\n节点和连线删除都具备选中感知。点击节点或连线会将其保存为当前选中项，工具栏中会出现对应的删除按钮，而点击画布背景则会同时清除本地选中状态和 relation-graph 的选中状态。除了编辑功能外，浮动辅助窗口还可以被拖动、切换为设置面板，并用于将当前图导出为图片。\n\n## 关键代码片段\n\n下面这个片段展示了初始图使用固定坐标定位，并且节点元数据已经携带了编辑器后续使用的类型和数值。\n\n```tsx\nconst myJsonData: RGJsonData = {\n  rootId: 'my-root',\n  nodes: [\n    { id: 'my-root', text: '成本', color: 'rgba(214,103,239,0.59)', x: -50, y: -2, data: { myNodeType: 'sum-node', numberValue: '510000' }},\n    { id: 'newNode-1', text: '材料成本', color: '#5da0f8', x: -167, y: -176, data: { myNodeType: 'value-node', numberValue: '10000' }},\n    { id: 'newNode-3', text: '人力成本', color: '#5da0f8', x: 29, y: -176, data: { myNodeType: 'value-node', numberValue: '500000' }}\n  ],\n  lines: [\n```\n\n下面这个片段证明了汇总值是由图拓扑推导出来的：代码读取流入节点，并把计算结果写回节点数据。\n\n```tsx\nconst incomingNodes = graphInstance.getNodeIncomingNodes(node);\n\nlet sum = 0;\nincomingNodes.forEach(inNode => {\n  let val = 0;\n  if (inNode.data?.myNodeType === 'sum-node') {\n    val = analyzeNodesForSum(inNode, newPath);\n  } else {\n    val = parseFloat(inNode.data?.numberValue || '0');\n  }\n  sum += isNaN(val) ? 0 : val;\n});\ngraphInstance.updateNodeData(node, { numberValue: sum.toFixed(3) });\n```\n\n下面这个片段展示了 `RGSlotOnNode` 如何把节点主体变成可编辑的属性界面，而不只是一个普通标签。\n\n```tsx\n{node.data?.myNodeType === 'value-node' ? (\n  \u003CMyEditableProperty\n    dataType=\"number\"\n    currentValue={node.data.numberValue}\n    onChange={(val) => {\n      graphInstance.updateNodeData(node, { numberValue: String(val) });\n      reCalcSumNodeValue();\n    }}\n  />\n) : (\n  \u003Cdiv className=\"font-bold px-2\">{node.data?.numberValue}\u003C/div>\n)}\n```\n\n下面这个片段展示了引导式连线编辑规则：工具栏启动 relation-graph 的建线流程，拒绝无效目标，并在插入后重新计算。\n\n```tsx\ngraphInstance.startCreatingLinePlot(e.nativeEvent, {\n  template: { ...lineTemplate },\n  onCreateLine: (from, to, finalTemplate) => {\n    if ('id' in to) {\n      if (to.data?.myNodeType === 'value-node') {\n        return alert('Value node cannot be a summary target!');\n      }\n      graphInstance.addLines([{ ...finalTemplate, from: (from as RGNode).id, to: (to as RGNode).id, text: 'Summarizes' }]);\n      onLineCreated();\n    }\n  }\n});\n```\n\n下面这个片段展示了浮动辅助窗口如何通过 relation-graph 实例 API 暴露运行时画布行为调整和图片导出能力。\n\n```tsx\nconst canvasDom = await graphInstance.prepareForImageGeneration();\nlet graphBackgroundColor = graphInstance.getOptions().backgroundColor;\nif (!graphBackgroundColor || graphBackgroundColor === 'transparent') {\n  graphBackgroundColor = '#ffffff';\n}\nconst imageBlob = await domToImageByModernScreenshot(canvasDom, {\n  backgroundColor: graphBackgroundColor\n});\nawait graphInstance.restoreAfterImageGeneration();\n```\n\n## 这个示例的独特之处\n\n对比数据表明，这个示例并不只是因为使用了插槽、固定坐标或浮动辅助窗口而显得独特。邻近示例如 `drag-to-create-nodes-with-preset-styles`、`create-object-from-menu`、`use-d3-layout` 和 `canvas-selection`，都已经覆盖了这部分实现空间中的某些要素。这里更少见的是围绕汇总模型构建的一整组编辑特性组合。\n\n与 `drag-to-create-nodes-with-preset-styles` 相比，这里并不是一个通用的样式编辑画布。被创建出来的节点带有语义类型，允许建立的连接受到约束，并且成功的编辑会直接驱动总值重算。与 `create-object-from-menu` 相比，这里的工作流是持续可见且带引导性的，而不是菜单驱动的：节点创建、连线创建、重命名、数值编辑和删除都保留在同一个覆盖式界面中。与 `use-d3-layout` 和 `canvas-selection` 相比，这里的重点从外部布局或选择机制，转向了由拓扑关系驱动的计算。\n\n准备好的稀有性与对比记录指向同一个结论：当需求是一个边表示聚合关系的轻量图编辑器时，这个示例是一个很强的起点。它最不常见的组合是模板驱动的节点创建、带校验的连线编辑、基于插槽的内联属性编辑、递归式汇总重算、选中感知删除、可选重新布局，以及共享的画布设置或导出工具同屏出现。\n\n## 这种模式还能应用到哪里\n\n这种模式非常适合用于 KPI 汇总设计器、预算和成本拆解规划工具、收入分解工具以及运营记分卡编辑器。在这些场景中，用户需要建模上游数值如何流入一个或多个汇总节点，并立即验证最终得到的总值。\n\n它同样适用于那些把图本身作为计算模型的依赖式规划工具，例如人员估算、组件成本分摊或分层目标规划。在不改变整体编辑结构的前提下，同样的基于插槽的编辑模式还可以扩展到每个节点更多字段、更严格的校验规则，或领域专用的节点模板。\n",false,500,1782615411646]