# 实现方案 — 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 能力完成。