# 实现方案 — 2026-04-17-13-32-07
## 目标
修复 `TemplateManage.tsx` 中两个编辑器交互问题:
1. `Ctrl+Z` 快捷键无法撤销自定义删除行为。
2. 在特定 HTML 结构(段落以 `
` 结尾)下插入 `smart-field-wrapper` 会导致换行错位。
---
## 变更文件
- `src/pages/TemplateManage.tsx`
---
## 具体改动
### 改动 A:拦截键盘 Undo/Redo 快捷键
**位置**:`TemplateManage.tsx` 中现有的 `keydown` 事件监听 `useEffect`(第 184~243 行附近)。
**做法**:
在 `handleKeyDown` 函数的最开头,增加对 `Ctrl+Z` / `Cmd+Z` / `Ctrl+Shift+Z` / `Ctrl+Y` / `Cmd+Y` 的拦截:
```typescript
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
e.preventDefault();
if (e.shiftKey) {
handleRedo();
} else {
handleUndo();
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
e.preventDefault();
handleRedo();
return;
}
```
**注意**:`handleUndo` / `handleRedo` 在该组件内是稳定函数(通过 ref 访问 DOM 状态),因此可直接在原生事件监听器闭包中调用,无需额外依赖。
---
### 改动 B:替换 `insertSmartField` 的插入方式
**位置**:`insertSmartField` 函数(第 304~315 行附近)。
**原逻辑**:
```typescript
document.execCommand('insertHTML', false, html);
```
**新逻辑**:
使用 `Range.insertNode()` 精确插入,避免 `execCommand` 在 `
` 边界处的标签逃逸。
```typescript
const insertSmartField = (field: FormField) => {
editorRef.current?.focus();
restoreSelection();
if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
return;
}
pushHistory();
const html = ` ×`;
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
range.deleteContents();
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
const fragment = document.createDocumentFragment();
while (wrapper.firstChild) {
fragment.appendChild(wrapper.firstChild);
}
range.insertNode(fragment);
// 将光标移动到插入内容末尾
const lastNode = fragment.lastChild;
if (lastNode) {
const newRange = document.createRange();
newRange.setStartAfter(lastNode);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
}
}
editorRef.current?.focus();
saveTemplateContent();
};
```
**说明**:
- `range.deleteContents()` 对 collapsed 的光标无实际影响,安全。
- `fragment.lastChild` 引用的是已被移入文档的具体节点,`setStartAfter` 可正确定位光标。
- 末尾的 ``(零宽空格)作为 `TextNode` 被一同插入,依然起到防止字段被意外吞并的作用。
---
## 回滚策略
- 修改前 `git` 仓库已处于干净状态(最新提交 `b822bb1`)。
- 若验证失败,可直接 `git checkout -- src/pages/TemplateManage.tsx` 回滚到上一版本,重新分析。
---
## 无其他依赖变更
- 不新增 npm 依赖。
- 不修改 `defaultContent.ts` 或 `index.css`。