For the complete documentation index, see llms.txt. This page is also available as Markdown.

画布编辑器中的撤销重做和剪贴板处理

画布编辑器一旦进入复杂场景,撤销重做就不能只保存整份 JSON。因为画布元素多了之后,全量快照会越来越大,性能和内存都会出问题。

比较稳的方式是:

  • 给每个元素注入稳定 UUID。

  • 每次操作后计算差异。

  • 历史栈里存差异,而不是全量 JSON。

  • 撤销/重做时反向应用差异。

  • 输入法、拖拽、内部文本编辑需要单独处理。

历史项结构

一个差异项可以设计成这样:

interface DiffItem {
  uuid: string;
  type: "update" | "add" | "remove";
  oldProps?: Record<string, any>;
  newProps?: Record<string, any>;
  data?: any;
  parentId?: string;
  index?: number;
}

含义:

  • update:保存变更前后的属性。

  • add:保存新增元素完整数据和插入位置。

  • remove:保存被删除元素完整数据和原位置。

这样撤销时可以反向执行:

防抖保存

拖拽、输入、缩放这类操作会连续触发事件,不能每一帧都入栈。

防抖时间不宜太长,否则用户操作结束后撤销不及时;也不能太短,否则历史栈会很碎。

输入法要特殊处理

中文输入时,input 事件会在拼音组合过程中不断触发。如果这个时候保存历史,会出现一次输入被拆成很多步的问题。

这段处理可以让中文输入确认后再保存一次历史。

拦截撤销快捷键

如果用户正在输入框里按 Cmd/Ctrl + Z,浏览器默认会撤销输入框内容。画布编辑器里通常希望统一走编辑器自己的撤销逻辑。

剪贴板复制

画布元素复制时,不建议只复制当前对象的引用,要复制它的 JSON 数据。

多选时可以把多个元素先组成一个 group,再写入剪贴板。

粘贴图片

设计器里用户经常会直接复制图片,然后在画布里粘贴。浏览器剪贴板里拿到的是 Blob,需要判断类型:

图片粘贴时可以先转成 File,再走统一上传逻辑:

粘贴位置

如果支持“粘贴到鼠标位置”,需要在 pointer move 时记录鼠标坐标:

粘贴时再把全局点转换为当前画布内部坐标:

总结

画布编辑器的撤销重做和剪贴板不是两个孤立功能,它们都依赖同一件事:元素数据必须可序列化、可定位、可恢复。

建议一开始就做好:

  • 元素 UUID。

  • 差异历史。

  • 输入法状态。

  • 快捷键拦截。

  • JSON 剪贴板。

  • 图片 Blob 粘贴。

这些基础能力做好之后,后面做多人协同、自动保存、版本恢复也会更容易。