- Intercept Ctrl+Z/Y keyboard shortcuts in keydown listener and route
to custom undoStack/redoStack to fix undo inconsistency.
- Replace execCommand('insertHTML') with precise Range.insertNode()
in insertSmartField to prevent <span> escaping out of <p> when
preceded by <br>.
74 lines
3.6 KiB
Markdown
74 lines
3.6 KiB
Markdown
# 需求分析 — 2026-04-17-13-32-07
|
||
|
||
## 用户反馈
|
||
|
||
1. **Ctrl+Z 快捷键无法撤销对 `smart-field-wrapper` 的删除**,但点击工具栏的撤销按钮可以正常撤销。
|
||
2. **插入 `smart-field-wrapper` 时仍会分成两行**。用户怀疑是原本文本结构的问题,并提供了出现问题的 HTML 片段:
|
||
```html
|
||
<p style="font-family: SimSun;">
|
||
<strong>手术日期:</strong><br>
|
||
</p>
|
||
<span class="smart-field-wrapper" ...>...</span>
|
||
```
|
||
用户补充:把字段插到下面的内容中(即上述 HTML 中已有字段的位置)则没有问题。
|
||
|
||
---
|
||
|
||
## 问题 1:Ctrl+Z 快捷键撤销失效
|
||
|
||
### 现状
|
||
- `TemplateManage.tsx` 已实现了自定义的 `undoStack` / `redoStack` 以及 `handleUndo()` / `handleRedo()`。
|
||
- 工具栏的撤销/重做按钮调用的是自定义函数,能够正确恢复 `innerHTML` 历史快照。
|
||
- 键盘按下 `Ctrl+Z` 时,浏览器会触发**原生 `undo`**(`document.execCommand('undo')`),它与自定义历史栈完全脱节,因此:
|
||
- 若原生栈为空(或已耗尽),则没有任何反应;
|
||
- 若原生栈有记录,可能恢复出意料之外的状态。
|
||
|
||
### 根因
|
||
- 当前只在编辑器上拦截了 `Backspace` / `Delete` 键(用于防止误删整段),**没有拦截 `Ctrl+Z` / `Ctrl+Y` 快捷键**。
|
||
|
||
### 需求
|
||
- 在 `TemplateManage.tsx` 的编辑器 `keydown` 事件中,拦截以下快捷键并路由到自定义 Undo/Redo 逻辑:
|
||
- `Ctrl+Z` / `Cmd+Z` → `handleUndo()`
|
||
- `Ctrl+Shift+Z` / `Cmd+Shift+Z` → `handleRedo()`
|
||
- `Ctrl+Y` / `Cmd+Y` → `handleRedo()`
|
||
- 拦截后调用 `e.preventDefault()` 阻止浏览器原生行为。
|
||
|
||
---
|
||
|
||
## 问题 2:插入 smart-field-wrapper 分成两行
|
||
|
||
### 现状
|
||
- `insertSmartField()` 使用 `document.execCommand('insertHTML', false, html)` 插入字段。
|
||
- 当光标位于一个以 `<br>` 结尾的 `<p>` 标签末尾时,WebKit/Blink 内核的 `insertHTML` 会把 `<span>` 插到 `<p>` **外部**,导致字段独自占据新行。
|
||
|
||
### 根因
|
||
- 用户提供的 HTML 明确显示了这一现象:`<span>` 跑到了 `</p>` 之后。
|
||
- `execCommand('insertHTML')` 对块级元素边界(尤其是末尾存在 `<br>` 时)的自动修正行为不可控。
|
||
|
||
### 需求
|
||
- 将 `insertSmartField()` 的插入方式从 `execCommand('insertHTML')` 替换为**精确的 `Range.insertNode()` 手动插入**:
|
||
1. `restoreSelection()` 恢复光标;
|
||
2. 获取当前 `Selection` 的 `Range`;
|
||
3. `range.deleteContents()`(对 collapsed 光标无实际删除);
|
||
4. 将 HTML 字符串转为 `DocumentFragment`;
|
||
5. `range.insertNode(fragment)` 精确插入到 Range 位置;
|
||
6. 把光标移动到插入内容的末尾(最后一个节点之后),保持编辑连贯性。
|
||
- 该方式不依赖浏览器的 `execCommand` 自动修正,可避免 `<span>` 被抛到 `<p>` 外部。
|
||
|
||
---
|
||
|
||
## 影响范围
|
||
|
||
- **仅 `src/pages/TemplateManage.tsx`** 需要修改:
|
||
- `insertSmartField()` 函数(替换插入逻辑)。
|
||
- `keydown` 事件监听 `useEffect`(增加快捷键拦截)。
|
||
- 其他页面(`ReportEditor.tsx`、`ReportManage.tsx` 等)不受影响。
|
||
|
||
---
|
||
|
||
## 验收标准
|
||
|
||
1. 在 `TemplateManage` 编辑器中删除一个 `smart-field-wrapper`(通过点击 × 或 Backspace/Delete)后,立即按 `Ctrl+Z`,字段能够完整恢复;按 `Ctrl+Y`(或 `Ctrl+Shift+Z`)能够重做删除。
|
||
2. 在 `<p>` 标签末尾(尤其是存在 `<br>` 的情况下)插入 `smart-field-wrapper`,字段与段落保持在同一行,不再被拆成两行。
|
||
3. `npm run lint` 通过,无 TypeScript 编译错误。
|