- 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.
9.2 KiB
实现方案 — 2026-04-17-18-38-47
变更文件
src/types.tssrc/utils/defaultContent.tssrc/pages/TemplateManage.tsxsrc/pages/ReportEditor.tsxsrc/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);
弹窗 JSX(Modal)包含三个 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.tsx 的 insertSmartField 中,对 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 修改
新增/微调以下样式:
.accordion-header:字段管理分组标题样式(可复用现有按钮类)。.accordion-body:分组内容过渡动画(可选)。.sidebar-field-active:高亮边框/背景色。- 图片选择弹窗遮罩与内容卡片样式(可复用现有 Modal 样式)。
回滚策略
修改前 git 仓库已处于干净状态(最新提交 b155dd4)。若验证失败,可执行 git reset --hard b155dd4 回滚。
无新增 npm 依赖
所有改动均利用现有 React + Tailwind 能力完成。