Files
TrueGrowth/docs/MEDIA_LIBRARY_INSERTION_LESSONS.md

304 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 素材库插入画布经验总结
这份文档沉淀 2026-04-22 这轮“素材库点击插入无响应”的排查与修复经验,重点不是复盘某一个按钮,而是明确一条规则:
- “打开素材库浏览”
- “打开素材库并选择后插入画布”
这两个动作在产品语义和代码语义上都不能混用。
## 一、问题现象
用户在画布页通过左侧工具栏或快捷工具栏打开素材库后,点击“插入”没有插入到画布,也没有明显反馈。
表面看像是“插入逻辑失效”,但真实原因是两层问题叠加:
- 工具栏入口想打开的是 `SELECT` 模式素材库
- 实际打开的是全局默认 `BROWSE` 模式素材库
- 即使走到 `onSelect`,素材库内部也没有等待异步插入完成,失败和卡顿都被伪装成“没反应”
## 二、根因
### 1. 全局素材库默认是 `BROWSE`,但画布入口需要 `SELECT`
画布页工具栏和快捷工具栏并不总是打开自身维护的 `MediaLibraryModal`,当父层传入 `onOpenMediaLibrary` 时,会改为打开 `drawnix.tsx` 里统一管理的全局素材库。
而全局素材库原来只接收:
- `isOpen`
- `onClose`
没有接收:
- `mode`
- `filterType`
- `onSelect`
- `selectButtonText`
结果就是:
- 入口想要“插入画布”
- 实际打开成了“浏览素材”
- 用户看见的是素材库开了,但插入语义丢了
经验:
- “统一弹窗管理”不能只统一开关状态
- 还必须统一携带“打开语义”
- 否则很容易出现入口 A 和入口 B 打开的是同一个弹窗壳,但行为完全不一致
### 2. 异步选择动作不能先关窗再说
原来的 `MediaLibraryModal` 在双击素材或点击“使用”按钮时是这样执行的:
- `onSelect(asset)`
- `onClose()`
这里最大的问题是 `onSelect` 允许异步,但弹窗不等待结果。
后果:
- 插入慢时,用户看到弹窗先消失,但画布没变化
- 插入失败时,错误反馈晚于关窗,用户会误以为“没点上”
- 如果中途 pending很容易被误判成点击失效
经验:
- 只要动作会改动画布、网络、缓存、解码资源,就应视为异步事务
- 弹窗不能在事务开始后立刻关闭
- 必须等待完成,再关闭或给失败反馈
## 三、这次修法
### 1. 给全局素材库增加“打开配置”
这次把全局素材库状态从单纯的 `boolean` 扩成了“开关 + 配置”:
- `mode`
- `filterType`
- `onSelect`
- `selectButtonText`
这样不同入口可以明确表达自己的意图:
- 缓存清理入口:`BROWSE`
- 画布插入入口:`SELECT + onSelect`
经验:
- 全局弹窗状态不要只存 `visible`
- 还要存“这个弹窗是为谁、以什么模式打开的”
### 2. 工具栏入口显式传入 `SELECT`
画布左侧工具栏和快捷工具栏现在打开素材库时,会显式传:
- `mode: SelectionMode.SELECT`
- `onSelect: handleInsertAsset`
- `selectButtonText: '插入'`
这样不会再依赖全局默认值。
经验:
- 入口的行为语义,必须在入口处声明
- 不能靠下游组件默认值“猜”
### 3. 素材库等待插入完成后再关闭
`MediaLibraryModal` 里的双击和“使用”按钮,现在改成:
- 设置 `isSelecting`
- `await onSelect(asset)`
- 成功后再 `onClose()`
同时:
- “插入”按钮显示 loading
- loading 时禁止重复点击
- 卸载后通过 `isMountedRef` 防止无意义的状态回写
经验:
- 交互上要把“正在插入”显式呈现出来
- 这不仅是体验问题,也是排障问题
- 用户可见的 loading本质上是运行时可观测性的一部分
## 四、这次沉淀出的规则
### 规则 1弹窗统一管理时必须保留入口语义
不要只抽象:
- `openXxxModal()`
要抽象成:
- `openXxxModal(config)`
至少要能带:
- 模式
- 回调
- 文案
- 筛选条件
### 规则 2`BROWSE` 和 `SELECT` 是两种产品态,不是一个小参数
`BROWSE` 表示:
- 查看
- 管理
- 下载
- 删除
`SELECT` 表示:
- 选中后回传
- 触发上游业务动作
- 按上游场景展示按钮文案
经验:
- 如果一个组件同时承载两种态,就必须在类型和状态上明确区分
- 不能让调用方靠“有没有按钮”去推断当前模式
### 规则 3任何“选择后执行动作”的弹窗都要把异步状态做完整
至少包含:
- loading
- 防重复提交
- 成功后关闭
- 失败时保留现场
不要做成:
- 点一下就关
- 剩下靠日志和运气
### 规则 4统一弹窗很方便但默认值很危险
这次问题本质上就是:
- 统一弹窗没错
- 但把行为寄托在默认值上,导致入口语义丢失
经验:
- 越是“被多个入口复用”的弹窗,越不能依赖默认行为
- 默认值只能兜底,不能承载主流程
## 五、建议后续继续保持
- 新增全局弹窗时,优先设计 `open(config)` 而不是 `setVisible(true)`
- 只要弹窗里的主按钮会触发异步副作用,就必须带 loading
- 当一个入口的目标是“插入到画布”,要从入口到弹窗都显式传递这层语义,不要中途丢失
- 如果未来再拆工具栏或迁移弹窗管理层,优先回归测试“入口模式是否正确透传”
## 六、涉及文件
- `packages/drawnix/src/drawnix.tsx`
- `packages/drawnix/src/components/startup/DrawnixDeferredFeatures.tsx`
- `packages/drawnix/src/components/toolbar/creation-toolbar.tsx`
- `packages/drawnix/src/components/toolbar/quick-creation-toolbar/quick-creation-toolbar.tsx`
- `packages/drawnix/src/components/media-library/MediaLibraryModal.tsx`
- `packages/drawnix/src/components/media-library/MediaLibraryInspector.tsx`
- `packages/drawnix/src/components/toolbar/toolbar.types.ts`
- `packages/drawnix/src/types/asset.types.ts`
## 七、一句话结论
素材库“能打开”不等于“能插入”。
对画布入口来说,真正重要的是:入口语义要带到弹窗里,异步插入要被用户看见。🎯
## 八、画布多文件拖拽插入经验
更新日期2026-04-27
### 1. 问题现象
用户希望一次拖多个图片/视频进入画布。第一轮修复后,多文件可以插入,但当原画布内容很多、画布很大时,拖入完成后视口会偏移,新增内容不在当前可见区域。
这类问题表面是“插入位置不对”,本质是两个坐标系统同时变化:
- Drop 事件发生时,鼠标位置来自屏幕/DOM 坐标。
- Plait 插入元素后,画布 `viewBox` 可能因为内容边界扩大而重算。
- 如果只在插入前计算一次 `toViewBoxPoint`,插入后 DOM scroll 和 viewport origination 可能不再对应,用户就会被带到别的位置。
### 2. 设计原则
1. 批量拖拽不是把 `files[0]` 改成循环这么简单。
- 图片、视频、音频要走各自已有插入链路。
- 单张图片拖到 mind node 时,应保留“替换节点图片”的旧语义。
- 多文件拖拽时,不应让多个文件抢同一个节点,应统一按坐标插入画布。
2. 大文件批量处理要顺序执行。
- 不要对多个视频同时 `Promise.all` 存储、解码或读元数据。
- 图片加载、视频缓存、音频封面读取都可能触发大内存占用。
- 高并发文件处理场景下,宁可略慢,也要避免内存峰值和交换分区压力。
3. 本地视频类型要同时看 MIME 和扩展名。
- macOS/浏览器可能把 `.mov` 标成 `video/quicktime`
- 有些拖入来源可能不给 `file.type`
- 类型兜底应集中在 `data/blob.ts`,不要让各入口各写一份扩展名判断。
4. 批量插入后要锚定拖放点。
- 记录 drop 时鼠标下的画布坐标 `point`
- 同时记录鼠标在 board 容器内的屏幕偏移 `activePoint`
- 插入完成并等待 Plait 完成一轮/两轮渲染后,用 `BoardTransforms.updateViewport` 恢复:
```typescript
const origination: [number, number] = [
point[0] - activePoint[0] / zoom,
point[1] - activePoint[1] / zoom,
];
BoardTransforms.updateViewport(board, origination, zoom);
```
这样无论 `viewBox` 怎么重算,用户鼠标落下的画布点都会回到原来的屏幕位置,新增内容仍在可见区域。
### 3. 实现要点
- `with-image.tsx` 的 drop 入口应先把 `FileList` 过滤成结构化的 `DroppedMediaFile[]`,而不是在循环里散落判断。
- 批量坐标使用固定网格偏移,避免所有文件堆在同一点。
- 视频文件先进入 `assetStorageService`,再复用 `insertVideoFromUrl`,不要新增一套视频元素结构。
- 当浏览器没有提供正确 MIME 时,可通过 `file.slice(0, file.size, resolvedMimeType)` 生成带类型的 Blob保证 Cache Storage 元数据和 Response header 正确。
- 图片插入函数可增加 `skipScroll` 参数,批量拖拽由上层统一控制视口,避免每张图自己滚动。
- 视口恢复应使用 `updateViewport` 一步设置 `origination + zoom`,不要拆成滚动 DOM 或 `updateZoom + moveToCenter`
### 4. 验证建议
1. 精确单测:
```bash
pnpm --dir packages/drawnix exec vitest run src/data/blob.test.ts
```
2. 类型检查:
```bash
pnpm nx run drawnix:typecheck
```
3. 手动回归:
- 大画布、已有大量元素、滚动到远离原点的位置。
- 一次拖入 2 张以上图片,新增内容应出现在 drop 点附近且可见。
- 一次拖入图片 + `.mov` / `.mp4` 视频,视频应进入画布并保留可播放语义。
- 单张图片拖到 mind node 上,仍应替换该节点图片。
### 5. 涉及文件
- `packages/drawnix/src/plugins/with-image.tsx`
- `packages/drawnix/src/data/blob.ts`
- `packages/drawnix/src/data/image.ts`
- `packages/drawnix/src/data/video.ts`
- `packages/drawnix/src/services/asset-storage-service.ts`
- `packages/drawnix/src/constants/ASSET_CONSTANTS.ts`
### 6. 一句话结论
画布文件拖拽要同时处理“文件批量语义”和“视口锚定语义”。
能插入多个文件只是第一步,真正稳定的体验是:插入完成后,用户仍然停在自己放手的位置。📍