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.
This commit is contained in:
@@ -63,6 +63,9 @@ export default function ReportEditor() {
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
||||
const [formFields, setFormFields] = useState<FormField[]>([]);
|
||||
const [imagePickerOpen, setImagePickerOpen] = useState(false);
|
||||
const [imagePickerTarget, setImagePickerTarget] = useState<HTMLElement | null>(null);
|
||||
const [imageAssets, setImageAssets] = useState<{id: string; name: string; dataUrl: string}[]>([]);
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
@@ -118,6 +121,9 @@ export default function ReportEditor() {
|
||||
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
||||
}
|
||||
|
||||
const savedAssets = storage.get<{id: string; name: string; dataUrl: string}[]>('imageAssets', []);
|
||||
setImageAssets(savedAssets);
|
||||
|
||||
const allTemplates = storage.get<Template[]>('templates', []);
|
||||
const visibleTplIds = Array.isArray(user.visibleTemplates) ? user.visibleTemplates : allTemplates.map(t => t.id);
|
||||
const filteredTemplates = allTemplates.filter(t => visibleTplIds.includes(t.id));
|
||||
@@ -320,7 +326,8 @@ export default function ReportEditor() {
|
||||
if (!placeholder.classList.contains('has-image')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerPlaceholderUpload(placeholder);
|
||||
setImagePickerTarget(placeholder);
|
||||
setImagePickerOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -374,6 +381,16 @@ export default function ReportEditor() {
|
||||
return () => editor.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
const execCmd = (command: string, value: string | undefined = undefined) => {
|
||||
editorRef.current?.focus();
|
||||
document.execCommand(command, false, value);
|
||||
@@ -1498,6 +1515,67 @@ export default function ReportEditor() {
|
||||
</div>
|
||||
</div>
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
|
||||
{imagePickerOpen && imagePickerTarget && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl p-6 w-full max-w-[360px] shadow-2xl border border-border">
|
||||
<h3 className="text-lg font-bold text-text-main mb-4">选择图片来源</h3>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = (ev) => {
|
||||
const file = (ev.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const src = event.target?.result as string;
|
||||
fillPlaceholderSrc(imagePickerTarget, src);
|
||||
setImagePickerOpen(false);
|
||||
setImagePickerTarget(null);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
className="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded text-sm font-semibold"
|
||||
>本地上传</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentUser?.signature) {
|
||||
fillPlaceholderSrc(imagePickerTarget, currentUser.signature);
|
||||
}
|
||||
setImagePickerOpen(false);
|
||||
setImagePickerTarget(null);
|
||||
}}
|
||||
disabled={!currentUser?.signature}
|
||||
className={`w-full py-2 rounded text-sm font-semibold ${currentUser?.signature ? 'bg-slate-100 hover:bg-slate-200 text-slate-700' : 'bg-slate-50 text-slate-400 cursor-not-allowed'}`}
|
||||
>我的签名 {!currentUser?.signature && '(未上传)'}</button>
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-2">系统素材</div>
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
{imageAssets.map(asset => (
|
||||
<button
|
||||
key={asset.id}
|
||||
onClick={() => { fillPlaceholderSrc(imagePickerTarget, asset.dataUrl); setImagePickerOpen(false); setImagePickerTarget(null); }}
|
||||
className="w-16 h-16 border rounded overflow-hidden hover:ring-2 ring-accent"
|
||||
>
|
||||
<img src={asset.dataUrl} alt={asset.name} className="w-full h-full object-cover" />
|
||||
</button>
|
||||
))}
|
||||
{imageAssets.length === 0 && <div className="text-xs text-slate-400">暂无素材</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button onClick={() => { setImagePickerOpen(false); setImagePickerTarget(null); }} className="px-4 py-2 bg-slate-100 text-slate-600 rounded text-sm">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ export default function TemplateManage() {
|
||||
const [formFields, setFormFields] = useState<FormField[]>([]);
|
||||
const [newFieldForm, setNewFieldForm] = useState({ label: '', category: '填空', type: 'text' as FieldType });
|
||||
const [newFieldOptions, setNewFieldOptions] = useState('');
|
||||
const [expandedCategories, setExpandedCategories] = useState<string[]>(['填空', '单选', '多选', '时间', '图片']);
|
||||
const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);
|
||||
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
|
||||
const [editFieldLabel, setEditFieldLabel] = useState('');
|
||||
const [editFieldOptions, setEditFieldOptions] = useState('');
|
||||
const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]);
|
||||
|
||||
const updatePageHeight = () => {
|
||||
if (!editorRef.current) return;
|
||||
@@ -50,6 +56,25 @@ export default function TemplateManage() {
|
||||
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
||||
}
|
||||
|
||||
const savedAssets = storage.get<{ id: string; name: string; dataUrl: string }[]>('imageAssets', []);
|
||||
if (savedAssets.length > 0) {
|
||||
setImageAssets(savedAssets);
|
||||
} else {
|
||||
fetch('/logo_square.png')
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const dataUrl = reader.result as string;
|
||||
const initialAssets = [{ id: 'asset_logo', name: '医院Logo', dataUrl }];
|
||||
setImageAssets(initialAssets);
|
||||
storage.set('imageAssets', initialAssets);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch(() => setImageAssets([]));
|
||||
}
|
||||
|
||||
const savedTemplates = storage.get<Template[]>('templates', []);
|
||||
if (savedTemplates.length === 0) {
|
||||
const initial: Template = {
|
||||
@@ -117,7 +142,6 @@ export default function TemplateManage() {
|
||||
// Handle image placeholder and smart field delete interactions via click capture
|
||||
useEffect(() => {
|
||||
const handleEditorClick = (e: MouseEvent) => {
|
||||
// e.target may be a text node; safely resolve to an Element
|
||||
let node: Node | null = e.target as Node;
|
||||
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
||||
const targetEl = node as HTMLElement | null;
|
||||
@@ -138,6 +162,23 @@ export default function TemplateManage() {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = targetEl.closest('.image-placeholder') as HTMLElement | null;
|
||||
if (!placeholder) return;
|
||||
|
||||
@@ -178,7 +219,7 @@ export default function TemplateManage() {
|
||||
editor.removeEventListener('click', handleEditorClick, true);
|
||||
}
|
||||
};
|
||||
}, [currentTemplateId, currentUser]);
|
||||
}, [currentTemplateId, currentUser, formFields]);
|
||||
|
||||
// Intercept Backspace/Delete next to smart fields to avoid whole-line deletion
|
||||
useEffect(() => {
|
||||
@@ -222,7 +263,6 @@ export default function TemplateManage() {
|
||||
}
|
||||
}
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Cursor is directly inside a block element (e.g. <p>) at a boundary
|
||||
const el = node as Element;
|
||||
if (e.key === 'Backspace' && offset > 0) {
|
||||
const prev = el.childNodes[offset - 1];
|
||||
@@ -318,12 +358,19 @@ export default function TemplateManage() {
|
||||
const insertSmartField = (field: FormField) => {
|
||||
editorRef.current?.focus();
|
||||
restoreSelection();
|
||||
if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
|
||||
if (field.type !== 'image' && editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
|
||||
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
|
||||
return;
|
||||
}
|
||||
pushHistory();
|
||||
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${field.key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>​`;
|
||||
|
||||
let html = '';
|
||||
if (field.type === 'image') {
|
||||
const id = 'ph_' + Date.now();
|
||||
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" data-bind="${field.key}" contenteditable="false" style="display:inline-block;vertical-align:middle;"><span class="delete-btn" contenteditable="false">×</span><p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p></div>​`;
|
||||
} else {
|
||||
html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${field.key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>​`;
|
||||
}
|
||||
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
@@ -377,6 +424,23 @@ export default function TemplateManage() {
|
||||
storage.set('formFieldsConfig', updated);
|
||||
};
|
||||
|
||||
const saveFieldEdit = (key: string) => {
|
||||
const updated = formFields.map(f => {
|
||||
if (f.key !== key) return f;
|
||||
const next: FormField = { ...f };
|
||||
if (!f.isSystemLocked) {
|
||||
next.label = editFieldLabel.trim() || f.label;
|
||||
}
|
||||
if (['单选', '多选', '图片'].includes(f.category)) {
|
||||
next.options = editFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setFormFields(updated);
|
||||
storage.set('formFieldsConfig', updated);
|
||||
setEditingFieldKey(null);
|
||||
};
|
||||
|
||||
const addField = () => {
|
||||
if (!newFieldForm.label.trim()) return;
|
||||
const key = 'custom_' + Date.now();
|
||||
@@ -398,6 +462,21 @@ export default function TemplateManage() {
|
||||
setNewFieldOptions('');
|
||||
};
|
||||
|
||||
const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const dataUrl = event.target?.result as string;
|
||||
const asset = { id: 'asset_' + Date.now(), name: file.name, dataUrl };
|
||||
const updated = [...imageAssets, asset];
|
||||
setImageAssets(updated);
|
||||
storage.set('imageAssets', updated);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const insertTable = () => {
|
||||
const rowsStr = prompt('请输入行数:', '2');
|
||||
const colsStr = prompt('请输入列数:', '3');
|
||||
@@ -504,7 +583,6 @@ export default function TemplateManage() {
|
||||
storage.set('templates', updated);
|
||||
setCurrentTemplateId(newTpl.id);
|
||||
|
||||
// Sync user permissions
|
||||
const savedUsers = storage.get<User[]>('users', []);
|
||||
let updatedUsers = savedUsers;
|
||||
if (currentUser?.role === 'super') {
|
||||
@@ -730,12 +808,13 @@ export default function TemplateManage() {
|
||||
{catFields.map(field => (
|
||||
<button
|
||||
key={field.key}
|
||||
id={`sidebar-field-${field.key}`}
|
||||
type="button"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => insertSmartField(field)}
|
||||
onMouseEnter={() => highlightField(field.key, true)}
|
||||
onMouseLeave={() => highlightField(field.key, false)}
|
||||
className="px-2 py-1 text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
|
||||
className={`px-2 py-1 text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors ${activeFieldKey === field.key ? 'ring-2 ring-accent bg-blue-50 border-accent' : ''}`}
|
||||
title={`插入 ${field.label}`}
|
||||
>
|
||||
{field.label}
|
||||
@@ -750,25 +829,121 @@ export default function TemplateManage() {
|
||||
|
||||
{fieldLibTab === 'manage' && (
|
||||
<div className="space-y-3">
|
||||
{formFields.filter(f => !f.isSystemLocked).map(field => (
|
||||
<div key={field.key} className="flex items-center justify-between p-2 bg-slate-50 rounded border border-slate-200">
|
||||
<div className="text-xs">
|
||||
<div className="font-medium text-text-main">{field.label}</div>
|
||||
<div className="text-[10px] text-slate-400">{field.category} · {field.type}</div>
|
||||
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
|
||||
const catFields = formFields.filter(f => f.category === cat);
|
||||
if (catFields.length === 0) return null;
|
||||
const expanded = expandedCategories.includes(cat);
|
||||
return (
|
||||
<div key={cat} className="border border-slate-200 rounded overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedCategories(prev => prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat])}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-slate-50 text-xs font-semibold text-slate-700 hover:bg-slate-100"
|
||||
>
|
||||
<span>{cat}</span>
|
||||
<span>{expanded ? '▾' : '▸'}</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="p-2 space-y-2 bg-white">
|
||||
{catFields.map(field => (
|
||||
<div
|
||||
key={field.key}
|
||||
id={`sidebar-field-${field.key}`}
|
||||
onClick={() => {
|
||||
setEditingFieldKey(field.key);
|
||||
setEditFieldLabel(field.label);
|
||||
setEditFieldOptions((field.options || []).join(', '));
|
||||
}}
|
||||
className={`cursor-pointer rounded border p-2 transition-all ${activeFieldKey === field.key ? 'border-accent bg-blue-50 ring-1 ring-accent' : 'border-slate-100 bg-slate-50 hover:border-slate-200'}`}
|
||||
>
|
||||
{editingFieldKey === field.key ? (
|
||||
<div className="space-y-2" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="text-xs font-medium text-text-main">
|
||||
{field.isSystemLocked ? field.label : (
|
||||
<input
|
||||
type="text"
|
||||
value={editFieldLabel}
|
||||
onChange={(e) => setEditFieldLabel(e.target.value)}
|
||||
className="w-full px-1.5 py-1 text-xs border border-border rounded"
|
||||
placeholder="字段名称"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{['单选', '多选', '图片'].includes(field.category) && (
|
||||
<input
|
||||
type="text"
|
||||
value={editFieldOptions}
|
||||
onChange={(e) => setEditFieldOptions(e.target.value)}
|
||||
className="w-full px-1.5 py-1 text-xs border border-border rounded"
|
||||
placeholder="选项,用逗号分隔"
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => saveFieldEdit(field.key)}
|
||||
className="px-2 py-1 bg-accent text-white text-[10px] rounded"
|
||||
>保存</button>
|
||||
<button
|
||||
onClick={() => setEditingFieldKey(null)}
|
||||
className="px-2 py-1 bg-slate-200 text-slate-700 text-[10px] rounded"
|
||||
>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-text-main text-xs">{field.label}</div>
|
||||
<div className="text-[10px] text-slate-400">{field.category} · {field.type}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-[10px] text-slate-600 cursor-pointer" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.visibleInForm}
|
||||
onChange={() => toggleFieldVisible(field.key)}
|
||||
/>
|
||||
显示
|
||||
</label>
|
||||
{!field.isSystemLocked && (
|
||||
<button onClick={(e) => { e.stopPropagation(); deleteField(field.key); }} className="text-red-500 text-[10px] hover:underline">删除</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-[10px] text-slate-600 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.visibleInForm}
|
||||
onChange={() => toggleFieldVisible(field.key)}
|
||||
/>
|
||||
显示
|
||||
</label>
|
||||
<button onClick={() => deleteField(field.key)} className="text-red-500 text-[10px] hover:underline">删除</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Asset Library */}
|
||||
<div className="border border-slate-200 rounded overflow-hidden mt-4">
|
||||
<div className="px-3 py-2 bg-slate-50 text-xs font-semibold text-slate-700">素材库</div>
|
||||
<div className="p-2 space-y-2 bg-white">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{imageAssets.map(asset => (
|
||||
<div key={asset.id} className="relative w-14 h-14 border rounded overflow-hidden group">
|
||||
<img src={asset.dataUrl} alt={asset.name} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
const updated = imageAssets.filter(a => a.id !== asset.id);
|
||||
setImageAssets(updated);
|
||||
storage.set('imageAssets', updated);
|
||||
}}
|
||||
className="text-white text-[10px]"
|
||||
>删</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<label className="block w-full py-1.5 bg-accent text-white text-xs font-semibold rounded text-center cursor-pointer hover:opacity-90">
|
||||
上传图片
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleAssetUpload} />
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-slate-200 space-y-2">
|
||||
<div className="text-xs font-semibold text-text-main">新增字段</div>
|
||||
@@ -788,6 +963,7 @@ export default function TemplateManage() {
|
||||
if (cat === '单选') t = 'single_select';
|
||||
else if (cat === '多选') t = 'multi_select';
|
||||
else if (cat === '时间') t = 'date';
|
||||
else if (cat === '图片') t = 'image';
|
||||
setNewFieldForm({ ...newFieldForm, category: cat, type: t });
|
||||
}}
|
||||
className="flex-1 px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent bg-white"
|
||||
@@ -796,16 +972,18 @@ export default function TemplateManage() {
|
||||
<option value="单选">单选</option>
|
||||
<option value="多选">多选</option>
|
||||
<option value="时间">时间</option>
|
||||
<option value="图片">图片</option>
|
||||
</select>
|
||||
<select
|
||||
value={newFieldForm.type}
|
||||
onChange={(e) => setNewFieldForm({ ...newFieldForm, type: e.target.value as FieldType })}
|
||||
className="flex-1 px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent bg-white"
|
||||
>
|
||||
<option value="text">文本</option>
|
||||
{newFieldForm.category === '填空' && <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>}
|
||||
</select>
|
||||
</div>
|
||||
{['单选', '多选'].includes(newFieldForm.category) && (
|
||||
|
||||
@@ -103,7 +103,7 @@ export const BINDABLE_FIELDS: BindableField[] = [
|
||||
{ key: 'anesthesiaType', label: '麻醉方式' },
|
||||
];
|
||||
|
||||
export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date' | 'signature';
|
||||
export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date' | 'signature' | 'image';
|
||||
|
||||
export interface FormField {
|
||||
key: string;
|
||||
@@ -130,6 +130,13 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
|
||||
{ key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['赵医生', '钱医生', '孙医生'] },
|
||||
{ key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['周医生', '吴医生', '郑医生'] },
|
||||
{ key: 'anesthesiaType', label: '麻醉方式', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉'] },
|
||||
{ key: 'preoperativeDiagnosis', label: '术前诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] },
|
||||
{ key: 'postoperativeDiagnosis', label: '术后诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] },
|
||||
{ key: 'postOpCondition', label: '手术后情况', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['患者麻醉恢复后安返病房'] },
|
||||
{ key: 'specimenDescription', label: '切除标本描述', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊一枚,壁厚约0.3cm,内含数枚结石'] },
|
||||
{ key: 'pathologyCheck', label: '是否送病理检查', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['是', '否'] },
|
||||
{ key: 'frozenPathology', label: '冰冻病理结果', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['未见恶性', '待石蜡'] },
|
||||
{ key: 'isSigned', label: '手术者签名确认', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['已签字', '未签字'] },
|
||||
{ key: 'surgeonSignature', label: '手术者签名', category: '图片', type: 'signature', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'hospitalLogo', label: '医院Logo', category: '图片', type: 'image', visibleInForm: true, isSystemLocked: true },
|
||||
];
|
||||
|
||||
@@ -3,7 +3,10 @@ const smartField = (key: string) => `<span class="smart-field-wrapper" contented
|
||||
export const defaultReportContent = `
|
||||
<!-- 医院Logo -->
|
||||
<p style="text-align: center; margin-bottom: 16px;" contenteditable="false">
|
||||
<img src="/logo_square.png" alt="医院Logo" style="width: 65px; height: auto; display: block; margin: 0 auto;">
|
||||
<div class="image-placeholder" data-placeholder="true" data-bind="hospitalLogo" contenteditable="false" style="width: 65px; margin: 0 auto;">
|
||||
<span class="delete-btn" contenteditable="false">×</span>
|
||||
<p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<!-- 医院名称 -->
|
||||
@@ -29,11 +32,11 @@ export const defaultReportContent = `
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>术前诊断:</strong><span style="color: #bdbdbd;">术前诊断</span>
|
||||
<strong>术前诊断:</strong>${smartField('preoperativeDiagnosis')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>术后诊断:</strong><span style="color: #bdbdbd;">术后诊断</span>
|
||||
<strong>术后诊断:</strong>${smartField('postoperativeDiagnosis')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
@@ -132,23 +135,23 @@ export const defaultReportContent = `
|
||||
|
||||
<div class="template-info-section">
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>手术后情况</strong>:患者麻醉恢复后安返病房
|
||||
<strong>手术后情况</strong>:${smartField('postOpCondition')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>切除标本描述</strong>:<span style="color: #bdbdbd;">切除标本描述</span>
|
||||
<strong>切除标本描述</strong>:${smartField('specimenDescription')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>是否送病理检查</strong>:是
|
||||
<strong>是否送病理检查</strong>:${smartField('pathologyCheck')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
<strong>冰冻病理结果</strong>:<span style="color: #bdbdbd;">冰冻病理结果</span>
|
||||
<strong>冰冻病理结果</strong>:${smartField('frozenPathology')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
手术者签名:<span style="color: #bdbdbd;">签名</span>
|
||||
手术者签名:${smartField('surgeonSignature')}
|
||||
</p>
|
||||
|
||||
<p style="text-align: right; font-family: SimSun; color: #bdbdbd;">
|
||||
|
||||
Reference in New Issue
Block a user