# 实现方案 — 2026-04-16-22-23-02 ## 根因分析 当前 `TemplateManage.tsx` 和 `ReportEditor.tsx` 均使用原生 `contentEditable` 实现富文本编辑,但模板中的占位符是纯 HTML(如 `姓名:*姓名*`),存在以下问题: 1. **固定文本无保护**:"姓名:" 等标签与普通文本无异,用户可随意删除或篡改。 2. **无双向绑定**:模板中的占位符与右侧表单之间没有数据通道,模板内容不会随表单变化,表单也不会随模板输入自动填充。 3. **打印样式混乱**:现有的红色占位文本在打印报告中显得不专业。 ## 修改文件清单 | 文件 | 修改类型 | 说明 | |------|---------|------| | `src/pages/TemplateManage.tsx` | 修改 | 新增右侧"字段库"面板,支持点击插入智能占位控件 | | `src/pages/ReportEditor.tsx` | 修改 | 新增 `data-bind` DOM 的双向监听与同步逻辑 | | `src/utils/print.ts` | 修改 | 打印样式中增加 `.field-value` 的打印适配 | | `src/index.css` | 修改 | 新增 `.smart-field-wrapper` 系列样式 | | `src/types.ts` | 修改(可选) | 定义字段映射常量数组,供两端复用 | --- ## 具体代码变更 ### 变更 1:`src/types.ts` — 定义字段库常量 **新增内容(在文件末尾追加):** ```typescript export interface BindableField { key: string; label: string; } export const BINDABLE_FIELDS: BindableField[] = [ { key: 'patientName', label: '姓名' }, { key: 'gender', label: '性别' }, { key: 'age', label: '年龄' }, { key: 'hospitalId', label: '住院号' }, { key: 'bedNumber', label: '床号' }, { key: 'surgeryDate', label: '手术日期' }, { key: 'surgeryType', label: '手术类型' }, { key: 'surgeon', label: '手术者' }, { key: 'assistant', label: '助手' }, { key: 'anesthesiaType', label: '麻醉方式' }, { key: 'preoperativeDiagnosis', label: '术前诊断' }, { key: 'intraoperativeDiagnosis', label: '术中诊断' }, { key: 'surgicalProcedure', label: '手术经过' }, ]; ``` ### 变更 2:`src/index.css` — 智能占位控件样式 **在 `@layer components` 或文件末尾新增:** ```css .smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 4px; vertical-align: middle; } .smart-field-wrapper .field-label { color: #64748b; user-select: none; } .smart-field-wrapper .field-value { min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a; outline: none; } .smart-field-wrapper .field-value:empty::before { content: '\200b'; /* zero-width space to keep min-height */ } @media print { .smart-field-wrapper .field-value { border: none !important; border-bottom: 1px solid #000 !important; border-radius: 0 !important; background: transparent !important; } } ``` ### 变更 3:`src/pages/TemplateManage.tsx` — 字段库面板与插入逻辑 **当前结构:** `TemplateManage.tsx` 右侧通常为操作按钮区(如保存、预览)。 **新增字段库面板(放在保存按钮下方或单独区域):** ```tsx import { BINDABLE_FIELDS } from '../types'; // 在组件内新增辅助函数 const insertSmartField = (field: typeof BINDABLE_FIELDS[0]) => { const html = ` ${field.label}:   `; document.execCommand('insertHTML', false, html); }; ``` **UI 位置(在保存按钮下方新增卡片):** ```tsx

表单字段库

{BINDABLE_FIELDS.map((field) => ( ))}

点击字段插入智能占位方格,Label 锁定,Value 可输入。

``` > 注意:`TemplateManage.tsx` 的具体行号需以实际文件为准,但插入逻辑与 UI 结构如上。 ### 变更 4:`src/pages/ReportEditor.tsx` — 双向绑定逻辑 #### 4.1 富文本 → 表单(`handleEditorInput` 或 `onInput` 事件) **当前代码**中 `ReportEditor.tsx` 的编辑器通常已有 `onInput` 处理(保存草稿)。 **在原有 `onInput` 处理器中追加:** ```tsx const handleEditorInput = (e: React.FormEvent) => { // 1. 原有逻辑:同步 contentRef 并保存草稿 if (editorRef.current) { contentRef.current = editorRef.current.innerHTML; } saveDraftToStorage(); // 2. 新增:双向绑定 — 方格内容变更时更新表单 State const target = e.target as HTMLElement; if (target && target.hasAttribute('data-bind')) { const fieldKey = target.getAttribute('data-bind')!; const newValue = target.innerText; setReportData((prev) => { const next = { ...prev, [fieldKey]: newValue }; // 同步 stateRef stateRef.current.reportData = next; return next; }); } }; ``` #### 4.2 表单 → 富文本(`useEffect` 监听 `reportData`) **在 `ReportEditor.tsx` 中新增一个 `useEffect`:** ```tsx useEffect(() => { if (!editorRef.current) return; const bindNodes = editorRef.current.querySelectorAll('[data-bind]'); bindNodes.forEach((node) => { const el = node as HTMLElement; const fieldKey = el.getAttribute('data-bind')!; const rawValue = (reportData as any)[fieldKey]; // 处理数组类型(如 surgeon / assistant) let newValue = ''; if (Array.isArray(rawValue)) { newValue = rawValue.join(', '); } else if (rawValue !== undefined && rawValue !== null) { newValue = String(rawValue); } // 仅在差异时更新 DOM,防止光标跳动 if (el.innerText !== newValue) { el.innerText = newValue; } }); }, [reportData]); ``` #### 4.3 光标/焦点保护(边界处理) 为避免 `useEffect` 在 `reportData` 变化时与用户的输入冲突,上述逻辑已通过 `if (el.innerText !== newValue)` 做短路保护。若用户当前正在该方格内输入(此时 `reportData` 已由 `handleEditorInput` 同步更新),`innerText` 通常等于 `newValue`,不会触发 DOM 重写,因此光标不会跳动。 ### 变更 5:`src/utils/print.ts` — 打印样式适配 **当前 `print.ts` 会将 HTML 内容包裹后打印。** **在注入的 `