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

163 lines
7.5 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-09-36-07
## 根因分析
1. **多余空格**`TemplateManage.tsx``insertSmartField` 函数在 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` |
---
## 具体代码变更
### 变更 1`src/pages/TemplateManage.tsx` — 优化插入 HTML
**当前代码(约第 159-173 行):**
```tsx
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();
};
```
**修改为:**
```tsx
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;`
### 变更 2`src/pages/TemplateManage.tsx` — 拦截 Backspace/Delete 防止误删整行
在现有的 `useEffect`(用于监听编辑器 click 事件)附近,新增一个 `useEffect` 监听 `keydown`
```tsx
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 行):**
```html
<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`)即可。**
### 变更 4`src/index.css` — 增加 `white-space: nowrap`
`.smart-field-wrapper` 的样式中增加:
```css
.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` 回滚。
---
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。**