- 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)
7.5 KiB
实现方案 — 2026-04-17-09-36-07
根因分析
- 多余空格:
TemplateManage.tsx的insertSmartField函数在 HTML 字符串末尾追加了 ,这是导致字段后跟随大量空白的主要原因。 - 异常换行:
inline-block元素默认会在边界处根据容器宽度自动换行;contenteditable="false"节点在行尾时,浏览器可能将其视为独立的渲染单元进行换行。 - Backspace 误删整行:当光标位于
contenteditable="false"的内联元素之后时,Webkit/Blink 内核的默认行为无法正确删除该节点,而是向上寻找到父级<p>并将其删除。这是contentEditable的经典 Bug。 - 默认模板未预置:
defaultContent.ts中的第一行仍使用红色纯文本占位符,没有使用smartField()函数生成智能控件。
修改文件清单
| 文件 | 修改类型 | 说明 |
|---|---|---|
src/pages/TemplateManage.tsx |
修改 | 优化 insertSmartField HTML(移除 、压缩为一行);增加 keydown 事件拦截,保护 .smart-field-wrapper 不被 Backspace 误删 |
src/utils/defaultContent.ts |
修改 | 将默认模板第一行的红色占位符替换为预置的智能字段控件 |
src/index.css |
修改(可选) | 给 .smart-field-wrapper 增加 white-space: nowrap |
具体代码变更
变更 1:src/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>
`;
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();
};
改动点:
- 移除末尾的
。 - 将多行模板字符串压缩为一行,消除源码换行被渲染为空格的问题。
- 在
field-value的内联样式中增加white-space: nowrap;。
变更 2:src/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思路一致。
变更 3:src/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 不带 (且是压缩的一行),defaultContent.ts 中的这段代码本身没有问题。
但:如果之前的 smartField() 定义末尾带有 ,则需要一并修正。当前 defaultContent.ts 中的 smartField 定义已经在上一版中被修正为压缩的一行且不带 ,所以默认模板本身已经符合要求。
确认结果:defaultContent.ts 中的第一行在上一版(2026-04-17-00-13-09)中已经替换为 smartField('patientName') 等智能控件。本次只需确保 smartField 辅助函数的定义与变更 1 保持一致(移除 、压缩为一行、增加 white-space: nowrap)即可。
变更 4:src/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增加此属性可作为双重保险。
风险点
| 风险 | 级别 | 应对措施 |
|---|---|---|
移除 后,字段与前/后文本之间没有间隔,显得拥挤 |
低 | margin: 0 2px 已经提供了 2px 的左右间距,视觉上足够紧凑 |
keydown 拦截可能影响编辑器其他正常删除操作 |
低 | 拦截逻辑严格限定在光标前一个节点为 .smart-field-wrapper 时才生效,其他情况正常放行 |
老模板中已插入的字段仍带有 |
低 | 老模板中的字段只是带有一个额外的空格,不影响功能;用户可手动删除重插 |
回滚策略
本次修改范围极小,仅调整 insertSmartField 的 HTML 输出和增加一个 keydown 事件监听。如出现异常,可直接 git revert 回滚。
⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。