图谱撤销重做与历史记录
这个示例展示一个紧凑可编辑 relation-graph 画布,支持基于快照的撤销、重做和历史跳转。它会在支持的编辑后记录图状态,提供键盘快捷键和悬浮历史控件,并通过序列化图 JSON 重建节点与连线以恢复旧版本。
紧凑型图编辑器中的撤销、重做与快照历史
这个示例构建了什么
这个示例构建了一个带有文档式历史控制的小型可编辑图画布。用户会看到一个固定位置的 relation graph、一个用于撤销、重做和显示历史记录的悬浮工具栏,以及一个按时间倒序列出已保存快照的悬浮历史面板。
图本身在视觉上保持简洁:矩形节点、标准连线,以及铺满整个视口的画布空间。关键行为在于,诸如移动节点、调整节点大小、创建连线和删除节点这类常规编辑,都会被记录为快照,这些快照可以被撤销、重做,或者通过点击某条历史记录直接回到对应版本。
这个示例最有价值的地方并不在于视觉定制,而是在比完整编辑器工作台小得多的例子里,紧凑地结合了图编辑覆盖层、键盘快捷键、回退后的分支截断,以及基于重建的历史恢复机制。
数据是如何组织的
初始图以内联 RGJsonData 的形式声明,其中每个节点都带有 id、text 以及手动编写的 x 和 y 坐标,每条线都带有 id、from、to 和标签文本。由于布局是固定的,这些坐标就是实际渲染位置,而不是自动布局过程的输入。
在 setJsonData(...) 之前,这里不存在任何转换流水线。示例直接加载种子数据集、将视口居中,并立刻保存一个初始快照。之后,历史管理器会把 getGraphJsonData() 返回的实时图状态序列化为一个 HistoryGraphData 记录,其中包含生成的快照 id、本地时间戳、序列化后的 JSON 字符串、操作描述,以及以 KB 为单位的大致载荷大小。
这种结构可以自然映射到真实应用数据,例如工作流草稿、拓扑修订、组织结构图编辑或关系审查。在这些场景中,节点和连线的载荷可以承载业务标识符与元数据,而快照包装层则作为图状态本身的轻量级修订历史。
relation-graph 是如何使用的
该演示在入口处使用 RGProvider,并渲染了一个配置为固定布局的 RelationGraph。图选项整体上保持接近库的默认基线,但通过设置矩形节点、显式默认节点尺寸、2 像素默认线宽、dragEventAction: 'selection' 和 disableDragNode: false,让编辑器的行为更加可预测。
RGHooks.useGraphInstance() 是主要集成点。组件通过它使用 setJsonData(...) 加载种子图、调用 moveToCenter() 将画布居中、通过 getGraphJsonData() 捕获快照、使用 removeNode(...) 删除节点、通过 generateNewUUID(...) 创建新 id、调用 startCreatingLinePlot(...) 启动交互式连线创建,并通过 clearGraph()、addNodes(...) 和 addLines(...) 恢复已保存状态。
编辑 UI 组装在 RGSlotOnView 内部。RGEditingNodeController 为当前激活节点启用编辑覆盖层,RGEditingResize 添加缩放手柄,RGEditingConnectController 支持连接目标选择,而自定义的 MyNodeToolbar 则在选中节点周围添加方向性连接按钮和删除按钮。RGHooks.useEditingNodes() 让该工具栏只在当前正好有一个节点处于编辑状态时可见。
定制被刻意控制在较小范围内。图仍然使用默认节点渲染,而历史工具栏与历史面板采用内联定位和简单的白色覆盖层样式。这样可以更容易复用状态管理模式,而不必继承一个庞大的视觉外壳。
关键交互
点击某个节点会将其设为当前编辑节点,从而激活缩放控制器以及节点周围的自定义工具栏。点击空白画布区域则会清空编辑中的节点、清空任何编辑中的连线状态,并清空已勾选的选择项,使编辑器回到空闲状态。
拖动节点和完成一次尺寸调整都会创建快照。删除当前选中节点时,也会在删除后生成一个新快照。对于连线创建,选中节点的工具栏会启动 startCreatingLinePlot(...);当用户完成一次有效连接时,代码会通过图实例添加新连线,并再存储一条历史记录。
撤销与重做既可以通过悬浮工具栏触发,也可以通过键盘快捷键触发。组件会监听 Ctrl 或 Cmd 加上 Z、Y 或 Shift+Z,阻止浏览器默认行为,并将该动作路由给历史管理器。历史面板随后以可见形式暴露同一条时间线,其中任意已保存版本卡片都可以被点击,以直接跳转到对应快照。
关键代码片段
这个片段说明,编辑器构建在一个具有简单默认值的固定布局之上,因此手动编写的节点坐标就是历史记录需要保留的图状态。
const graphOptions: RGOptions = {
debug: false,
defaultNodeShape: RGNodeShape.rect,
defaultNodeWidth: 100,
defaultNodeHeight: 40,
defaultLineWidth: 2,
layout: {
layoutName: 'fixed',
},
dragEventAction: 'selection',
disableDragNode: false,
};
这个 effect 证明,键盘快捷键是工作流中的一等组成部分,而不仅仅是额外绑定的按钮行为。
if (isCtrlOrCmd) {
if (e.key === 'z' && !e.shiftKey) {
e.preventDefault();
if (currentIndex > 0) {
myHistoryManager.current.undo();
}
} else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) {
e.preventDefault();
if (currentIndex < history.length - 1) {
myHistoryManager.current.redo();
}
}
}
这个片段是历史策略的核心:在回退之后再次编辑会移除前向分支,并且已存储的快照列表会被限制在最大长度以内。
let newHistory = [...this.historyList];
if (this.currentIndex < newHistory.length - 1) {
newHistory = newHistory.slice(0, this.currentIndex + 1);
}
newHistory.push(snapshot);
if (newHistory.length > this.maxHistorySize) {
newHistory = newHistory.slice(-this.maxHistorySize);
}
这个恢复路径表明,撤销、重做以及直接跳转到某个版本,都是通过用序列化后的节点和连线重新构建图来实现的。
loadHistory(snapshot: HistoryGraphData) {
const data = JSON.parse(snapshot.jsonString);
this.graphInstance.clearChecked();
this.graphInstance.clearGraph();
this.graphInstance.addNodes(data.nodes);
this.graphInstance.addLines(data.lines);
}
这个选中节点工具栏片段展示了,连线创建和节点删除是如何作为上下文式图内操作呈现出来的,而不是放在单独的侧边面板中。
const editingNodes = RGHooks.useEditingNodes();
const onlyNode = editingNodes.nodes[0];
return (
editingNodes.nodes.length === 1 ? <div className="w-full h-full">
<div className="pointer-events-auto absolute top-[50%] left-[-40px] transform translate-y-[-50%]">
<button
className="cursor-pointer h-6 w-6 bg-blue-500 hover:bg-blue-700 text-white rounded shadow flex place-items-center justify-center"
onClick={(e) => {
startCreateLineFromCurrentNode(onlyNode, RGJunctionPoint.right, e)
}}
>
这个示例的独特之处
根据预先准备的对比数据,这个示例最接近 create-line-from-node、line-vertex-on-node 和 editor-button-on-line 这类紧凑型编辑示例,因为它使用了相同的选中节点编辑外壳和图内连接工作流。不同之处在于其教学重点的落点。这里,上下文式连线创建只是为一个可复用历史系统提供变更来源的方式之一。
这使该示例在两个方面具有独特性。第一,它在一个很小的界面中结合了若干相对少见的元素:固定布局编辑、选中节点操作按钮、尺寸调整覆盖层、在受支持编辑后捕获快照、键盘撤销与重做、回退后的分支截断,以及可点击的版本恢复。第二,与更完整的编辑器示例相比,它刻意避免了调色板创建、自定义连线插槽以及更厚重的编辑器外壳,从而让撤销/重做机制更容易被单独研究。
对对比文件的保守解读是:当需求是在常规编辑手势之上加入图状态历史时,这个示例是一个很好的起点,但它并不是完整的编辑器工作台。不应把它视为覆盖所有可能编辑动作或画布级状态的完整编辑器参考。
这种模式还适用于哪里
这种模式非常适合那些需要具备安全回退编辑能力、但又不想引入大型编辑器外壳的图工具。例如,用户经常需要重新排列并重连步骤的工作流设计器、需要尝试布局或连接变更并进行回滚的基础设施拓扑工具,以及分析人员希望在细化小型子图时保留可见检查点的知识图谱整理界面。
它也适用于那些直接版本跳转比单步撤销按钮更有价值的审查型场景。审核控制台、协作式草稿审查界面,或用于图编辑教学的工具,都可以复用同样的快照管理器与历史面板模式,同时替换为领域特定的节点数据和自定义样式。