Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-18-38-47.md
admin 0c57409c59 feat: TemplateManage field system upgrade and bidirectional navigation
- Fix new field type linkage (remove text option under single/multi/image).
- Add system fields: pre/post-op diagnosis, pathology checks, etc.
- Replace placeholder text in default template with smart fields.
- Accordion grouping and inline option editing in field management.
- Add image field type, asset library with logo preloading.
- Image source picker modal in ReportEditor (local/signature/asset).
- Editor-to-sidebar highlight and scroll navigation on smart field click.
2026-04-17 18:54:10 +08:00

9.2 KiB
Raw Blame History

实现方案 — 2026-04-17-18-38-47

变更文件

  1. src/types.ts
  2. src/utils/defaultContent.ts
  3. src/pages/TemplateManage.tsx
  4. src/pages/ReportEditor.tsx
  5. src/index.css

一、types.ts 修改

1.1 扩展 FieldType

export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date' | 'signature' | 'image';

1.2 更新 DEFAULT_FORM_FIELDS

在现有字段基础上追加/修改:

  • preoperativeDiagnosis(术前诊断,单选)
  • postoperativeDiagnosis(术后诊断,单选)
  • postOpCondition(手术后情况,单选,默认选项含"患者麻醉恢复后安返病房"
  • pathologyCheck(是否送病理检查,单选,选项["是","否"]
  • frozenPathology(冰冻病理结果,单选)
  • specimenDescription(切除标本描述,单选)
  • hospitalLogo医院Logo图片type: 'image',对应默认模板顶部 logo

所有新增诊断类字段默认 visibleInForm: true, isSystemLocked: true


二、defaultContent.ts 修改

将模板 HTML 中的静态占位文本替换为 smartField(...)

// 术前诊断
<strong>术前诊断</strong>${smartField('preoperativeDiagnosis')}

// 术后诊断
<strong>术后诊断</strong>${smartField('postoperativeDiagnosis')}

// 手术后情况
<strong>手术后情况</strong>${smartField('postOpCondition')}

// 切除标本描述
<strong>切除标本描述</strong>${smartField('specimenDescription')}

// 是否送病理检查
<strong>是否送病理检查</strong>${smartField('pathologyCheck')}

// 冰冻病理结果
<strong>冰冻病理结果</strong>${smartField('frozenPathology')}

// 手术者签名
手术者签名${smartField('surgeonSignature')}

// 医院 Logo 替换为图片字段占位符(使用 image-placeholder 结构但带 data-bind
// 保留原有居中样式

Logo 部分不再硬编码 <img src="/logo_square.png">,改为可管理的图片占位符:

<div class="image-placeholder" data-placeholder="true" data-bind="hospitalLogo" contenteditable="false">
  <span class="delete-btn" contenteditable="false">×</span>
  <p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p>
</div>

三、TemplateManage.tsx 修改

3.1 新增字段表单修复(需求 1

在 category onChange 中:

  • 选择"单选" → type 强制设为 single_select
  • 选择"多选" → type 强制设为 multi_select
  • 选择"图片" → type 强制设为 image

在 type select 的 options 渲染中,移除单选/多选/图片下的"文本" option

<option value="text">文本</option>
{newFieldForm.category === '单选' && <option value="single_select">下拉单选</option>}
{newFieldForm.category === '多选' && <option value="multi_select">标签多选</option>}
{newFieldForm.category === '时间' && <><option value="date">日期</option><option value="time">时分</option></>}
{newFieldForm.category === '图片' && <option value="image">图片</option>}

3.2 字段管理折叠分组(需求 5

新增状态:

const [expandedCategories, setExpandedCategories] = useState<string[]>(['填空','单选','多选','时间','图片']);

将字段管理列表改为按 category 分组渲染。每组一个可点击标题栏,点击时 toggle 该 category 在 expandedCategories 中的存在性。展开的组内渲染对应字段列表。

3.3 字段管理点击编辑选项(需求 3

新增状态:

const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
const [editFieldOptions, setEditFieldOptions] = useState('');
const [editFieldLabel, setEditFieldLabel] = useState('');

在字段分组列表中,每个字段行增加 onClick

onClick={() => { setEditingFieldKey(field.key); setEditFieldOptions((field.options || []).join(', ')); setEditFieldLabel(field.label); }}

editingFieldKey === field.key 时,将该行替换为编辑表单:

  • 显示字段名 input仅非系统锁定字段可改 label系统字段只读展示
  • 选项 input逗号分隔
  • "保存"/"取消"按钮。

保存函数:

const saveFieldEdit = (key: string) => {
  const updated = formFields.map(f => {
    if (f.key !== key) return f;
    const next = { ...f, options: ['单选','多选','图片'].includes(f.category) ? editFieldOptions.split(/[,]/).map(s => s.trim()).filter(Boolean) : f.options };
    if (!f.isSystemLocked) next.label = editFieldLabel.trim() || f.label;
    return next;
  });
  setFormFields(updated);
  storage.set('formFieldsConfig', updated);
  setEditingFieldKey(null);
};

3.4 编辑器点击联动侧边栏(需求 6、7

在现有的 handleEditorClick 事件监听中(已存在于 useEffect),增加非 delete-btn 的 smart-field-wrapper 点击处理:

const smartField = targetEl.closest('.smart-field-wrapper') as HTMLElement | null;
if (smartField) {
  const valueSpan = smartField.querySelector('.field-value');
  const fieldKey = valueSpan?.getAttribute('data-bind') || smartField.getAttribute('data-bind');
  if (fieldKey) {
    setActiveFieldKey(fieldKey);
    const field = formFields.find(f => f.key === fieldKey);
    if (field) {
      // 展开对应分组
      setExpandedCategories(prev => prev.includes(field.category) ? prev : [...prev, field.category]);
      // 滚动到可视区域
      setTimeout(() => {
        const el = document.getElementById(`sidebar-field-${fieldKey}`);
        el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 50);
    }
  }
}

新增状态 const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);

在"插入字段"Tab 的按钮上增加 id={sidebar-field-${field.key}} 和动态高亮类:

className={... + (activeFieldKey === field.key ? ' ring-2 ring-accent bg-blue-50 border-accent' : '')}

在"字段管理"Tab 的字段卡片上同样增加 id 和高亮边框。

3.5 素材管理(需求 4 的一部分)

在"字段管理"Tab 底部新增"素材库"折叠面板(或放在图片分组下方)。

新增状态:

const [imageAssets, setImageAssets] = useState<{id: string, name: string, dataUrl: string}[]>([]);

初始化时从 storage.get('imageAssets', []) 读取。若为空且存在 /logo_square.png,则通过 fetch('/logo_square.png') -> blob -> FileReader 将其转为 Base64 并作为默认素材 hospital-logo 存入。

提供本地上传按钮:选择图片后用 Canvas 压缩max 500px转 Base64追加到 imageAssets 并保存 storage.set('imageAssets', ...)


四、ReportEditor.tsx 修改

4.1 图片来源选择弹窗(需求 4

新增状态:

const [imagePickerOpen, setImagePickerOpen] = useState(false);
const [imagePickerTarget, setImagePickerTarget] = useState<HTMLElement | null>(null);
const [imageAssets, setImageAssets] = useState<{id: string, name: string, dataUrl: string}[]>([]);

修改 triggerPlaceholderUpload 的调用逻辑:当点击无图片的 image-placeholder 时,不再直接 input.click(),而是:

setImagePickerTarget(placeholder);
setImagePickerOpen(true);

弹窗 JSXModal包含三个 Tab 按钮:

  • 本地上传:内部隐藏 <input type="file" accept="image/*">,点击"选择文件"触发,读取后填充 placeholder。
  • 我的签名:若 currentUser.signature 存在,展示签名缩略图,点击后填充。
  • 系统素材:读取 imageAssets 列表,展示缩略图网格,点击后填充。

填充函数:

const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
  placeholder.innerHTML = `<span class="delete-btn" contenteditable="false">×</span><img src="${src}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">`;
  placeholder.classList.add('has-image');
  if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
  saveDraftToStorage();
};

4.2 图片字段在 TemplateManage 中的插入

TemplateManage.tsxinsertSmartField 中,对 type === 'image' 的字段,不再插入 span.smart-field-wrapper,而是插入 image-placeholder

if (field.type === 'image') {
  const id = 'ph_' + Date.now();
  const html = `<div id="${id}" class="image-placeholder" data-placeholder="true" data-bind="${field.key}" contenteditable="false"><span class="delete-btn" contenteditable="false">×</span><p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p></div>`;
  // 同样的 Range.insertNode 逻辑插入 html
}

五、index.css 修改

新增/微调以下样式:

  1. .accordion-header:字段管理分组标题样式(可复用现有按钮类)。
  2. .accordion-body:分组内容过渡动画(可选)。
  3. .sidebar-field-active:高亮边框/背景色。
  4. 图片选择弹窗遮罩与内容卡片样式(可复用现有 Modal 样式)。

回滚策略

修改前 git 仓库已处于干净状态(最新提交 b155dd4)。若验证失败,可执行 git reset --hard b155dd4 回滚。

无新增 npm 依赖

所有改动均利用现有 React + Tailwind 能力完成。