Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-16-22-23-02.md

8.7 KiB
Raw Blame History

实现方案 — 2026-04-16-22-23-02

根因分析

当前 TemplateManage.tsxReportEditor.tsx 均使用原生 contentEditable 实现富文本编辑,但模板中的占位符是纯 HTML姓名:<span style="color: #ff0000;">*姓名*</span>),存在以下问题:

  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 修改(可选) 定义字段映射常量数组,供两端复用

具体代码变更

变更 1src/types.ts — 定义字段库常量

新增内容(在文件末尾追加):

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: '手术经过' },
];

变更 2src/index.css — 智能占位控件样式

@layer components 或文件末尾新增:

.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;
  }
}

变更 3src/pages/TemplateManage.tsx — 字段库面板与插入逻辑

当前结构: TemplateManage.tsx 右侧通常为操作按钮区(如保存、预览)。

新增字段库面板(放在保存按钮下方或单独区域):

import { BINDABLE_FIELDS } from '../types';

// 在组件内新增辅助函数
const insertSmartField = (field: typeof BINDABLE_FIELDS[0]) => {
  const html = `
    <span class="smart-field-wrapper" contenteditable="false">
      <span class="field-label">${field.label}</span>
      <span class="field-value"
            data-bind="${field.key}"
            contenteditable="true"
            style="min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a;">
      </span>
    </span>&nbsp;
  `;
  document.execCommand('insertHTML', false, html);
};

UI 位置(在保存按钮下方新增卡片):

<div className="card-minimal mt-4">
  <h3 className="text-sm font-semibold text-primary mb-2">表单字段库</h3>
  <div className="flex flex-wrap gap-2">
    {BINDABLE_FIELDS.map((field) => (
      <button
        key={field.key}
        type="button"
        onClick={() => insertSmartField(field)}
        className="px-2 py-1 text-xs bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
        title={`插入 ${field.label}`}
      >
        {field.label}
      </button>
    ))}
  </div>
  <p className="text-[10px] text-slate-400 mt-2">点击字段插入智能占位方格,Label 锁定,Value 可输入。</p>
</div>

注意:TemplateManage.tsx 的具体行号需以实际文件为准,但插入逻辑与 UI 结构如上。

变更 4src/pages/ReportEditor.tsx — 双向绑定逻辑

4.1 富文本 → 表单(handleEditorInputonInput 事件)

当前代码ReportEditor.tsx 的编辑器通常已有 onInput 处理(保存草稿)。

在原有 onInput 处理器中追加:

const handleEditorInput = (e: React.FormEvent<HTMLDivElement>) => {
  // 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

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 光标/焦点保护(边界处理)

为避免 useEffectreportData 变化时与用户的输入冲突,上述逻辑已通过 if (el.innerText !== newValue) 做短路保护。若用户当前正在该方格内输入(此时 reportData 已由 handleEditorInput 同步更新),innerText 通常等于 newValue,不会触发 DOM 重写,因此光标不会跳动。

变更 5src/utils/print.ts — 打印样式适配

当前 print.ts 会将 HTML 内容包裹后打印。

在注入的 <style> 中追加:

@media print {
  /* 现有打印样式保留 ... */

  .smart-field-wrapper .field-value {
    border: none !important;
    border-bottom: 1px solid #000 !important;
    border-radius: 0 !important;
    background: transparent !important;
    padding: 0 2px !important;
  }
}

print.ts 本身会读取 index.css 中的打印样式,可确认是否重复。保险起见,两边同时维护或仅在 index.css 维护即可。此处优先在 index.css 中维护,print.ts 若已内联样式则同步追加。


风险点

风险 级别 应对措施
光标跳动/输入中断 useEffect 同步时严格判断 innerText !== newValue,仅在差异时重写 DOM
contenteditable="false" 外层导致整个控件无法删除 这是预期行为,用户可通过选中整个控件后按 Delete 删除;若需要允许删除,可在外层增加 tabindex 或删除按钮
数组字段surgeon同步时格式异常 useEffect 中加入 Array.isArray(rawValue) 分支,统一用 join(', ')
老模板未自动升级,用户反馈"联动不生效" TemplateManage 页面增加提示文案:"请重新编辑模板并插入字段库控件以激活联动"

回滚策略

本次改动仅涉及前端 UI 和 DOM 事件处理,不修改数据结构和存储接口。如出现异常,可直接执行以下任一方式:

  1. git revert 撤销相关提交;
  2. 手动注释掉 ReportEditor.tsx 中的 useEffect 双向绑定逻辑和 handleEditorInput 的新增分支,保留原有草稿保存逻辑即可恢复。

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