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

3.6 KiB
Raw Blame History

实现方案 — 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 的拦截:

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 行附近)。

原逻辑

document.execCommand('insertHTML', false, html);

新逻辑 使用 Range.insertNode() 精确插入,避免 execCommand<br> 边界处的标签逃逸。

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.tsindex.css