refactor: unify image-placeholder across editors and remove image field type

- Remove surgeonSignature and hospitalLogo from DEFAULT_FORM_FIELDS.
- Replace logo and signature in default template with inline image-placeholder spans.
- Enhance insertImage() in both editors with prompt for max-width/height (px).
- Abbreviate placeholder text to '插入图片' when width < 80px.
- Force inline insertion using display:inline-flex + vertical-align:middle.
- Port image-source picker modal from ReportEditor to TemplateManage.
- Remove legacy triggerPlaceholderUpload direct upload logic.
This commit is contained in:
2026-04-17 19:34:03 +08:00
parent 0c57409c59
commit 5fee3352c1
8 changed files with 432 additions and 57 deletions

View File

@@ -306,7 +306,7 @@ export default function ReportEditor() {
placeholder.classList.remove('has-image');
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p>
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
`;
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
@@ -384,7 +384,7 @@ export default function ReportEditor() {
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">
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
`;
placeholder.classList.add('has-image');
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
@@ -422,13 +422,21 @@ export default function ReportEditor() {
const insertImage = () => {
editorRef.current?.focus();
const widthStr = prompt('请输入占位符最大宽度 (px),留空无限制:\n(提示:正文一行文字高度约为 20 像素左右)', '');
const heightStr = prompt('请输入占位符最大高度 (px),留空无限制:', '');
const width = parseInt(widthStr || '0');
const height = parseInt(heightStr || '0');
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
if (width > 0) styleStr += ` max-width:${width}px;`;
if (height > 0) styleStr += ` max-height:${height}px;`;
if (!width && !height) styleStr += ' padding:8px 16px;';
const showShortText = width > 0 && width < 80;
const hintText = showShortText ? '插入图片' : '插入/点击放置图片';
const id = 'ph_' + Date.now();
const html = `
<div id="${id}" class="image-placeholder" data-placeholder="true" 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>
`;
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>&#8203;`;
execCmd('insertHTML', html);
};

View File

@@ -24,7 +24,9 @@ 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 [expandedCategories, setExpandedCategories] = useState<string[]>(['填空', '单选', '多选', '时间']);
const [imagePickerOpen, setImagePickerOpen] = useState(false);
const [imagePickerTarget, setImagePickerTarget] = useState<HTMLElement | null>(null);
const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
const [editFieldLabel, setEditFieldLabel] = useState('');
@@ -117,26 +119,13 @@ export default function TemplateManage() {
return () => observer.disconnect();
}, [currentUser]);
const triggerPlaceholderUpload = (placeholder: HTMLElement) => {
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;
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');
};
reader.readAsDataURL(file);
}
};
input.click();
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
`;
placeholder.classList.add('has-image');
saveTemplateContent();
};
// Handle image placeholder and smart field delete interactions via click capture
@@ -190,7 +179,7 @@ export default function TemplateManage() {
placeholder.classList.remove('has-image');
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p>
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
`;
} else {
const range = document.createRange();
@@ -206,7 +195,8 @@ export default function TemplateManage() {
if (!placeholder.classList.contains('has-image')) {
e.preventDefault();
e.stopPropagation();
triggerPlaceholderUpload(placeholder);
setImagePickerTarget(placeholder);
setImagePickerOpen(true);
}
};
@@ -358,19 +348,13 @@ export default function TemplateManage() {
const insertSmartField = (field: FormField) => {
editorRef.current?.focus();
restoreSelection();
if (field.type !== 'image' && editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
return;
}
pushHistory();
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>&#8203;`;
} 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>&#8203;`;
}
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>&#8203;`;
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
@@ -502,13 +486,21 @@ export default function TemplateManage() {
const insertImage = () => {
editorRef.current?.focus();
restoreSelection();
const widthStr = prompt('请输入占位符最大宽度 (px),留空无限制:\n(提示:正文一行文字高度约为 20 像素左右)', '');
const heightStr = prompt('请输入占位符最大高度 (px),留空无限制:', '');
const width = parseInt(widthStr || '0');
const height = parseInt(heightStr || '0');
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
if (width > 0) styleStr += ` max-width:${width}px;`;
if (height > 0) styleStr += ` max-height:${height}px;`;
if (!width && !height) styleStr += ' padding:8px 16px;';
const showShortText = width > 0 && width < 80;
const hintText = showShortText ? '插入图片' : '插入/点击放置图片';
const id = 'ph_' + Date.now();
const html = `
<div id="${id}" class="image-placeholder" data-placeholder="true" 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>
`;
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>&#8203;`;
pushHistory();
execCmd('insertHTML', html);
};
@@ -798,7 +790,7 @@ export default function TemplateManage() {
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{fieldLibTab === 'insert' && (
<div className="space-y-4">
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
{['填空', '单选', '多选', '时间'].map(cat => {
const catFields = formFields.filter(f => f.category === cat);
if (catFields.length === 0) return null;
return (
@@ -829,7 +821,7 @@ export default function TemplateManage() {
{fieldLibTab === 'manage' && (
<div className="space-y-3">
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
{['填空', '单选', '多选', '时间'].map(cat => {
const catFields = formFields.filter(f => f.category === cat);
if (catFields.length === 0) return null;
const expanded = expandedCategories.includes(cat);
@@ -868,7 +860,7 @@ export default function TemplateManage() {
/>
)}
</div>
{['单选', '多选', '图片'].includes(field.category) && (
{['单选', '多选'].includes(field.category) && (
<input
type="text"
value={editFieldOptions}
@@ -963,7 +955,6 @@ 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"
@@ -972,7 +963,6 @@ export default function TemplateManage() {
<option value="单选"></option>
<option value="多选"></option>
<option value="时间"></option>
<option value="图片"></option>
</select>
<select
value={newFieldForm.type}
@@ -983,7 +973,6 @@ export default function TemplateManage() {
{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) && (
@@ -1054,6 +1043,67 @@ export default function TemplateManage() {
</div>
</div>
)}
{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>
);
}

View File

@@ -137,6 +137,4 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
{ 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 },
];

View File

@@ -3,10 +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">
<div class="image-placeholder" data-placeholder="true" data-bind="hospitalLogo" contenteditable="false" style="width: 65px; margin: 0 auto;">
<span class="image-placeholder" data-placeholder="true" contenteditable="false" style="display:inline-flex;align-items:center;justify-content:center;width:65px;height:65px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 auto;cursor:pointer;">
<span class="delete-btn" contenteditable="false">×</span>
<p class="placeholder-text" style="color: #94a3b8; font-size: 11px; margin: 0; pointer-events: none;">插入/点击放置图片</p>
</div>
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入图片</span>
</span>
</p>
<!-- 医院名称 -->
@@ -151,7 +151,7 @@ export const defaultReportContent = `
</p>
<p style="font-family: SimSun;">
手术者签名:${smartField('surgeonSignature')}
手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" style="display:inline-flex;align-items:center;justify-content:center;min-width:80px;min-height:24px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入图片</span></span>
</p>
<p style="text-align: right; font-family: SimSun; color: #bdbdbd;">