# 实现方案 — 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 ```typescript 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(...)`: ```javascript // 术前诊断 术前诊断:${smartField('preoperativeDiagnosis')} // 术后诊断 术后诊断:${smartField('postoperativeDiagnosis')} // 手术后情况 手术后情况:${smartField('postOpCondition')} // 切除标本描述 切除标本描述:${smartField('specimenDescription')} // 是否送病理检查 是否送病理检查:${smartField('pathologyCheck')} // 冰冻病理结果 冰冻病理结果:${smartField('frozenPathology')} // 手术者签名 手术者签名:${smartField('surgeonSignature')} // 医院 Logo 替换为图片字段占位符(使用 image-placeholder 结构但带 data-bind) // 保留原有居中样式 ``` Logo 部分不再硬编码 ``,改为可管理的图片占位符: ```html
×

插入/点击放置图片

``` --- ## 三、TemplateManage.tsx 修改 ### 3.1 新增字段表单修复(需求 1) 在 category `onChange` 中: - 选择"单选" → `type` 强制设为 `single_select` - 选择"多选" → `type` 强制设为 `multi_select` - 选择"图片" → `type` 强制设为 `image` 在 type select 的 options 渲染中,移除单选/多选/图片下的"文本" option: ```tsx {newFieldForm.category === '单选' && } {newFieldForm.category === '多选' && } {newFieldForm.category === '时间' && <>} {newFieldForm.category === '图片' && } ``` ### 3.2 字段管理折叠分组(需求 5) 新增状态: ```tsx const [expandedCategories, setExpandedCategories] = useState(['填空','单选','多选','时间','图片']); ``` 将字段管理列表改为按 category 分组渲染。每组一个可点击标题栏,点击时 toggle 该 category 在 `expandedCategories` 中的存在性。展开的组内渲染对应字段列表。 ### 3.3 字段管理点击编辑选项(需求 3) 新增状态: ```tsx const [editingFieldKey, setEditingFieldKey] = useState(null); const [editFieldOptions, setEditFieldOptions] = useState(''); const [editFieldLabel, setEditFieldLabel] = useState(''); ``` 在字段分组列表中,每个字段行增加 `onClick`: ```tsx onClick={() => { setEditingFieldKey(field.key); setEditFieldOptions((field.options || []).join(', ')); setEditFieldLabel(field.label); }} ``` 当 `editingFieldKey === field.key` 时,将该行替换为编辑表单: - 显示字段名 input(仅非系统锁定字段可改 label,系统字段只读展示)。 - 选项 input(逗号分隔)。 - "保存"/"取消"按钮。 保存函数: ```tsx 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` 点击处理: ```typescript 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(null);`。 在"插入字段"Tab 的按钮上增加 `id={`sidebar-field-${field.key}`}` 和动态高亮类: ```tsx className={... + (activeFieldKey === field.key ? ' ring-2 ring-accent bg-blue-50 border-accent' : '')} ``` 在"字段管理"Tab 的字段卡片上同样增加 `id` 和高亮边框。 ### 3.5 素材管理(需求 4 的一部分) 在"字段管理"Tab 底部新增"素材库"折叠面板(或放在图片分组下方)。 新增状态: ```tsx 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) 新增状态: ```tsx const [imagePickerOpen, setImagePickerOpen] = useState(false); const [imagePickerTarget, setImagePickerTarget] = useState(null); const [imageAssets, setImageAssets] = useState<{id: string, name: string, dataUrl: string}[]>([]); ``` 修改 `triggerPlaceholderUpload` 的调用逻辑:当点击无图片的 `image-placeholder` 时,不再直接 `input.click()`,而是: ```tsx setImagePickerTarget(placeholder); setImagePickerOpen(true); ``` 弹窗 JSX(Modal)包含三个 Tab 按钮: - **本地上传**:内部隐藏 ``,点击"选择文件"触发,读取后填充 placeholder。 - **我的签名**:若 `currentUser.signature` 存在,展示签名缩略图,点击后填充。 - **系统素材**:读取 `imageAssets` 列表,展示缩略图网格,点击后填充。 填充函数: ```tsx const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => { placeholder.innerHTML = `×`; placeholder.classList.add('has-image'); if (editorRef.current) contentRef.current = editorRef.current.innerHTML; saveDraftToStorage(); }; ``` ### 4.2 图片字段在 TemplateManage 中的插入 在 `TemplateManage.tsx` 的 `insertSmartField` 中,对 `type === 'image'` 的字段,不再插入 `span.smart-field-wrapper`,而是插入 `image-placeholder`: ```tsx if (field.type === 'image') { const id = 'ph_' + Date.now(); const html = `
×

插入/点击放置图片

`; // 同样的 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 能力完成。