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:
@@ -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>​`;
|
||||
execCmd('insertHTML', html);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>​`;
|
||||
} 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 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) {
|
||||
@@ -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>​`;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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;">
|
||||
|
||||
Reference in New Issue
Block a user