Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-09-36-07.md
admin 38ff67a6a8 fix: smart field spacing/line-break in TemplateManage and default template
- Compress insertSmartField HTML to single-line, remove trailing  
- Compress smartField helper in defaultContent.ts to single-line
- Add white-space: nowrap to .smart-field-wrapper (CSS + inline)
- Add keydown interceptor in TemplateManage to prevent Backspace/Delete
  from removing whole <p> when adjacent to smart-field-wrapper
- Update experience record (#14)
2026-04-17 09:47:21 +08:00

7.5 KiB
Raw Blame History

实现方案 — 2026-04-17-09-36-07

根因分析

  1. 多余空格TemplateManage.tsxinsertSmartField 函数在 HTML 字符串末尾追加了 &nbsp;,这是导致字段后跟随大量空白的主要原因。
  2. 异常换行inline-block 元素默认会在边界处根据容器宽度自动换行;contenteditable="false" 节点在行尾时,浏览器可能将其视为独立的渲染单元进行换行。
  3. Backspace 误删整行:当光标位于 contenteditable="false" 的内联元素之后时Webkit/Blink 内核的默认行为无法正确删除该节点,而是向上寻找到父级 <p> 并将其删除。这是 contentEditable 的经典 Bug。
  4. 默认模板未预置defaultContent.ts 中的第一行仍使用红色纯文本占位符,没有使用 smartField() 函数生成智能控件。

修改文件清单

文件 修改类型 说明
src/pages/TemplateManage.tsx 修改 优化 insertSmartField HTML移除 &nbsp;、压缩为一行);增加 keydown 事件拦截,保护 .smart-field-wrapper 不被 Backspace 误删
src/utils/defaultContent.ts 修改 将默认模板第一行的红色占位符替换为预置的智能字段控件
src/index.css 修改(可选) .smart-field-wrapper 增加 white-space: nowrap

具体代码变更

变更 1src/pages/TemplateManage.tsx — 优化插入 HTML

当前代码(约第 159-173 行):

  const insertSmartField = (field: FormField) => {
    editorRef.current?.focus();
    const html = `
      <span class="smart-field-wrapper" contenteditable="false">
        <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;">
        </span>
      </span>&nbsp;
    `;
    document.execCommand('insertHTML', false, html);
    editorRef.current?.focus();
  };

修改为:

  const insertSmartField = (field: FormField) => {
    editorRef.current?.focus();
    const html = `<span class="smart-field-wrapper" contenteditable="false"><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; white-space: nowrap;"></span></span>`;
    document.execCommand('insertHTML', false, html);
    editorRef.current?.focus();
  };

改动点

  • 移除末尾的 &nbsp;
  • 将多行模板字符串压缩为一行,消除源码换行被渲染为空格的问题。
  • field-value 的内联样式中增加 white-space: nowrap;

变更 2src/pages/TemplateManage.tsx — 拦截 Backspace/Delete 防止误删整行

在现有的 useEffect(用于监听编辑器 click 事件)附近,新增一个 useEffect 监听 keydown

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key !== 'Backspace' && e.key !== 'Delete') return;
      const sel = window.getSelection();
      if (!sel || sel.rangeCount === 0) return;
      const range = sel.getRangeAt(0);
      if (!range.collapsed) return;

      const container = range.startContainer;
      const offset = range.startOffset;

      // Find the node immediately before the cursor
      let prevNode: Node | null = null;
      if (container.nodeType === Node.TEXT_NODE) {
        if (offset === 0) {
          prevNode = container.previousSibling;
        }
      } else if (container.nodeType === Node.ELEMENT_NODE) {
        prevNode = (container as Element).childNodes[offset - 1] || null;
      }

      if (!prevNode) return;

      // If the previous node is our smart field wrapper, remove it manually
      const fieldWrapper = prevNode.nodeType === Node.ELEMENT_NODE
        ? (prevNode as Element).closest('.smart-field-wrapper')
        : prevNode.parentElement?.closest('.smart-field-wrapper');

      if (fieldWrapper && editorRef.current?.contains(fieldWrapper)) {
        e.preventDefault();
        e.stopPropagation();
        fieldWrapper.remove();
      }
    };

    const editor = editorRef.current;
    if (editor) {
      editor.addEventListener('keydown', handleKeyDown, true);
    }
    return () => {
      if (editor) {
        editor.removeEventListener('keydown', handleKeyDown, true);
      }
    };
  }, [currentTemplateId]);

注意:此逻辑与 ReportEditor.tsx 中保护 .image-placeholder 不被误删的 handleKeyDown 思路一致。

变更 3src/utils/defaultContent.ts — 默认模板预置字段控件

当前第一行(约第 15-23 行):

<div class="template-info-section">
    <p style="font-family: SimSun;">
        姓名:${smartField('patientName')} 
        性别:${smartField('patientGender')} 
        年龄:${smartField('patientAge')} 
        科别:${smartField('department')} 
        床号:${smartField('bedNumber')} 
        住院号:${smartField('hospitalId')}
    </p>

这部分已经在上一版中被替换为 smartField(),但需要确认是否末尾有空格问题。由于 smartField() 函数本身返回的 HTML 不带 &nbsp;(且是压缩的一行),defaultContent.ts 中的这段代码本身没有问题。

:如果之前的 smartField() 定义末尾带有 &nbsp;,则需要一并修正。当前 defaultContent.ts 中的 smartField 定义已经在上一版中被修正为压缩的一行且不带 &nbsp;,所以默认模板本身已经符合要求。

确认结果defaultContent.ts 中的第一行在上一版(2026-04-17-00-13-09)中已经替换为 smartField('patientName') 等智能控件。本次只需确保 smartField 辅助函数的定义与变更 1 保持一致(移除 &nbsp;、压缩为一行、增加 white-space: nowrap)即可。

变更 4src/index.css — 增加 white-space: nowrap

.smart-field-wrapper 的样式中增加:

.smart-field-wrapper {
  display: inline-flex;
  align-items: center;
  margin: 0 2px;
  vertical-align: text-bottom;
  white-space: nowrap;
}

由于 field-value 已经通过内联样式设置了 white-space: nowrap,给外层 .smart-field-wrapper 增加此属性可作为双重保险。

风险点

风险 级别 应对措施
移除 &nbsp; 后,字段与前/后文本之间没有间隔,显得拥挤 margin: 0 2px 已经提供了 2px 的左右间距,视觉上足够紧凑
keydown 拦截可能影响编辑器其他正常删除操作 拦截逻辑严格限定在光标前一个节点为 .smart-field-wrapper 时才生效,其他情况正常放行
老模板中已插入的字段仍带有 &nbsp; 老模板中的字段只是带有一个额外的空格,不影响功能;用户可手动删除重插

回滚策略

本次修改范围极小,仅调整 insertSmartField 的 HTML 输出和增加一个 keydown 事件监听。如出现异常,可直接 git revert 回滚。


⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。