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.classList.remove('has-image');
|
||||||
placeholder.innerHTML = `
|
placeholder.innerHTML = `
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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;
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
saveDraftToStorage();
|
saveDraftToStorage();
|
||||||
@@ -384,7 +384,7 @@ export default function ReportEditor() {
|
|||||||
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
||||||
placeholder.innerHTML = `
|
placeholder.innerHTML = `
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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');
|
placeholder.classList.add('has-image');
|
||||||
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
@@ -422,13 +422,21 @@ export default function ReportEditor() {
|
|||||||
|
|
||||||
const insertImage = () => {
|
const insertImage = () => {
|
||||||
editorRef.current?.focus();
|
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 id = 'ph_' + Date.now();
|
||||||
const html = `
|
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>​`;
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
execCmd('insertHTML', html);
|
execCmd('insertHTML', html);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export default function TemplateManage() {
|
|||||||
const [formFields, setFormFields] = useState<FormField[]>([]);
|
const [formFields, setFormFields] = useState<FormField[]>([]);
|
||||||
const [newFieldForm, setNewFieldForm] = useState({ label: '', category: '填空', type: 'text' as FieldType });
|
const [newFieldForm, setNewFieldForm] = useState({ label: '', category: '填空', type: 'text' as FieldType });
|
||||||
const [newFieldOptions, setNewFieldOptions] = useState('');
|
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 [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);
|
||||||
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
|
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
|
||||||
const [editFieldLabel, setEditFieldLabel] = useState('');
|
const [editFieldLabel, setEditFieldLabel] = useState('');
|
||||||
@@ -117,26 +119,13 @@ export default function TemplateManage() {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
const triggerPlaceholderUpload = (placeholder: HTMLElement) => {
|
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
||||||
const input = document.createElement('input');
|
placeholder.innerHTML = `
|
||||||
input.type = 'file';
|
<span class="delete-btn" contenteditable="false">×</span>
|
||||||
input.accept = 'image/*';
|
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
|
||||||
input.onchange = (ev) => {
|
`;
|
||||||
const file = (ev.target as HTMLInputElement).files?.[0];
|
placeholder.classList.add('has-image');
|
||||||
if (file) {
|
saveTemplateContent();
|
||||||
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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle image placeholder and smart field delete interactions via click capture
|
// 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.classList.remove('has-image');
|
||||||
placeholder.innerHTML = `
|
placeholder.innerHTML = `
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 {
|
} else {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
@@ -206,7 +195,8 @@ export default function TemplateManage() {
|
|||||||
if (!placeholder.classList.contains('has-image')) {
|
if (!placeholder.classList.contains('has-image')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
triggerPlaceholderUpload(placeholder);
|
setImagePickerTarget(placeholder);
|
||||||
|
setImagePickerOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,19 +348,13 @@ export default function TemplateManage() {
|
|||||||
const insertSmartField = (field: FormField) => {
|
const insertSmartField = (field: FormField) => {
|
||||||
editorRef.current?.focus();
|
editorRef.current?.focus();
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
if (field.type !== 'image' && editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
|
if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
|
||||||
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
|
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pushHistory();
|
pushHistory();
|
||||||
|
|
||||||
let html = '';
|
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>​`;
|
||||||
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();
|
const sel = window.getSelection();
|
||||||
if (sel && sel.rangeCount > 0) {
|
if (sel && sel.rangeCount > 0) {
|
||||||
@@ -502,13 +486,21 @@ export default function TemplateManage() {
|
|||||||
const insertImage = () => {
|
const insertImage = () => {
|
||||||
editorRef.current?.focus();
|
editorRef.current?.focus();
|
||||||
restoreSelection();
|
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 id = 'ph_' + Date.now();
|
||||||
const html = `
|
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>​`;
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
pushHistory();
|
pushHistory();
|
||||||
execCmd('insertHTML', html);
|
execCmd('insertHTML', html);
|
||||||
};
|
};
|
||||||
@@ -798,7 +790,7 @@ export default function TemplateManage() {
|
|||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||||
{fieldLibTab === 'insert' && (
|
{fieldLibTab === 'insert' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
|
{['填空', '单选', '多选', '时间'].map(cat => {
|
||||||
const catFields = formFields.filter(f => f.category === cat);
|
const catFields = formFields.filter(f => f.category === cat);
|
||||||
if (catFields.length === 0) return null;
|
if (catFields.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
@@ -829,7 +821,7 @@ export default function TemplateManage() {
|
|||||||
|
|
||||||
{fieldLibTab === 'manage' && (
|
{fieldLibTab === 'manage' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
|
{['填空', '单选', '多选', '时间'].map(cat => {
|
||||||
const catFields = formFields.filter(f => f.category === cat);
|
const catFields = formFields.filter(f => f.category === cat);
|
||||||
if (catFields.length === 0) return null;
|
if (catFields.length === 0) return null;
|
||||||
const expanded = expandedCategories.includes(cat);
|
const expanded = expandedCategories.includes(cat);
|
||||||
@@ -868,7 +860,7 @@ export default function TemplateManage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{['单选', '多选', '图片'].includes(field.category) && (
|
{['单选', '多选'].includes(field.category) && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editFieldOptions}
|
value={editFieldOptions}
|
||||||
@@ -963,7 +955,6 @@ export default function TemplateManage() {
|
|||||||
if (cat === '单选') t = 'single_select';
|
if (cat === '单选') t = 'single_select';
|
||||||
else if (cat === '多选') t = 'multi_select';
|
else if (cat === '多选') t = 'multi_select';
|
||||||
else if (cat === '时间') t = 'date';
|
else if (cat === '时间') t = 'date';
|
||||||
else if (cat === '图片') t = 'image';
|
|
||||||
setNewFieldForm({ ...newFieldForm, category: cat, type: t });
|
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"
|
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>
|
||||||
<option value="时间">时间</option>
|
<option value="时间">时间</option>
|
||||||
<option value="图片">图片</option>
|
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={newFieldForm.type}
|
value={newFieldForm.type}
|
||||||
@@ -983,7 +973,6 @@ export default function TemplateManage() {
|
|||||||
{newFieldForm.category === '单选' && <option value="single_select">下拉单选</option>}
|
{newFieldForm.category === '单选' && <option value="single_select">下拉单选</option>}
|
||||||
{newFieldForm.category === '多选' && <option value="multi_select">标签多选</option>}
|
{newFieldForm.category === '多选' && <option value="multi_select">标签多选</option>}
|
||||||
{newFieldForm.category === '时间' && <><option value="date">日期</option><option value="time">时分</option></>}
|
{newFieldForm.category === '时间' && <><option value="date">日期</option><option value="time">时分</option></>}
|
||||||
{newFieldForm.category === '图片' && <option value="image">图片</option>}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{['单选', '多选'].includes(newFieldForm.category) && (
|
{['单选', '多选'].includes(newFieldForm.category) && (
|
||||||
@@ -1054,6 +1043,67 @@ export default function TemplateManage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,4 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
|
|||||||
{ key: 'pathologyCheck', label: '是否送病理检查', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['是', '否'] },
|
{ 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: 'frozenPathology', label: '冰冻病理结果', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['未见恶性', '待石蜡'] },
|
||||||
{ key: 'isSigned', label: '手术者签名确认', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, 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 = `
|
export const defaultReportContent = `
|
||||||
<!-- 医院Logo -->
|
<!-- 医院Logo -->
|
||||||
<p style="text-align: center; margin-bottom: 16px;" contenteditable="false">
|
<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>
|
<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>
|
||||||
</div>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 医院名称 -->
|
<!-- 医院名称 -->
|
||||||
@@ -151,7 +151,7 @@ export const defaultReportContent = `
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<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>
|
||||||
|
|
||||||
<p style="text-align: right; font-family: SimSun; color: #bdbdbd;">
|
<p style="text-align: right; font-family: SimSun; color: #bdbdbd;">
|
||||||
|
|||||||
154
工程分析/实现方案-2026-04-17-19-26-17.md
Normal file
154
工程分析/实现方案-2026-04-17-19-26-17.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 实现方案 — 2026-04-17-19-26-17
|
||||||
|
|
||||||
|
## 变更文件
|
||||||
|
|
||||||
|
1. `src/types.ts`
|
||||||
|
2. `src/utils/defaultContent.ts`
|
||||||
|
3. `src/pages/TemplateManage.tsx`
|
||||||
|
4. `src/pages/ReportEditor.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、types.ts 修改
|
||||||
|
|
||||||
|
从 `DEFAULT_FORM_FIELDS` 中移除以下两个字段:
|
||||||
|
- `surgeonSignature`
|
||||||
|
- `hospitalLogo`
|
||||||
|
|
||||||
|
同时从 `FieldType` 中移除 `'image'`(因为图片不再作为可插入的字段类型,仅作为占位符存在)。若移除 `'image'` 会导致大量类型错误,也可保留类型但不在 UI 中暴露。为最小侵入,保留 `'image'` 类型但不再在 `DEFAULT_FORM_FIELDS` 中使用。
|
||||||
|
|
||||||
|
实际执行:删除 `DEFAULT_FORM_FIELDS` 中的最后两项(`surgeonSignature` 和 `hospitalLogo`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、defaultContent.ts 修改
|
||||||
|
|
||||||
|
### 2.1 医院Logo
|
||||||
|
将原有的 `div.image-placeholder` 替换为 `span.image-placeholder`(保持居中):
|
||||||
|
```html
|
||||||
|
<!-- 医院Logo -->
|
||||||
|
<p style="text-align: center; margin-bottom: 16px;" contenteditable="false">
|
||||||
|
<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>
|
||||||
|
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入图片</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 手术者签名
|
||||||
|
将 `手术者签名:${smartField('surgeonSignature')}` 替换为:
|
||||||
|
```html
|
||||||
|
<p style="font-family: SimSun;">
|
||||||
|
手术者签名:<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>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、TemplateManage.tsx 修改
|
||||||
|
|
||||||
|
### 3.1 移除"图片"分类暴露
|
||||||
|
- `expandedCategories` 初始值从 `['填空','单选','多选','时间','图片']` 改为 `['填空','单选','多选','时间']`。
|
||||||
|
- "插入字段"Tab 的遍历数组从 `['填空','单选','多选','时间','图片']` 改为 `['填空','单选','多选','时间']`。
|
||||||
|
- "字段管理"Tab 的遍历数组同样移除 `'图片'`。
|
||||||
|
- 新增字段表单的 category select 中移除 `<option value="图片">图片</option>`。
|
||||||
|
- type select 的条件渲染中移除图片相关的 option。
|
||||||
|
|
||||||
|
### 3.2 改造 insertImage()
|
||||||
|
将现有的 `insertImage()` 替换为:
|
||||||
|
```typescript
|
||||||
|
const insertImage = () => {
|
||||||
|
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 = `<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>​`;
|
||||||
|
|
||||||
|
editorRef.current?.focus();
|
||||||
|
restoreSelection();
|
||||||
|
pushHistory();
|
||||||
|
execCmd('insertHTML', html);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 统一图片源选择弹窗
|
||||||
|
从 `ReportEditor.tsx` 复用以下逻辑到 `TemplateManage.tsx`:
|
||||||
|
- 新增状态:`imagePickerOpen`、`imagePickerTarget`、`imageAssets`(已存在)。
|
||||||
|
- 新增 `fillPlaceholderSrc` 函数。
|
||||||
|
- 修改 `handleEditorClick` 中的 placeholder 点击逻辑:
|
||||||
|
```typescript
|
||||||
|
if (!placeholder.classList.contains('has-image')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setImagePickerTarget(placeholder);
|
||||||
|
setImagePickerOpen(true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 删除原有的 `triggerPlaceholderUpload` 函数及其直接调用。
|
||||||
|
- 在 JSX 底部(`isModalOpen` 弹窗之后)新增 `imagePickerOpen` 弹窗组件(与 ReportEditor 完全一致)。
|
||||||
|
|
||||||
|
### 3.4 清理删除后的重置逻辑
|
||||||
|
当 placeholder 被删除(点击 × 后)时,重置为:
|
||||||
|
```html
|
||||||
|
<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>
|
||||||
|
```
|
||||||
|
同时保留原有内联样式(避免把 `inline-flex` 等样式清掉)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、ReportEditor.tsx 修改
|
||||||
|
|
||||||
|
### 4.1 改造 insertImage()
|
||||||
|
与 TemplateManage 保持一致:
|
||||||
|
```typescript
|
||||||
|
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 = `<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);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 删除后重置逻辑
|
||||||
|
在 `handleEditorClick` 中,placeholder 删除后重置的 HTML 改为使用 `<span>` 结构并保留内联样式:
|
||||||
|
```typescript
|
||||||
|
placeholder.innerHTML = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 fillPlaceholderSrc 保持兼容
|
||||||
|
已有 `fillPlaceholderSrc` 可继续使用,但建议填充的图片增加 `max-width: 100%; max-height: 100%; object-fit: contain;`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回滚策略
|
||||||
|
|
||||||
|
修改前最新提交为 `0c57409`。若失败可 `git reset --hard 0c57409`。
|
||||||
|
|
||||||
|
## 无新增 npm 依赖
|
||||||
71
工程分析/测试方案-2026-04-17-19-26-17.md
Normal file
71
工程分析/测试方案-2026-04-17-19-26-17.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# 测试方案 — 2026-04-17-19-26-17
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
|
||||||
|
验证 6 项需求全部正确实现,且不破坏现有编辑、保存、打印功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 编译检查
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
- **预期**:`tsc --noEmit` 通过,0 errors。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 字段体系清理(需求 1)
|
||||||
|
1. 进入 `/template-manage`。
|
||||||
|
2. 在"插入字段"Tab 中,确认分类列表只有"填空、单选、多选、时间",**没有"图片"**。
|
||||||
|
3. 在"字段管理"Tab 中,确认同样没有"图片"分组。
|
||||||
|
4. 在"字段管理 → 新增字段"中,确认 category select 没有"图片"选项,type select 也不会出现"图片"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 默认模板占位符替换(需求 4)
|
||||||
|
1. 重新加载默认模板(或清空 localStorage 后重新登录)。
|
||||||
|
2. 确认模板顶部 Logo 处显示为一个虚线框占位符(而非直接显示医院 Logo 图片)。
|
||||||
|
3. 确认"手术者签名:"后方显示为一个虚线框占位符,而非 `smart-field-wrapper` 文本框。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 插入图片占位符同行与尺寸设置(需求 2、5)
|
||||||
|
1. 在 `/template-manage` 编辑器中,将光标放在一段文字中间,点击工具栏"插入图片占位符"。
|
||||||
|
2. 在 prompt 中输入宽度 `120`,高度 `60`。
|
||||||
|
3. 确认占位符插入后与前后文字**保持在同一行**,没有换行。
|
||||||
|
4. 使用浏览器 DevTools 检查该占位符,确认 `style` 中包含 `display:inline-flex` 和 `max-width:120px; max-height:60px;`。
|
||||||
|
5. 在 `/report-editor` 中重复上述操作,确认行为一致。
|
||||||
|
6. 测试留空宽高:确认插入的占位符没有 `max-width/max-height`,但有默认的 `padding: 8px 16px;`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 占位符文字自适应(需求 3)
|
||||||
|
1. 插入一个宽度为 `60px` 的图片占位符。
|
||||||
|
2. 确认占位符内显示的文字是**"插入图片"**(而非"插入/点击放置图片")。
|
||||||
|
3. 插入一个宽度为 `120px` 的占位符,确认显示"插入/点击放置图片"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 图片来源选择弹窗统一(需求 6)
|
||||||
|
1. 在 `/template-manage` 中,点击任意无图片的 `image-placeholder`。
|
||||||
|
2. 确认弹出"选择图片来源"弹窗,包含"本地上传"、"我的签名"、"系统素材"三个选项。
|
||||||
|
3. 选择"系统素材"中的医院 Logo,确认占位符被替换为 Logo 图片。
|
||||||
|
4. 在 `/report-editor` 中点击占位符,确认弹窗行为与 `/template-manage` 完全一致。
|
||||||
|
5. 测试弹窗中的"取消"按钮,确认点击后弹窗关闭且占位符未被修改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 回归测试
|
||||||
|
1. **保存模板**:修改模板后点击保存,刷新页面确认内容不丢失。
|
||||||
|
2. **保存报告**:在 `/report-editor` 中填写表单并保存草稿,确认内容持久化。
|
||||||
|
3. **打印预览**:确认图片占位符(已填充和未填充)在打印预览中显示正常。
|
||||||
|
4. **撤销重做**:插入占位符后按 `Ctrl+Z`,确认占位符被正确撤销。
|
||||||
|
5. **拖拽/自动插入关键帧**:确认 `/report-editor` 中的视频关键帧仍能正常插入到图片占位符中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 判定标准
|
||||||
|
|
||||||
|
全部测试通过后方可认为任务完成。若任何测试失败,需回滚并重新分析根因。
|
||||||
32
工程分析/经验记录.md
32
工程分析/经验记录.md
@@ -610,3 +610,35 @@ ange.insertNode(fragment) 精确插入到 Range 位置;
|
|||||||
- 图片字段与普通文本字段的 DOM 结构差异大,插入逻辑需要按 type 分支。
|
- 图片字段与普通文本字段的 DOM 结构差异大,插入逻辑需要按 type 分支。
|
||||||
- 编辑器与侧边栏联动建议使用 `scrollIntoView` + 临时 CSS 类,避免复杂的状态同步。
|
- 编辑器与侧边栏联动建议使用 `scrollIntoView` + 临时 CSS 类,避免复杂的状态同步。
|
||||||
- 新增 localStorage key 时应提供合理的默认值或降级处理。
|
- 新增 localStorage key 时应提供合理的默认值或降级处理。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 记录 23:图片占位符体系重构与双端统一
|
||||||
|
|
||||||
|
**A. 具体问题**
|
||||||
|
1. `template-manage` 的"插入字段"中仍存在"图片"分类(手术者签名、医院Logo),用户认为不再需要。
|
||||||
|
2. 插入图片占位符时无法自定义默认宽高,且使用 `<div>` 导致强制换行。
|
||||||
|
3. 占位符框太小时"插入/点击放置图片"文字显示不全。
|
||||||
|
4. 默认模板中签名和 Logo 的结构不统一(一个是 `smartField`,一个是 `div.image-placeholder`)。
|
||||||
|
5. `template-manage` 点击图片占位符直接调起本地文件选择器,与 `report-editor` 的三选一弹窗行为不一致。
|
||||||
|
|
||||||
|
**B. 问题产生原因**
|
||||||
|
1. `DEFAULT_FORM_FIELDS` 仍包含 `surgeonSignature` 和 `hospitalLogo`。
|
||||||
|
2. 两端编辑器的 `insertImage()` 使用块级 `<div>` 插入,未提供尺寸 prompt。
|
||||||
|
3. 占位符提示文本固定为长文本,未根据容器宽度做缩写适配。
|
||||||
|
4. `TemplateManage` 的 placeholder 点击事件直接调用 `triggerPlaceholderUpload()`,缺少与 `ReportEditor` 一致的弹窗组件。
|
||||||
|
|
||||||
|
**C. 解决问题方法**
|
||||||
|
1. **清理图片字段**:从 `DEFAULT_FORM_FIELDS` 和 `types.ts` 中移除 `surgeonSignature` 和 `hospitalLogo`;在 `TemplateManage.tsx` 的插入字段/字段管理/新增字段表单中彻底移除"图片"分类。
|
||||||
|
2. **统一默认模板**:在 `defaultContent.ts` 中将 Logo 和签名均替换为 `<span class="image-placeholder" style="display:inline-flex;...">`。
|
||||||
|
3. **改造 insertImage()**:在 `TemplateManage.tsx` 和 `ReportEditor.tsx` 中,插入前通过 `prompt` 获取最大宽度/高度(px),生成带 `max-width/max-height` 的 `<span>` 行内占位符;提示文字中附加"正文一行文字高度约为 20 像素左右"。
|
||||||
|
4. **文本自适应**:根据 prompt 输入的宽度决定提示文字:宽度 < 80px 时显示"插入图片",否则显示"插入/点击放置图片"。
|
||||||
|
5. **统一弹窗行为**:将 `ReportEditor` 的 `imagePickerOpen` / `imagePickerTarget` / `fillPlaceholderSrc` 逻辑完整移植到 `TemplateManage`;删除旧的 `triggerPlaceholderUpload` 直接上传逻辑;两端点击图片占位符均弹出"本地上传 / 我的签名 / 系统素材"三选一弹窗。
|
||||||
|
6. **优化填充样式**:`fillPlaceholderSrc` 中给 `<img>` 增加 `max-width:100%; max-height:100%; object-fit:contain;`,避免撑破设置了固定尺寸的占位符。
|
||||||
|
|
||||||
|
**D. 经验与教训总结**
|
||||||
|
- 当从字段体系中彻底移除某一分类时,需要同时清理:`DEFAULT_FORM_FIELDS`、UI 渲染数组、新增表单 options、以及可能残留的分类判断逻辑(如编辑字段时显示 options 输入框的条件)。
|
||||||
|
- 在 `contentEditable` 中实现"同行插入"必须使用行内元素(`<span>`)并显式设置 `display:inline-flex` + `vertical-align:middle`;块级 `<div>` 即使通过 CSS 改 display 也可能因浏览器 execCommand 修正导致换行。
|
||||||
|
- 跨页面/跨编辑器的一致交互(如图片选择弹窗)应抽取为可复用逻辑或至少保持代码结构一致,避免用户在不同页面产生认知割裂。
|
||||||
|
- `prompt` 虽不是最优雅的用户交互,但在工具栏快捷操作中是一种零依赖、快速落地的方案;若后续需要更复杂交互,可再替换为 Modal 组件。
|
||||||
|
|||||||
62
工程分析/需求分析-2026-04-17-19-26-17.md
Normal file
62
工程分析/需求分析-2026-04-17-19-26-17.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 需求分析 — 2026-04-17-19-26-17
|
||||||
|
|
||||||
|
## 用户反馈的 6 项需求
|
||||||
|
|
||||||
|
### 1. 移除插入字段中的"图片"类型字段
|
||||||
|
在 `template-manage` 的"插入字段"Tab 中,"图片"分类(包含手术者签名、医院Logo)不再需要。需要:
|
||||||
|
- 从插入字段的分类列表中移除"图片";
|
||||||
|
- 从 `DEFAULT_FORM_FIELDS` 中移除 `surgeonSignature` 和 `hospitalLogo`;
|
||||||
|
- 清理 `TemplateManage.tsx` 中与图片类型字段相关的分类渲染逻辑(如折叠分组、新增字段表单中的"图片"选项等)。
|
||||||
|
|
||||||
|
### 2. 细化"插入图片占位符"功能(支持自定义默认宽高)
|
||||||
|
在 `template-manage` 和 `report-editor` 中,点击工具栏的"插入图片占位符"按钮时:
|
||||||
|
- 弹出提示框让用户输入默认最大宽度(px)和最大高度(px),留空则表示无限制;
|
||||||
|
- 提示文字中附加说明:"一个文字高度约为 20 像素左右";
|
||||||
|
- 将用户输入的宽高写入占位符的 `style` 属性中(`max-width` / `max-height`)。
|
||||||
|
|
||||||
|
### 3. 占位符文字自适应缩写
|
||||||
|
`class="image-placeholder"` 中的提示文字,如果占位符框太小(通过宽度 < 80px 判断),则将 "插入/点击放置图片" 缩写为 "插入图片"。
|
||||||
|
|
||||||
|
### 4. 手术者签名、医院Logo 改用图片占位符
|
||||||
|
在 `defaultContent.ts` 中:
|
||||||
|
- 将 `smartField('surgeonSignature')` 替换为行内 `<span class="image-placeholder">`;
|
||||||
|
- 将顶部的 `hospitalLogo` 占位符替换为标准的 `<span class="image-placeholder">`(保持居中)。
|
||||||
|
|
||||||
|
### 5. 插入图片占位符时保持同行
|
||||||
|
当前使用 `<div>` 插入图片占位符会导致强制换行。需要:
|
||||||
|
- 将占位符的容器标签从 `<div>` 改为 `<span>`;
|
||||||
|
- 设置 `display: inline-flex` + `vertical-align: middle`;
|
||||||
|
- 确保插入后与前后文字保持在同一行。
|
||||||
|
|
||||||
|
### 6. 统一两个编辑器的图片占位符点击行为
|
||||||
|
`template-manage` 中点击图片占位符时目前直接调起本地文件选择器。需要将其改为与 `report-editor` 一致的行为:
|
||||||
|
- 弹出"图片来源选择器"弹窗;
|
||||||
|
- 支持"本地上传"、"我的签名"、"系统素材"三种来源;
|
||||||
|
- 将 `ReportEditor` 中的弹窗逻辑复用到 `TemplateManage` 中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- `src/types.ts`:移除 `surgeonSignature` 和 `hospitalLogo` 字段(或将其从字段配置中移除)。
|
||||||
|
- `src/utils/defaultContent.ts`:签名和 Logo 位置替换为 `image-placeholder`。
|
||||||
|
- `src/pages/TemplateManage.tsx`:
|
||||||
|
- 移除"图片"分类相关渲染和新增字段表单选项;
|
||||||
|
- 改造 `insertImage()` 支持 prompt 输入宽高;
|
||||||
|
- 新增图片来源选择弹窗状态和填充逻辑;
|
||||||
|
- 移除 `triggerPlaceholderUpload` 的直接调用。
|
||||||
|
- `src/pages/ReportEditor.tsx`:
|
||||||
|
- 改造 `insertImage()` 支持 prompt 输入宽高;
|
||||||
|
- 保持现有的图片来源选择弹窗逻辑不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. `template-manage` 的"插入字段"和"字段管理"中不再出现"图片"分类和相关字段。
|
||||||
|
2. `defaultContent.ts` 中签名和 Logo 均为 `image-placeholder`。
|
||||||
|
3. 点击"插入图片占位符"时弹出宽高 prompt,正确应用 `max-width` / `max-height`。
|
||||||
|
4. 图片占位符使用 `<span>` + `display: inline-flex`,插入后与文字同行。
|
||||||
|
5. 宽度 < 80px 的占位符显示"插入图片",否则显示"插入/点击放置图片"。
|
||||||
|
6. `template-manage` 和 `report-editor` 点击图片占位符均弹出三选一图片来源弹窗。
|
||||||
|
7. `npm run lint` 通过。
|
||||||
Reference in New Issue
Block a user