Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-13-32-07.md
admin b155dd42d6 fix(TemplateManage): Ctrl+Z undo and smart-field insertion layout
- 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>.
2026-04-17 13:39:16 +08:00

116 lines
3.6 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-17-13-32-07
## 目标
修复 `TemplateManage.tsx` 中两个编辑器交互问题:
1. `Ctrl+Z` 快捷键无法撤销自定义删除行为。
2. 在特定 HTML 结构(段落以 `<br>` 结尾)下插入 `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``<br>` 边界处的标签逃逸。
```typescript
const insertSmartField = (field: FormField) => {
editorRef.current?.focus();
restoreSelection();
if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
return;
}
pushHistory();
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${field.key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>&#8203;`;
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` 可正确定位光标。
- 末尾的 `&#8203;`(零宽空格)作为 `TextNode` 被一同插入,依然起到防止字段被意外吞并的作用。
---
## 回滚策略
- 修改前 `git` 仓库已处于干净状态(最新提交 `b822bb1`)。
- 若验证失败,可直接 `git checkout -- src/pages/TemplateManage.tsx` 回滚到上一版本,重新分析。
---
## 无其他依赖变更
- 不新增 npm 依赖。
- 不修改 `defaultContent.ts``index.css`