2026-04-16-20-33-12 - 将自动帧插入改为 setTimeout 非阻塞异步执行,避免阻塞关键帧摘取
This commit is contained in:
@@ -520,18 +520,24 @@ export default function ReportEditor() {
|
||||
setCapturedFrames(accumulatedFrames);
|
||||
});
|
||||
stateRef.current = { ...stateRef.current, capturedFrames: accumulatedFrames };
|
||||
if (settings.autoInsertFrames && settings.autoInsertFrameIndices?.includes(i) && editorRef.current) {
|
||||
if ((settings.autoInsertDelay || 0) > 0) {
|
||||
await new Promise<void>(r => setTimeout(r, (settings.autoInsertDelay || 0) * 1000));
|
||||
}
|
||||
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
|
||||
if (emptyPlaceholder) {
|
||||
emptyPlaceholder.innerHTML = `
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<img src="${newFrame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
|
||||
`;
|
||||
emptyPlaceholder.classList.add('has-image');
|
||||
}
|
||||
if (settings.autoInsertFrames && settings.autoInsertFrameIndices?.includes(i)) {
|
||||
const baseDelay = (settings.autoInsertDelay || 0) * 1000;
|
||||
const insertOrderIndex = settings.autoInsertFrameIndices.indexOf(i);
|
||||
const actualDelay = baseDelay > 0 ? baseDelay * (insertOrderIndex + 1) : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!editorRef.current) return;
|
||||
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
|
||||
if (emptyPlaceholder) {
|
||||
emptyPlaceholder.innerHTML = `
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<img src="${newFrame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
|
||||
`;
|
||||
emptyPlaceholder.classList.add('has-image');
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
saveDraftToStorage();
|
||||
}
|
||||
}, actualDelay);
|
||||
}
|
||||
}
|
||||
if (settings.autoInsertFrames && editorRef.current) {
|
||||
|
||||
90
工程分析/实现方案-2026-04-16-20-33-12.md
Normal file
90
工程分析/实现方案-2026-04-16-20-33-12.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 实现方案 — 2026-04-16-20-33-12
|
||||
|
||||
## 根因分析
|
||||
|
||||
`autoCaptureFrames` 的 `for` 循环内部,自动插入逻辑使用了:
|
||||
```tsx
|
||||
if ((settings.autoInsertDelay || 0) > 0) {
|
||||
await new Promise<void>(r => setTimeout(r, (settings.autoInsertDelay || 0) * 1000));
|
||||
}
|
||||
```
|
||||
|
||||
`await` 会暂停整个 `for` 循环的执行,导致:
|
||||
1. 关键帧摘取被强制暂停,等待延迟结束;
|
||||
2. 所有帧必须一张一张串行处理,整体耗时 = 摘取时间 + 插入延迟 × 插入帧数;
|
||||
3. 用户体验上感觉"卡顿"或"慢"。
|
||||
|
||||
## 修改文件清单
|
||||
|
||||
| 文件 | 修改类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| `src/pages/ReportEditor.tsx` | 修改 | `autoCaptureFrames` 中自动插入逻辑改为 `setTimeout` 非阻塞执行 |
|
||||
|
||||
## 具体代码变更
|
||||
|
||||
### 变更:`autoCaptureFrames` 中的插入逻辑(约第 523-535 行)
|
||||
|
||||
**当前代码:**
|
||||
```tsx
|
||||
if (settings.autoInsertFrames && settings.autoInsertFrameIndices?.includes(i) && editorRef.current) {
|
||||
if ((settings.autoInsertDelay || 0) > 0) {
|
||||
await new Promise<void>(r => setTimeout(r, (settings.autoInsertDelay || 0) * 1000));
|
||||
}
|
||||
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
|
||||
if (emptyPlaceholder) {
|
||||
emptyPlaceholder.innerHTML = `...`;
|
||||
emptyPlaceholder.classList.add('has-image');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**修改为:**
|
||||
```tsx
|
||||
if (settings.autoInsertFrames && settings.autoInsertFrameIndices?.includes(i)) {
|
||||
const baseDelay = (settings.autoInsertDelay || 0) * 1000;
|
||||
const insertOrderIndex = settings.autoInsertFrameIndices.indexOf(i);
|
||||
const actualDelay = baseDelay > 0 ? baseDelay * (insertOrderIndex + 1) : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!editorRef.current) return;
|
||||
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
|
||||
if (emptyPlaceholder) {
|
||||
emptyPlaceholder.innerHTML = `
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<img src="${newFrame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
|
||||
`;
|
||||
emptyPlaceholder.classList.add('has-image');
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
saveDraftToStorage();
|
||||
}
|
||||
}, actualDelay);
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
- `for` 循环全速运行,不再被插入延迟阻塞;
|
||||
- 每张需要插入的帧按顺序延迟(第 1 张 delay,第 2 张 2×delay...),避免同时插入;
|
||||
- `setTimeout` 回调中实时查询 DOM 获取最新的空 placeholder;
|
||||
- 插入后同步 `contentRef.current` 并保存草稿。
|
||||
|
||||
### 附加变更:移除循环后的批量 `contentRef` 更新
|
||||
|
||||
当前代码在循环结束后:
|
||||
```tsx
|
||||
if (settings.autoInsertFrames && editorRef.current) {
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
由于每个 `setTimeout` 回调内部已经单独更新 `contentRef` 和保存草稿,且循环结束时可能 `setTimeout` 尚未执行,这句批量更新既不及时也可能遗漏。建议**移除或保留不影响功能**。为简化逻辑,选择保留但无实质影响,因为非阻塞的 `setTimeout` 回调会各自负责自己的保存。
|
||||
|
||||
## 风险点
|
||||
|
||||
| 风险 | 级别 | 应对措施 |
|
||||
|------|------|---------|
|
||||
| `setTimeout` 回调执行时 placeholder 已被用户手动填充 | 低 | 回调中实时查询 `.image-placeholder:not(.has-image)`,找不到则跳过,不会覆盖用户内容 |
|
||||
| 多张图片按顺序延迟插入时,用户快速离开页面 | 低 | 每次插入后都调用 `saveDraftToStorage()`,已插入的图片会被保存;未执行的 `setTimeout` 自然丢弃 |
|
||||
|
||||
## 回滚策略
|
||||
|
||||
修改范围极小,仅涉及 `autoCaptureFrames` 中的几行代码。如有异常可直接 revert。
|
||||
37
工程分析/测试方案-2026-04-16-20-33-12.md
Normal file
37
工程分析/测试方案-2026-04-16-20-33-12.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 测试方案 — 2026-04-16-20-33-12
|
||||
|
||||
## 测试目标
|
||||
|
||||
验证自动帧插入改为非阻塞后:
|
||||
1. 关键帧摘取过程不再被插入延迟阻塞;
|
||||
2. 图片按顺序、按延迟时间依次插入 placeholder;
|
||||
3. 插入后草稿正常保存。
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 用例 1:非阻塞摘取
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1.1 | 系统设置开启自动插入,延迟 2 秒,勾选多个帧索引 | — |
|
||||
| 1.2 | 上传视频,点击「自动关键帧摘取」 | 右侧关键帧列表在 1-2 秒内迅速全部出现,不再卡顿等待 |
|
||||
|
||||
### 用例 2:顺序延迟插入
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 2.1 | 确保编辑器中有足够空 placeholder | — |
|
||||
| 2.2 | 点击「自动关键帧摘取」 | 摘取完成后,placeholder 按顺序每隔约 2 秒插入一张,不是同时插入 |
|
||||
|
||||
### 用例 3:插入后内容保存
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 3.1 | 等待自动插入全部完成 | — |
|
||||
| 3.2 | 切换到 `/report-manage` 再返回 | 已插入 placeholder 的图片保留在编辑器中 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 关键帧摘取不受插入延迟阻塞,快速完成
|
||||
- [ ] 图片按顺序依次插入,不堆积
|
||||
- [ ] 插入后的内容能正常保存
|
||||
28
工程分析/经验记录.md
28
工程分析/经验记录.md
@@ -206,3 +206,31 @@
|
||||
- 自动保存函数的 `useCallback` dependency 应尽量精简(如只保留 `reportId`),避免因 state 变化导致闭包更新不同步。
|
||||
- 任何直接操作 DOM 修改编辑器内容的代码,都必须**紧跟一行 `contentRef.current = editorRef.current.innerHTML`**,确保内存中的内容快照与 DOM 保持一致。
|
||||
- 在开发阶段应定期测试「组件卸载 → 重新挂载」的场景(React 18 `StrictMode` 会自动模拟),提前暴露闭包和 ref 同步问题。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 记录 10:自动帧插入阻塞关键帧摘取——改为 setTimeout 非阻塞异步插入
|
||||
|
||||
**A. 具体问题**
|
||||
开启「自动帧插入」后,点击「自动关键帧摘取」时,系统不是快速完成所有关键帧的摘取,而是每摘取一张就停下来等待插入延迟(如 2 秒),插入完成后才继续摘取下一张。整体过程非常缓慢,用户体验卡顿。
|
||||
|
||||
**B. 产生问题原因**
|
||||
`autoCaptureFrames` 的 `for` 循环内部,自动插入逻辑使用了 `await new Promise<void>(r => setTimeout(...))`:
|
||||
```tsx
|
||||
if ((settings.autoInsertDelay || 0) > 0) {
|
||||
await new Promise<void>(r => setTimeout(r, (settings.autoInsertDelay || 0) * 1000));
|
||||
}
|
||||
```
|
||||
|
||||
`await` 会暂停整个 `for` 循环的执行,导致关键帧摘取和插入变成了串行阻塞流程:必须等插入完成才能摘取下一张。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. 将 `await new Promise(...)` 替换为 `setTimeout(...)`,把插入操作推入事件队列异步执行,`for` 循环不再被阻塞,可以全速完成所有关键帧的摘取。
|
||||
2. 实现延迟叠加(顺序插入):通过 `settings.autoInsertFrameIndices.indexOf(i)` 计算当前帧是第几个需要插入的,延迟时间为 `baseDelay * (insertOrderIndex + 1)`,避免所有图片在同一时刻同时插入。
|
||||
3. `setTimeout` 回调中实时查询 `.image-placeholder:not(.has-image)`,找到则插入,并同步更新 `contentRef.current` 和调用 `saveDraftToStorage()`。
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 在异步循环中,如果某个操作不需要依赖前一步的完成结果,**绝对不要使用 `await` 阻塞主循环**,应改用 `setTimeout` 或 `Promise.all` 实现并行/异步解耦。
|
||||
- 当多个定时任务需要按顺序执行时,可以通过索引计算累积延迟(`delay * (index + 1)`),实现简单的"队列式"顺序触发,而不需要阻塞主流程。
|
||||
- 在 `setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。
|
||||
|
||||
26
工程分析/需求分析-2026-04-16-20-33-12.md
Normal file
26
工程分析/需求分析-2026-04-16-20-33-12.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 需求分析 — 2026-04-16-20-33-12
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
`autoCaptureFrames` 中自动插入关键帧到 placeholder 的逻辑使用了 `await new Promise(setTimeout(...))`,这会**阻塞 `for` 循环**,导致必须等待插入延迟结束后才会开始摘取下一帧。期望将其改为**异步非阻塞**,使关键帧摘取全速运行,插入操作在延迟后独立执行,两者互不影响。
|
||||
|
||||
## 需求拆解
|
||||
|
||||
### 功能点
|
||||
- 移除 `autoCaptureFrames` 中自动插入逻辑的 `await` 阻塞;
|
||||
- 使用 `setTimeout` 将插入操作推入事件队列异步执行;
|
||||
- 实现延迟叠加(顺序插入),避免多张图片在同一时刻同时插入。
|
||||
|
||||
### 非功能点
|
||||
- 保持现有 `flushSync` 实时显示关键帧的效果;
|
||||
- 不破坏现有的 `contentRef` 同步和草稿保存机制。
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 模块 | 影响程度 | 说明 |
|
||||
|------|---------|------|
|
||||
| `src/pages/ReportEditor.tsx` | 中 | 仅修改 `autoCaptureFrames` 中的自动插入逻辑 |
|
||||
|
||||
## 待确认问题
|
||||
|
||||
无。修改方向明确。
|
||||
Reference in New Issue
Block a user