- 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.
239 lines
9.2 KiB
Markdown
239 lines
9.2 KiB
Markdown
# 实现方案 — 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
|
||
// 术前诊断
|
||
<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">`,改为可管理的图片占位符:
|
||
```html
|
||
<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:
|
||
```tsx
|
||
<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)
|
||
新增状态:
|
||
```tsx
|
||
const [expandedCategories, setExpandedCategories] = useState<string[]>(['填空','单选','多选','时间','图片']);
|
||
```
|
||
|
||
将字段管理列表改为按 category 分组渲染。每组一个可点击标题栏,点击时 toggle 该 category 在 `expandedCategories` 中的存在性。展开的组内渲染对应字段列表。
|
||
|
||
### 3.3 字段管理点击编辑选项(需求 3)
|
||
新增状态:
|
||
```tsx
|
||
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(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<string | null>(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<HTMLElement | null>(null);
|
||
const [imageAssets, setImageAssets] = useState<{id: string, name: string, dataUrl: string}[]>([]);
|
||
```
|
||
|
||
修改 `triggerPlaceholderUpload` 的调用逻辑:当点击无图片的 `image-placeholder` 时,不再直接 `input.click()`,而是:
|
||
```tsx
|
||
setImagePickerTarget(placeholder);
|
||
setImagePickerOpen(true);
|
||
```
|
||
|
||
弹窗 JSX(Modal)包含三个 Tab 按钮:
|
||
- **本地上传**:内部隐藏 `<input type="file" accept="image/*">`,点击"选择文件"触发,读取后填充 placeholder。
|
||
- **我的签名**:若 `currentUser.signature` 存在,展示签名缩略图,点击后填充。
|
||
- **系统素材**:读取 `imageAssets` 列表,展示缩略图网格,点击后填充。
|
||
|
||
填充函数:
|
||
```tsx
|
||
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.tsx` 的 `insertSmartField` 中,对 `type === 'image'` 的字段,不再插入 `span.smart-field-wrapper`,而是插入 `image-placeholder`:
|
||
```tsx
|
||
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 能力完成。
|