Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-12-51-47.md
admin b822bb1b47 fix: custom undo/redo stack and cursor positioning in TemplateManage
- Add undoStack/redoStack refs with pushHistory/handleUndo/handleRedo
- Replace execCmd('undo')/execCmd('redo') with custom stack handlers
- Call pushHistory before structural changes (delete, insert field/table/image, formatting)
- Add onMouseDown preventDefault to toolbar and field library buttons to stop focus loss
- Implement saveSelection/restoreSelection using savedRangeRef
- Bind onBlur/onMouseUp/onKeyUp on editor to persist cursor position
- Restore selection in insertSmartField and insertImage before insertHTML
- Update experience record (#20)
2026-04-17 13:18:54 +08:00

124 lines
3.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.
# 实现方案 — TemplateManage 撤销/重做修复与插入字段光标定位2026-04-17-12-51-47
## 一、修改文件清单
1. `src/pages/TemplateManage.tsx` — 核心改动:自定义 undo/redo 栈 + 光标保存恢复 + 阻止焦点流失
## 二、详细改动
### 2.1 自定义 Undo/Redo 栈
#### A. 新增 Refs
在组件顶部增加:
```ts
const undoStack = useRef<string[]>([]);
const redoStack = useRef<string[]>([]);
```
#### B. `pushHistory` 函数
在执行任何会改变编辑器内容的操作前调用,将当前 `innerHTML` 推入 undo 栈,并清空 redo 栈:
```ts
const pushHistory = () => {
if (!editorRef.current) return;
undoStack.current.push(editorRef.current.innerHTML);
redoStack.current = [];
};
```
#### C. `handleUndo` / `handleRedo`
替换原来调用的 `execCmd('undo')` / `execCmd('redo')`
```ts
const handleUndo = () => {
if (undoStack.current.length === 0 || !editorRef.current) return;
redoStack.current.push(editorRef.current.innerHTML);
const prev = undoStack.current.pop();
if (prev !== undefined) {
editorRef.current.innerHTML = prev;
saveTemplateContent();
}
};
const handleRedo = () => {
if (redoStack.current.length === 0 || !editorRef.current) return;
undoStack.current.push(editorRef.current.innerHTML);
const next = redoStack.current.pop();
if (next !== undefined) {
editorRef.current.innerHTML = next;
saveTemplateContent();
}
};
```
#### D. 埋点位置
在以下操作**执行前**调用 `pushHistory()`
- `handleEditorClick` 中删除 smart field 之前
- `handleKeyDown` 中删除 smart field 之前
- `insertSmartField` 执行 `insertHTML` 之前
- `insertTable` 执行 `insertHTML` 之前
- `insertImage` 执行 `insertHTML` 之前
- 工具栏按钮(粗体/斜体/下划线/对齐/颜色/字体等)操作前
**注意**:为了不过度累积历史记录,键盘输入不需要每次按键都 pushHistory浏览器原生 undo 可以处理普通文本输入。我们的自定义栈主要负责保护“结构性变更”(插入/删除字段、表格、图片等)。
### 2.2 阻止焦点流失 + 恢复光标位置
#### A. 阻止焦点流失
在字段库按钮和工具栏按钮上增加 `onMouseDown={(e) => e.preventDefault()}`,这是最简洁有效的办法:
```tsx
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => insertSmartField(field)}
...
>
```
#### B. 保存/恢复光标位置
利用已有的 `savedRangeRef`
```ts
const saveSelection = () => {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
savedRangeRef.current = sel.getRangeAt(0);
}
};
const restoreSelection = () => {
if (!savedRangeRef.current) return;
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRangeRef.current);
};
```
在编辑器 `<div>` 上绑定事件:
```tsx
<div
ref={editorRef}
contentEditable
className="..."
onBlur={saveSelection}
onMouseUp={saveSelection}
onKeyUp={saveSelection}
>
```
`insertSmartField` 中恢复光标:
```ts
const insertSmartField = (field: FormField) => {
editorRef.current?.focus();
restoreSelection();
// ... 唯一性校验 + insertHTML
};
```
### 2.3 工具栏按钮改造
所有工具栏按钮undo/redo 除外)增加 `onMouseDown={(e) => e.preventDefault()}`,并在 `onClick` 中先 `pushHistory()` 再执行命令。Undo/Redo 按钮不需要 `preventDefault`,因为它们本身不需要保持编辑器焦点,但也可以加上统一处理。
## 三、风险与回滚
- **风险**:自定义 undo/redo 栈会占用少量内存(存储 HTML 字符串),但模板内容通常不大,几十步历史记录的内存开销可忽略。
- **风险**`onMouseDown={e => e.preventDefault()}` 会阻止按钮的默认按下行为,但不会影响 `onClick` 的触发,这是 React 中阻止焦点流失的标准做法。
- **回滚**:如出现问题,可回退 `TemplateManage.tsx` 的修改。