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)
This commit is contained in:
@@ -102,6 +102,7 @@
|
||||
align-items: center;
|
||||
margin: 0 2px;
|
||||
vertical-align: text-bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.smart-field-wrapper .field-label {
|
||||
color: #64748b;
|
||||
|
||||
@@ -162,6 +162,53 @@ export default function TemplateManage() {
|
||||
};
|
||||
}, [currentTemplateId, currentUser]);
|
||||
|
||||
// Intercept Backspace/Delete next to smart fields to avoid whole-line deletion
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Backspace' && e.key !== 'Delete') return;
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
const node = range.startContainer;
|
||||
if (node.nodeType !== Node.TEXT_NODE) return;
|
||||
const offset = range.startOffset;
|
||||
|
||||
if (e.key === 'Backspace' && offset === 0) {
|
||||
const prev = node.previousSibling;
|
||||
if (prev && prev.nodeType === Node.ELEMENT_NODE && (prev as Element).classList?.contains('smart-field-wrapper')) {
|
||||
e.preventDefault();
|
||||
prev.remove();
|
||||
const allTemplates = storage.get<Template[]>('templates', []);
|
||||
const updated = allTemplates.map(t =>
|
||||
t.id === currentTemplateId ? { ...t, content: editorRef.current!.innerHTML, updatedAt: new Date().toISOString() } : t
|
||||
);
|
||||
setTemplates(prevTemplates => prevTemplates.map(t => updated.find(u => u.id === t.id) || t));
|
||||
storage.set('templates', updated);
|
||||
}
|
||||
} else if (e.key === 'Delete' && offset === (node.textContent?.length || 0)) {
|
||||
const next = node.nextSibling;
|
||||
if (next && next.nodeType === Node.ELEMENT_NODE && (next as Element).classList?.contains('smart-field-wrapper')) {
|
||||
e.preventDefault();
|
||||
next.remove();
|
||||
const allTemplates = storage.get<Template[]>('templates', []);
|
||||
const updated = allTemplates.map(t =>
|
||||
t.id === currentTemplateId ? { ...t, content: editorRef.current!.innerHTML, updatedAt: new Date().toISOString() } : t
|
||||
);
|
||||
setTemplates(prevTemplates => prevTemplates.map(t => updated.find(u => u.id === t.id) || t));
|
||||
storage.set('templates', updated);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
editor.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => {
|
||||
editor.removeEventListener('keydown', handleKeyDown, true);
|
||||
};
|
||||
}, [currentTemplateId]);
|
||||
|
||||
const execCmd = (command: string, value: string | undefined = undefined) => {
|
||||
editorRef.current?.focus();
|
||||
document.execCommand(command, false, value);
|
||||
@@ -170,15 +217,7 @@ export default function TemplateManage() {
|
||||
|
||||
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>
|
||||
`;
|
||||
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;"><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();
|
||||
};
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
const smartField = (key: string) => `
|
||||
<span class="smart-field-wrapper" contenteditable="false">
|
||||
<span class="field-value"
|
||||
data-bind="${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>
|
||||
`;
|
||||
const smartField = (key: string) => `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;"><span class="field-value" data-bind="${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>`;
|
||||
|
||||
export const defaultReportContent = `
|
||||
<!-- 医院Logo -->
|
||||
|
||||
162
工程分析/实现方案-2026-04-17-09-36-07.md
Normal file
162
工程分析/实现方案-2026-04-17-09-36-07.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# 实现方案 — 2026-04-17-09-36-07
|
||||
|
||||
## 根因分析
|
||||
|
||||
1. **多余空格**:`TemplateManage.tsx` 的 `insertSmartField` 函数在 HTML 字符串末尾追加了 ` `,这是导致字段后跟随大量空白的主要原因。
|
||||
2. **异常换行**:`inline-block` 元素默认会在边界处根据容器宽度自动换行;`contenteditable="false"` 节点在行尾时,浏览器可能将其视为独立的渲染单元进行换行。
|
||||
3. **Backspace 误删整行**:当光标位于 `contenteditable="false"` 的内联元素之后时,Webkit/Blink 内核的默认行为无法正确删除该节点,而是向上寻找到父级 `<p>` 并将其删除。这是 `contentEditable` 的经典 Bug。
|
||||
4. **默认模板未预置**:`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 行):**
|
||||
```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>
|
||||
`;
|
||||
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();
|
||||
};
|
||||
```
|
||||
|
||||
**改动点**:
|
||||
- 移除末尾的 ` `。
|
||||
- 将多行模板字符串压缩为一行,消除源码换行被渲染为空格的问题。
|
||||
- 在 `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 不带 ` `(且是压缩的一行),`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` 的样式中增加:
|
||||
|
||||
```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` 增加此属性可作为双重保险。
|
||||
|
||||
## 风险点
|
||||
|
||||
| 风险 | 级别 | 应对措施 |
|
||||
|------|------|---------|
|
||||
| 移除 ` ` 后,字段与前/后文本之间没有间隔,显得拥挤 | 低 | `margin: 0 2px` 已经提供了 2px 的左右间距,视觉上足够紧凑 |
|
||||
| `keydown` 拦截可能影响编辑器其他正常删除操作 | 低 | 拦截逻辑严格限定在光标前一个节点为 `.smart-field-wrapper` 时才生效,其他情况正常放行 |
|
||||
| 老模板中已插入的字段仍带有 ` ` | 低 | 老模板中的字段只是带有一个额外的空格,不影响功能;用户可手动删除重插 |
|
||||
|
||||
## 回滚策略
|
||||
|
||||
本次修改范围极小,仅调整 `insertSmartField` 的 HTML 输出和增加一个 `keydown` 事件监听。如出现异常,可直接 `git revert` 回滚。
|
||||
|
||||
---
|
||||
|
||||
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。**
|
||||
77
工程分析/测试方案-2026-04-17-09-36-07.md
Normal file
77
工程分析/测试方案-2026-04-17-09-36-07.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 测试方案 — 2026-04-17-09-36-07
|
||||
|
||||
## 测试目标
|
||||
|
||||
验证 `TemplateManage` 中插入智能字段后的空格消除、Backspace 删除保护、异常换行修复,以及默认模板预置字段控件的正确性。
|
||||
|
||||
## 测试环境
|
||||
|
||||
- 浏览器:Chrome / Edge
|
||||
- 前置条件:已登录系统(建议使用 `admin` 超级管理员账号)
|
||||
- 测试页面:`/template-manage`
|
||||
|
||||
---
|
||||
|
||||
## 测试用例设计
|
||||
|
||||
### 用例 1:插入字段后无多余空格
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1.1 | 进入 `/template-manage` | 默认模板加载,第一行已预置姓名/性别/年龄等智能字段方格 |
|
||||
| 1.2 | 将光标定位到编辑器任意位置,点击字段库中的「手术名称」按钮 | 编辑器中插入一个蓝色边框的方格,**方格与后方文字之间没有明显的大片空白** |
|
||||
| 1.3 | 右键检查插入元素的 DOM | HTML 中没有 ` `,`smart-field-wrapper` 与前后文本节点紧密相连 |
|
||||
|
||||
### 用例 2:行尾插入字段不异常换行
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|--------- |
|
||||
| 2.1 | 在第一行"住院号:"的方格后点击,使光标位于行尾 | — |
|
||||
| 2.2 | 点击字段库插入「手术日期」 | 新插入的方格**紧跟在住院号方格后面**,不会跳到下一行(只要一行空间足够) |
|
||||
|
||||
### 用例 3:Backspace 删除字段不误删整行
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|--------- |
|
||||
| 3.1 | 在编辑器中插入一个「手术名称」字段 | 方格正常插入 |
|
||||
| 3.2 | 将光标定位到该方格的**紧右侧**(点击方格后方的文字前) | 光标闪烁在方格之后 |
|
||||
| 3.3 | 按下键盘 **Backspace** 键 | **仅删除该「手术名称」方格**,方格前方的文字(如"手术名称:")和整行 `<p>` **完好保留** |
|
||||
| 3.4 | 再次按 Backspace | 正常删除方格前方的文字字符(如冒号或文字),不会删行 |
|
||||
|
||||
### 用例 4:Delete 键同样受保护
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|--------- |
|
||||
| 4.1 | 将光标定位到「手术名称」方格的**紧左侧**(点击方格前方的文字后) | 光标闪烁在方格之前 |
|
||||
| 4.2 | 按下键盘 **Delete** 键 | **仅删除该「手术名称」方格**,整行内容保留 |
|
||||
|
||||
### 用例 5:默认模板预置字段验证
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|--------- |
|
||||
| 5.1 | 进入 `/template-manage`,观察默认模板第一行 | 姓名、性别、年龄、科别、床号、住院号后面**直接就是可填写的蓝色边框方格**,没有红色 `*姓名*` 纯文本占位符 |
|
||||
| 5.2 | 新建一个模板 | 新模板内容中也包含第一行的预置智能字段 |
|
||||
|
||||
### 用例 6:类型检查
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|--------- |
|
||||
| 6.1 | 在项目根目录执行 `npm run lint` | 无 TypeScript 编译错误 |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 插入字段后,方格与前后文字之间没有多余空格。
|
||||
- [ ] 行尾插入字段时,空间足够则不会异常跳到下一行。
|
||||
- [ ] 按 Backspace/Delete 删除字段时,仅删除该字段节点,不会误删整行。
|
||||
- [ ] 默认模板第一行已预置姓名、性别、年龄等智能字段方格。
|
||||
- [ ] `npm run lint` 通过。
|
||||
|
||||
## 测试方式
|
||||
|
||||
手工浏览器验证,结合 DevTools 观察 DOM 结构和键盘事件响应。
|
||||
|
||||
---
|
||||
|
||||
**⚠️ 请审核以上测试方案,确认无误后回复「确认」或提出修改意见,我将进入最终执行阶段。**
|
||||
26
工程分析/经验记录.md
26
工程分析/经验记录.md
@@ -120,7 +120,7 @@
|
||||
2. **UI 延迟原因**:`autoCaptureFrames` 是一个 async 函数,内部循环中连续调用 `setCapturedFrames`。由于 React 18 的自动批处理机制,在异步函数中连续的状态更新会被合并,DOM 重渲染被推迟到整个循环结束后才执行一次,导致用户看不到逐帧实时更新的效果。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. **修复数据丢失**:在 `ReportEditor.tsx` 初始化 `useEffect` 的 3 个数据恢复分支(draft 恢复已有报告、found 恢复已有报告、draft 恢复新建报告)中,将 `stateRef.current` 的同步赋值**移到 `editorRef.current/content` 判断条件的外部**,确保无论编辑器 DOM 是否已挂载、`content` 是否为空,`reportData`、`videos`、`capturedFrames` 都会立即写入 `stateRef.current`。
|
||||
1. **修复数据丢失**:在 `ReportEditor.tsx` 初始化 `useEffect` 的 3 个数据恢复分支(draft 恢复已有报告、found 恢复已有报告、draf t 恢复新建报告)中,将 `stateRef.current` 的同步赋值**移到 `editorRef.current/content` 判断条件的外部**,确保无论编辑器 DOM 是否已挂载、`content` 是否为空,`reportData`、`videos`、`capturedFrames` 都会立即写入 `stateRef.current`。
|
||||
2. **清理重复代码**:顺带移除了 `found` 恢复分支中 `contentRef.current = found.content;` 的重复赋值。
|
||||
3. **修复 UI 延迟**:在 `autoCaptureFrames` 的 for 循环中,将 `setCapturedFrames` 包裹在 `flushSync(() => { ... })` 中,强制每一帧被摘取后立即触发 DOM 更新,实现逐张实时显示和逐张插入 placeholder。
|
||||
|
||||
@@ -342,3 +342,27 @@ if ((settings.autoInsertDelay || 0) > 0) {
|
||||
- 对于需要将多个子字段映射到单一 UI 控件的场景,应在事件处理器和 `useEffect` 中各维护一层"拼接/解析"转换逻辑,保持底层数据结构不变。
|
||||
- 当表单字段超过 5 个且存在频繁变更需求时,应尽早从硬编码 JSX 转向"配置驱动渲染"(Config-Driven UI),降低后续维护成本。
|
||||
- 在 `contentEditable` 中插入 `inline-block` 元素时,务必通过 `line-height`、`vertical-align` 和最小化 `padding` 控制其对行高的影响,避免破坏段落排版的紧凑性。
|
||||
|
||||
---
|
||||
|
||||
## 记录 14:智能字段插入间距修复与 Backspace 防误删
|
||||
|
||||
**A. 具体问题**
|
||||
1. `TemplateManage.tsx` 中使用 `insertSmartField` 插入智能字段后,字段后方会出现一个可见的空格(由 ` ` 和多行模板字符串中的换行/缩进空白引起)。
|
||||
2. 在 `contenteditable` 中,光标位于 `<p>` 行首且后紧跟 `.smart-field-wrapper` 时按 Backspace,WebKit 内核会直接删除整段 `<p>` 而不是仅删除字段节点。
|
||||
3. `defaultContent.ts` 中的 `smartField` 辅助函数同样存在多行缩进导致的模板 HTML 中夹杂空白文本节点问题。
|
||||
|
||||
**B. 产生问题原因**
|
||||
1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 ` `,导致插入后字段与后续文字之间总有一个不必要的空格。
|
||||
2. `contenteditable="false"` 的 inline 元素处于行边界时,WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除,而不是只删除该不可编辑元素。
|
||||
3. `defaultContent.ts` 中的 `smartField` 为了可读性也使用了多行缩进模板字面量,导致默认模板里每个 `smartField` 调用前后都引入了额外的空白文本节点。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. **压缩 HTML 字符串**:将 `insertSmartField` 和 `defaultContent.ts` 的 `smartField` 输出改为单行 HTML,移除所有无意义的换行和缩进,并去掉尾部的 ` `。
|
||||
2. **防止内部折行**:给 `.smart-field-wrapper` 增加 `white-space: nowrap;`(内联样式 + CSS 类双保险),确保标签和输入框不会在行中间被拆开。
|
||||
3. **拦截 Backspace/Delete**:在 `TemplateManage.tsx` 的编辑器上增加 `keydown` 事件监听(capture 阶段)。当光标位于文本节点起始位置且前一个兄弟节点是 `.smart-field-wrapper` 时按 Backspace,或光标在文本节点末尾且后一个兄弟节点是 `.smart-field-wrapper` 时按 Delete,主动 `preventDefault()` 并手动移除该字段节点,随后同步更新 `localStorage` 中的模板内容。
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 在 `contentEditable` 中使用 `document.execCommand('insertHTML', ...)` 插入 HTML 时,**传入的字符串必须是无多余空白的紧凑单行**,否则浏览器会将其中的换行符解析为额外的文本节点,破坏排版和光标行为。
|
||||
- 对于 `contenteditable="false"` 的内联控件,若放置在块级边界(如 `<p>` 开头/结尾),务必增加键盘事件拦截,防止浏览器默认行为误删父级块。
|
||||
- 默认模板或任何通过代码生成的 HTML,应避免为了代码可读性而牺牲运行时 DOM 的纯净性;必要时在生成后对字符串进行 `.replace(/\s+/g, ' ').trim()` 处理。
|
||||
|
||||
45
工程分析/需求分析-2026-04-17-09-36-07.md
Normal file
45
工程分析/需求分析-2026-04-17-09-36-07.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 需求分析 — 2026-04-17-09-36-07
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
全面优化 `TemplateManage` 模板编辑器的交互体验,解决以下三个核心问题:
|
||||
1. 消除从右侧字段库插入智能字段后产生的多余空格与排版松散问题。
|
||||
2. 修复在行尾插入字段时出现的异常换行,以及按 Backspace 删除字段时误删整行的底层 Bug。
|
||||
3. 将常用基本信息字段(姓名、性别、年龄等)直接预置到系统默认模板中,实现开箱即用。
|
||||
|
||||
## 需求拆解
|
||||
|
||||
### 功能点
|
||||
|
||||
1. **消除插入字段后的多余空格**
|
||||
- 问题:`insertSmartField` 生成的 HTML 字符串末尾带有 ` `,且可能包含换行符,导致字段后跟随大量不可见空格。
|
||||
- 方案:移除 ` `,将 HTML 压缩为一行;调整 `margin` 为更小的值(如 `0 2px`)。
|
||||
|
||||
2. **修复异常换行与 Backspace 误删整行**
|
||||
- 问题 2a(异常换行):当在"住院号:"等行尾插入 `smart-field-wrapper` 后,即使空间足够,字段也可能被挤到下一行。这与 `inline-block` 的默认换行行为以及 `contenteditable="false"` 节点的边界处理有关。
|
||||
- 问题 2b(Backspace 误删):光标位于 `contenteditable="false"` 的字段节点之后时,浏览器内核(Webkit/Blink)无法正确选中该不可编辑节点,会向上寻址删除其父级 `<p>` 节点,导致整行被删。
|
||||
- 方案:
|
||||
- 给 `smart-field-wrapper` 增加 `white-space: nowrap`。
|
||||
- 在 `TemplateManage.tsx` 中增加 `keydown` 事件监听,拦截 Backspace/Delete,当光标紧挨着 `.smart-field-wrapper` 时,手动选中并删除该节点,阻止默认行为。
|
||||
|
||||
3. **默认模板预置字段控件**
|
||||
- 问题:当前 `defaultContent.ts` 中第一行是红色纯文本占位符(`*姓名* *性别*...`),用户需要手动在 `TemplateManage` 中逐个替换为智能字段。
|
||||
- 方案:修改 `defaultContent.ts`,将第一行的纯文本直接替换为 `smartField('patientName')` 等智能控件,使新建模板时即自带可联动的字段方格。
|
||||
|
||||
### 非功能点
|
||||
|
||||
- 不引入新的依赖。
|
||||
- 保持 `npm run lint` 通过。
|
||||
- 保持现有 `ReportEditor` 的双向绑定逻辑不受影响。
|
||||
|
||||
## 影响范围预估
|
||||
|
||||
| 模块 | 影响程度 | 说明 |
|
||||
|------|---------|------|
|
||||
| `src/pages/TemplateManage.tsx` | 高 | 修改 `insertSmartField` HTML 结构、增加 `keydown` 拦截逻辑保护字段节点 |
|
||||
| `src/utils/defaultContent.ts` | 中 | 默认模板第一行替换为预置的智能字段控件 |
|
||||
| `src/index.css` | 低 | 给 `.smart-field-wrapper` 增加 `white-space: nowrap`(可选) |
|
||||
|
||||
## 待确认问题
|
||||
|
||||
无。用户已提供详细的问题现象和解决方案思路,可直接进入实现方案。
|
||||
Reference in New Issue
Block a user