2026-04-18-18-36-43 - 报告管理列名修正、字段下划线控制、下载导出功能、右对齐排版修复、签名默认右对齐
This commit is contained in:
@@ -5,7 +5,7 @@ import Sidebar from '../components/Sidebar';
|
|||||||
import {
|
import {
|
||||||
Check, Printer, Undo, Redo, Bold, Italic, Underline,
|
Check, Printer, Undo, Redo, Bold, Italic, Underline,
|
||||||
AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon,
|
AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon,
|
||||||
Video, Play, Pause, Plus, X, ChevronLeft
|
Video, Play, Pause, Plus, X, ChevronLeft, Download
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { User, Report, Template, CapturedFrame, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types';
|
import { User, Report, Template, CapturedFrame, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types';
|
||||||
import { defaultReportContent } from '../utils/defaultContent';
|
import { defaultReportContent } from '../utils/defaultContent';
|
||||||
@@ -48,6 +48,7 @@ export default function ReportEditor() {
|
|||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const [isSaved, setIsSaved] = useState(false);
|
const [isSaved, setIsSaved] = useState(false);
|
||||||
|
const [exportModalOpen, setExportModalOpen] = useState(false);
|
||||||
const [loadedTemplateId, setLoadedTemplateId] = useState('');
|
const [loadedTemplateId, setLoadedTemplateId] = useState('');
|
||||||
const [pendingTemplateId, setPendingTemplateId] = useState<string | null>(null);
|
const [pendingTemplateId, setPendingTemplateId] = useState<string | null>(null);
|
||||||
const prevVideoCountRef = useRef(0);
|
const prevVideoCountRef = useRef(0);
|
||||||
@@ -1306,6 +1307,13 @@ export default function ReportEditor() {
|
|||||||
<Check size={16} />
|
<Check size={16} />
|
||||||
完成报告
|
完成报告
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setExportModalOpen(true)}
|
||||||
|
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
||||||
|
title="下载"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => editorRef.current && printDocument(editorRef.current.innerHTML)}
|
onClick={() => editorRef.current && printDocument(editorRef.current.innerHTML)}
|
||||||
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
||||||
@@ -1965,11 +1973,11 @@ export default function ReportEditor() {
|
|||||||
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
||||||
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
||||||
} else {
|
} else {
|
||||||
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;';
|
let styleStr = 'display:inline-block;text-align:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;';
|
||||||
styleStr += `width:${w}px;height:${h}px;`;
|
styleStr += `width:${w}px;height:${h}px;line-height:${h}px;`;
|
||||||
const showShortText = w > 0 && w < 80;
|
const showShortText = w > 0 && w < 80;
|
||||||
const text = showShortText ? '插图' : hintText;
|
const text = showShortText ? '插图' : hintText;
|
||||||
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} 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;">${text}</span></span>​`;
|
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;display:inline-block;vertical-align:middle;line-height:normal;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${text}</span></span>​`;
|
||||||
}
|
}
|
||||||
execCmd('insertHTML', html);
|
execCmd('insertHTML', html);
|
||||||
setPlaceholderModal({...placeholderModal, isOpen: false});
|
setPlaceholderModal({...placeholderModal, isOpen: false});
|
||||||
@@ -2021,6 +2029,48 @@ export default function ReportEditor() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{exportModalOpen && (
|
||||||
|
<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 ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const title = reportData.title || '无标题';
|
||||||
|
const patient = reportData.patientName || '未知';
|
||||||
|
const hid = reportData.hospitalId || '无号';
|
||||||
|
printDocument(editorRef.current?.innerHTML || '', `图文报告-${title}-${patient}-${hid}-${ts}`);
|
||||||
|
setExportModalOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full py-2.5 bg-accent text-white rounded text-sm font-semibold hover:opacity-90 transition-colors"
|
||||||
|
>导出 PDF</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const title = reportData.title || '无标题';
|
||||||
|
const patient = reportData.patientName || '未知';
|
||||||
|
const hid = reportData.hospitalId || '无号';
|
||||||
|
const blob = new Blob([JSON.stringify(reportData, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `图文报告-${title}-${patient}-${hid}-${ts}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
setExportModalOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||||
|
>导出 JSON</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setExportModalOpen(false)}
|
||||||
|
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||||
|
>取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{imagePickerOpen && imagePickerTarget && (
|
{imagePickerOpen && imagePickerTarget && (
|
||||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
<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">
|
<div className="bg-white rounded-2xl p-6 w-full max-w-[360px] shadow-2xl border border-border">
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export default function ReportManage() {
|
|||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">报告信息</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">报告信息</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">患者</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">患者</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">患者号</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">住院号</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">创建者</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">创建者</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border w-40">时间</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border w-40">时间</th>
|
||||||
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border w-24">状态</th>
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border w-24">状态</th>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import { Plus, Edit, Trash2, Save, Printer, Undo, Redo, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon, Check } from 'lucide-react';
|
import { Plus, Edit, Trash2, Save, Printer, Undo, Redo, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon, Check, Download } from 'lucide-react';
|
||||||
import { User, Template, FormField, FieldType, DEFAULT_FORM_FIELDS } from '../types';
|
import { User, Template, FormField, FieldType, DEFAULT_FORM_FIELDS } from '../types';
|
||||||
import { defaultReportContent } from '../utils/defaultContent';
|
import { defaultReportContent } from '../utils/defaultContent';
|
||||||
import { printDocument } from '../utils/print';
|
import { printDocument } from '../utils/print';
|
||||||
@@ -13,6 +13,7 @@ export default function TemplateManage() {
|
|||||||
const [templates, setTemplates] = useState<Template[]>([]);
|
const [templates, setTemplates] = useState<Template[]>([]);
|
||||||
const [currentTemplateId, setCurrentTemplateId] = useState<string | null>(null);
|
const [currentTemplateId, setCurrentTemplateId] = useState<string | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [exportModalOpen, setExportModalOpen] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [formData, setFormData] = useState({ name: '', desc: '' });
|
const [formData, setFormData] = useState({ name: '', desc: '' });
|
||||||
const [isSaved, setIsSaved] = useState(false);
|
const [isSaved, setIsSaved] = useState(false);
|
||||||
@@ -34,9 +35,11 @@ export default function TemplateManage() {
|
|||||||
const [editFieldTimeFormat, setEditFieldTimeFormat] = useState('');
|
const [editFieldTimeFormat, setEditFieldTimeFormat] = useState('');
|
||||||
const [editFieldTimeDefault, setEditFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
const [editFieldTimeDefault, setEditFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
||||||
const [editFieldFixedTimeValue, setEditFieldFixedTimeValue] = useState('');
|
const [editFieldFixedTimeValue, setEditFieldFixedTimeValue] = useState('');
|
||||||
|
const [editFieldHasUnderline, setEditFieldHasUnderline] = useState(true);
|
||||||
const [newFieldTimeFormat, setNewFieldTimeFormat] = useState('YYYY年MM月DD日');
|
const [newFieldTimeFormat, setNewFieldTimeFormat] = useState('YYYY年MM月DD日');
|
||||||
const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
||||||
const [newFieldFixedTimeValue, setNewFieldFixedTimeValue] = useState('');
|
const [newFieldFixedTimeValue, setNewFieldFixedTimeValue] = useState('');
|
||||||
|
const [newFieldHasUnderline, setNewFieldHasUnderline] = useState(true);
|
||||||
const [customTimeFormats, setCustomTimeFormats] = useState<string[]>([]);
|
const [customTimeFormats, setCustomTimeFormats] = useState<string[]>([]);
|
||||||
const [formatDropdownOpen, setFormatDropdownOpen] = useState(false);
|
const [formatDropdownOpen, setFormatDropdownOpen] = useState(false);
|
||||||
const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false);
|
const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false);
|
||||||
@@ -390,7 +393,8 @@ export default function TemplateManage() {
|
|||||||
}
|
}
|
||||||
pushHistory();
|
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>​`;
|
const underlineClass = field.hasUnderline === false ? ' no-underline' : '';
|
||||||
|
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value${underlineClass}" 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) {
|
||||||
@@ -459,6 +463,7 @@ export default function TemplateManage() {
|
|||||||
next.timeDefault = editFieldTimeDefault;
|
next.timeDefault = editFieldTimeDefault;
|
||||||
next.fixedTimeValue = editFieldFixedTimeValue;
|
next.fixedTimeValue = editFieldFixedTimeValue;
|
||||||
}
|
}
|
||||||
|
next.hasUnderline = editFieldHasUnderline;
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
setFormFields(updated);
|
setFormFields(updated);
|
||||||
@@ -476,6 +481,7 @@ export default function TemplateManage() {
|
|||||||
type: newFieldForm.type,
|
type: newFieldForm.type,
|
||||||
visibleInForm: true,
|
visibleInForm: true,
|
||||||
isSystemLocked: false,
|
isSystemLocked: false,
|
||||||
|
hasUnderline: newFieldHasUnderline,
|
||||||
options: ['单选', '多选'].includes(newFieldForm.category) && newFieldOptions.trim()
|
options: ['单选', '多选'].includes(newFieldForm.category) && newFieldOptions.trim()
|
||||||
? newFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean)
|
? newFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean)
|
||||||
: undefined
|
: undefined
|
||||||
@@ -493,6 +499,7 @@ export default function TemplateManage() {
|
|||||||
setNewFieldTimeFormat('YYYY年MM月DD日');
|
setNewFieldTimeFormat('YYYY年MM月DD日');
|
||||||
setNewFieldTimeDefault('specific');
|
setNewFieldTimeDefault('specific');
|
||||||
setNewFieldFixedTimeValue('');
|
setNewFieldFixedTimeValue('');
|
||||||
|
setNewFieldHasUnderline(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -720,6 +727,13 @@ export default function TemplateManage() {
|
|||||||
<Save size={16} />
|
<Save size={16} />
|
||||||
保存模板
|
保存模板
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setExportModalOpen(true)}
|
||||||
|
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
||||||
|
title="下载"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => editorRef.current && printDocument(editorRef.current.innerHTML)}
|
onClick={() => editorRef.current && printDocument(editorRef.current.innerHTML)}
|
||||||
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
className="p-2.5 rounded-lg bg-slate-100 text-text-muted hover:bg-slate-200 transition-colors"
|
||||||
@@ -887,6 +901,7 @@ export default function TemplateManage() {
|
|||||||
setEditFieldTimeFormat(field.timeFormat || '');
|
setEditFieldTimeFormat(field.timeFormat || '');
|
||||||
setEditFieldTimeDefault(field.timeDefault || 'specific');
|
setEditFieldTimeDefault(field.timeDefault || 'specific');
|
||||||
setEditFieldFixedTimeValue(field.fixedTimeValue || '');
|
setEditFieldFixedTimeValue(field.fixedTimeValue || '');
|
||||||
|
setEditFieldHasUnderline(field.hasUnderline !== false);
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
@@ -991,6 +1006,10 @@ export default function TemplateManage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<label className="flex items-center gap-1 text-[10px] text-slate-600 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={editFieldHasUnderline} onChange={(e) => setEditFieldHasUnderline(e.target.checked)} />
|
||||||
|
打印时显示下划线
|
||||||
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => saveFieldEdit(field.key)}
|
onClick={() => saveFieldEdit(field.key)}
|
||||||
@@ -1187,6 +1206,10 @@ export default function TemplateManage() {
|
|||||||
className="w-full px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent"
|
className="w-full px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<label className="flex items-center gap-1 text-xs text-slate-600 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={newFieldHasUnderline} onChange={(e) => setNewFieldHasUnderline(e.target.checked)} />
|
||||||
|
打印时显示下划线
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={addField}
|
onClick={addField}
|
||||||
className="w-full py-1.5 bg-accent text-white text-xs font-semibold rounded hover:opacity-90 transition-colors"
|
className="w-full py-1.5 bg-accent text-white text-xs font-semibold rounded hover:opacity-90 transition-colors"
|
||||||
@@ -1201,6 +1224,45 @@ export default function TemplateManage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{exportModalOpen && (
|
||||||
|
<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 ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const name = currentTemplate?.name || '模板';
|
||||||
|
printDocument(editorRef.current?.innerHTML || '', `${name}-${ts}`);
|
||||||
|
setExportModalOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full py-2.5 bg-accent text-white rounded text-sm font-semibold hover:opacity-90 transition-colors"
|
||||||
|
>导出 PDF</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const name = currentTemplate?.name || '模板';
|
||||||
|
const data = currentTemplate ? { ...currentTemplate, content: editorRef.current?.innerHTML } : { content: editorRef.current?.innerHTML };
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${name}-${ts}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
setExportModalOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||||
|
>导出 JSON</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setExportModalOpen(false)}
|
||||||
|
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||||
|
>取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
<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-10 w-full max-w-[500px] shadow-2xl border border-border">
|
<div className="bg-white rounded-2xl p-10 w-full max-w-[500px] shadow-2xl border border-border">
|
||||||
@@ -1293,11 +1355,11 @@ export default function TemplateManage() {
|
|||||||
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
||||||
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
||||||
} else {
|
} else {
|
||||||
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;';
|
let styleStr = 'display:inline-block;text-align:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;';
|
||||||
styleStr += `width:${w}px;height:${h}px;`;
|
styleStr += `width:${w}px;height:${h}px;line-height:${h}px;`;
|
||||||
const showShortText = w > 0 && w < 80;
|
const showShortText = w > 0 && w < 80;
|
||||||
const text = showShortText ? '插图' : hintText;
|
const text = showShortText ? '插图' : hintText;
|
||||||
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} 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;">${text}</span></span>​`;
|
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;display:inline-block;vertical-align:middle;line-height:normal;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${text}</span></span>​`;
|
||||||
}
|
}
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = html;
|
wrapper.innerHTML = html;
|
||||||
|
|||||||
@@ -116,11 +116,12 @@ export interface FormField {
|
|||||||
timeFormat?: string;
|
timeFormat?: string;
|
||||||
timeDefault?: 'current' | 'specific';
|
timeDefault?: 'current' | 'specific';
|
||||||
fixedTimeValue?: string;
|
fixedTimeValue?: string;
|
||||||
|
hasUnderline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_FORM_FIELDS: FormField[] = [
|
export const DEFAULT_FORM_FIELDS: FormField[] = [
|
||||||
{ key: 'patientName', label: '患者姓名', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true },
|
{ key: 'patientName', label: '患者姓名', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true, hasUnderline: true },
|
||||||
{ key: 'hospitalId', label: '住院号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true },
|
{ key: 'hospitalId', label: '住院号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true, hasUnderline: true },
|
||||||
{ key: 'title', label: '手术名称', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
{ key: 'title', label: '手术名称', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||||
{ key: 'patientGender', label: '患者性别', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['男', '女'] },
|
{ key: 'patientGender', label: '患者性别', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['男', '女'] },
|
||||||
{ key: 'patientAge', label: '患者年龄', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
{ key: 'patientAge', label: '患者年龄', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ export const defaultReportContent = `
|
|||||||
<strong>冰冻病理结果</strong>:${smartField('frozenPathology')}
|
<strong>冰冻病理结果</strong>:${smartField('frozenPathology')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
<p style="text-align: right; font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0; white-space: nowrap;">
|
||||||
手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-flex;align-items:center;justify-content:center;width:200px;height:40px;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>
|
手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-block;text-align:center;width:200px;height:40px;line-height:40px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;display:inline-block;vertical-align:middle;line-height:normal;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0;">
|
<p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const printDocument = (htmlContent: string) => {
|
export const printDocument = (htmlContent: string, docTitle: string = '图文报告') => {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.style.position = 'fixed';
|
iframe.style.position = 'fixed';
|
||||||
iframe.style.right = '0';
|
iframe.style.right = '0';
|
||||||
@@ -40,6 +40,7 @@ export const printDocument = (htmlContent: string) => {
|
|||||||
.report-signature-img { max-width: 120px; max-height: 40px; width: auto; height: auto; object-fit: contain; vertical-align: middle; display: inline-block; }
|
.report-signature-img { max-width: 120px; max-height: 40px; width: auto; height: auto; object-fit: contain; vertical-align: middle; display: inline-block; }
|
||||||
@media print {
|
@media print {
|
||||||
.smart-field-wrapper .field-value { border: none !important; border-bottom: 1px solid #000 !important; border-radius: 0 !important; background: transparent !important; padding: 0 2px !important; }
|
.smart-field-wrapper .field-value { border: none !important; border-bottom: 1px solid #000 !important; border-radius: 0 !important; background: transparent !important; padding: 0 2px !important; }
|
||||||
|
.smart-field-wrapper .field-value.no-underline { border-bottom: none !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
91
工程分析/实现方案-2026-04-18-18-36-43.md
Normal file
91
工程分析/实现方案-2026-04-18-18-36-43.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-18-36-43
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
实现五项系统改进:列名修正、字段下划线控制、下载导出、右对齐排版修复、默认模板签名右对齐。
|
||||||
|
|
||||||
|
## 需求 1:ReportManage 列名修正
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportManage.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
找到 `<thead>` 中「患者号」`<th>`,将文本改为「住院号」。同步检查表格数据渲染中是否有对应的 patientId/hospitalId 显示逻辑需调整。
|
||||||
|
|
||||||
|
## 需求 2:字段管理增加下划线控制
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/types.ts`
|
||||||
|
- `src/pages/TemplateManage.tsx`
|
||||||
|
- `src/utils/print.ts`
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. **扩展 FormField 接口**:增加 `hasUnderline?: boolean`(默认 `true`)。
|
||||||
|
2. **修改 DEFAULT_FORM_FIELDS**:为所有默认字段设置 `hasUnderline: true`。
|
||||||
|
3. **TemplateManage 字段管理 UI**:
|
||||||
|
- 新增字段表单中增加「打印时显示下划线」checkbox,默认勾选。
|
||||||
|
- 编辑字段面板中同样增加该 checkbox。
|
||||||
|
- 保存字段配置时将 `hasUnderline` 写入 `formFieldsConfig`。
|
||||||
|
4. **insertSmartField 注入类名**:
|
||||||
|
- 在生成 `smart-field-wrapper` HTML 时,若 `field.hasUnderline === false`,给 `.field-value` 增加 `no-underline` 类。
|
||||||
|
5. **print.ts 打印样式**:
|
||||||
|
- 在 `@media print` 中增加 `.smart-field-wrapper .field-value.no-underline { border-bottom: none !important; }`
|
||||||
|
|
||||||
|
## 需求 3:ReportEditor / TemplateManage 新增下载按钮
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/pages/ReportEditor.tsx`
|
||||||
|
- `src/pages/TemplateManage.tsx`
|
||||||
|
- `src/utils/print.ts`
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. **print.ts 支持自定义标题**:
|
||||||
|
- `printDocument(htmlContent: string, docTitle?: string)` 增加可选 `docTitle` 参数。
|
||||||
|
- 在 iframe HTML 的 `<head>` 中注入 `<title>${docTitle || '图文报告'}</title>`,使浏览器保存 PDF 时使用该文件名。
|
||||||
|
2. **ReportEditor 下载功能**:
|
||||||
|
- 引入 `Download` 图标。
|
||||||
|
- 在顶部操作栏打印按钮旁增加下载按钮。
|
||||||
|
- 新增 `exportModalOpen` 状态控制导出弹窗。
|
||||||
|
- 实现 `getExportFilename()`:基于 `reportData.title`、`patientName`、`hospitalId` 和当前时间生成文件名。
|
||||||
|
- 实现 `downloadJSON()`:将 `reportData` 序列化为 JSON Blob 并触发下载。
|
||||||
|
- 导出 PDF 时调用 `printDocument(editorRef.current.innerHTML, getExportFilename())`。
|
||||||
|
3. **TemplateManage 下载功能**:
|
||||||
|
- 类似实现。模板管理页面没有 reportData,文件名中患者信息使用"模板"或空值替代。
|
||||||
|
- PDF 导出调用 `printDocument(editorRef.current.innerHTML, filename)`。
|
||||||
|
- JSON 导出下载模板内容。
|
||||||
|
|
||||||
|
## 需求 4:修复右对齐时签名与图片框分离
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `src/pages/TemplateManage.tsx`(占位符插入逻辑)
|
||||||
|
- `src/pages/ReportEditor.tsx`(占位符插入逻辑,如有)
|
||||||
|
- `src/utils/defaultContent.ts`(默认模板签名占位符)
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
将 `display: inline-flex` 改为 `display: inline-block`,并通过 `line-height` 实现垂直居中:
|
||||||
|
- **运行时插入**:`styleStr` 从 `display:inline-flex;align-items:center;justify-content:center;` 改为 `display:inline-block;text-align:center;position:relative;line-height:${h}px;`
|
||||||
|
- **占位文本**:`.placeholder-text` 增加 `display:inline-block;vertical-align:middle;line-height:normal;`
|
||||||
|
- **默认模板**:手术者签名占位符同步应用上述样式。
|
||||||
|
|
||||||
|
## 需求 5:默认模板手术者签名右对齐
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将「手术者签名」`<p>` 增加 `text-align: right;`,并应用需求 4 的 `inline-block` 样式修复。
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/pages/ReportManage.tsx` | 「患者号」→「住院号」 |
|
||||||
|
| `src/types.ts` | `FormField` 增加 `hasUnderline?: boolean` |
|
||||||
|
| `src/pages/TemplateManage.tsx` | 字段管理 UI 增加下划线 checkbox;insertSmartField 注入 no-underline 类;工具栏增加下载按钮和弹窗 |
|
||||||
|
| `src/pages/ReportEditor.tsx` | 工具栏增加下载按钮和弹窗;占位符插入样式改为 inline-block |
|
||||||
|
| `src/utils/print.ts` | 增加 `docTitle` 参数;打印样式支持 `.no-underline` |
|
||||||
|
| `src/utils/defaultContent.ts` | 签名占位符改为 inline-block;签名行设为 `text-align: right` |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `FormField` 接口扩展后,需确保 `DEFAULT_FORM_FIELDS` 和所有已有字段配置(localStorage 中的 `formFieldsConfig`)兼容。对于旧数据缺少 `hasUnderline` 的情况,按 `true` 处理。
|
||||||
|
2. `printDocument` 增加 `docTitle` 参数后,需检查所有调用方是否已更新。现有调用方(如 ReportView)可保持默认行为。
|
||||||
|
3. `inline-block` 替换 `inline-flex` 后,需验证占位符在非右对齐场景(如正常左对齐)下的垂直居中效果是否正常。
|
||||||
|
4. 下载 JSON 时,TemplateManage 的 JSON 内容与 ReportEditor 不同(模板 vs 报告),需分别处理。
|
||||||
117
工程分析/测试方案-2026-04-18-18-36-43.md
Normal file
117
工程分析/测试方案-2026-04-18-18-36-43.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-18-36-43
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证五项系统改进:列名修正、字段下划线控制、下载导出、右对齐排版修复、默认模板签名右对齐。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:ReportManage 列名显示
|
||||||
|
**前置条件**:进入 /report-manage
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看表格表头
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 表头显示为「住院号」而非「患者号」
|
||||||
|
- 数据列正确显示 hospitalId 值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:字段管理下划线开关
|
||||||
|
**前置条件**:进入 /template-manage,点击字段管理
|
||||||
|
**操作步骤**:
|
||||||
|
1. 新建一个字段
|
||||||
|
2. 观察「打印时显示下划线」checkbox,默认应为勾选
|
||||||
|
3. 取消勾选并保存
|
||||||
|
4. 将该字段插入模板
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 新建字段表单中有「打印时显示下划线」选项
|
||||||
|
- 编辑字段时也可修改该选项
|
||||||
|
- 取消下划线的字段插入后,`.field-value` 带有 `no-underline` 类
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:打印时下划线控制
|
||||||
|
**前置条件**:模板中有带/不带下划线的字段
|
||||||
|
**操作步骤**:
|
||||||
|
1. 进入 report-editor,新建报告
|
||||||
|
2. 填写字段内容
|
||||||
|
3. 点击打印
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 默认勾选下划线的字段,打印时 `.field-value` 底部有黑色下划线
|
||||||
|
- 取消下划线的字段,打印时 `.field-value` 底部无下划线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:ReportEditor 下载按钮
|
||||||
|
**前置条件**:进入 /report-editor,有内容的报告
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击顶部下载按钮
|
||||||
|
2. 在弹窗中选择「导出 PDF」
|
||||||
|
3. 在弹窗中选择「导出 JSON」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 弹窗正常显示两个导出选项
|
||||||
|
- PDF 导出时浏览器保存对话框的文件名包含「图文报告-{手术名称}-{患者}-{住院号}-{时间}」
|
||||||
|
- JSON 导出时下载的文件名格式同上,内容包含 reportData
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:TemplateManage 下载按钮
|
||||||
|
**前置条件**:进入 /template-manage
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击顶部下载按钮
|
||||||
|
2. 选择导出 PDF/JSON
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 导出功能正常
|
||||||
|
- 文件名格式合理(模板名称 + 时间)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:右对齐时签名不换行
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 找到「手术者签名」行
|
||||||
|
2. 选中该行,点击工具栏「右对齐」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 「手术者签名:」文字与图片占位符在同一行
|
||||||
|
- 两者一起靠右对齐
|
||||||
|
- 图片框不会单独换到下一行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-07:默认模板签名右对齐
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看报告底部「手术者签名」行
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 默认即为右对齐
|
||||||
|
- 文字与图片框在同一行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-08:占位符 inline-block 样式
|
||||||
|
**前置条件**:在 template-manage 中插入静态图片占位符
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击工具栏「插入图片占位符」
|
||||||
|
2. 选择「静态图片占位」
|
||||||
|
3. 确认插入
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 占位符的 style 中 `display` 为 `inline-block` 而非 `inline-flex`
|
||||||
|
- 占位符在编辑器中正常显示,垂直居中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 验证所有现有字段(默认模板中的)打印时仍显示下划线
|
||||||
|
- 验证 smart-field-wrapper 双向绑定正常工作
|
||||||
|
- 验证 image-placeholder 点击上传、拖拽填充、删除功能正常
|
||||||
|
- 验证 report-manage 的搜索、筛选、批量操作不受影响
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
TC-01~TC-08 全部通过,即可确认所有需求均正确实现。
|
||||||
39
工程分析/需求分析-2026-04-18-18-36-43.md
Normal file
39
工程分析/需求分析-2026-04-18-18-36-43.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-18-36-43
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户提出报告管理列名修正、字段下划线控制、下载功能、右对齐排版修复及默认模板调整等五项改进需求。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:ReportManage 列名"患者号"改为"住院号"
|
||||||
|
报告管理列表中表头显示为"患者号",但实际数据对应的是住院号字段,需修正列名。
|
||||||
|
|
||||||
|
### 需求 2:TemplateManage 字段管理增加"下划线"控制
|
||||||
|
在模板管理的字段管理面板中,每个字段增加"打印时显示下划线"单选框,默认勾选。若取消勾选,则该字段在打印输出时不显示底部下划线。需在 `FormField` 数据结构中增加 `hasUnderline` 属性,并在打印样式中支持 `.no-underline` 类。
|
||||||
|
|
||||||
|
### 需求 3:ReportEditor / TemplateManage 新增下载按钮
|
||||||
|
在报告编辑器和模板管理页面的打印按钮旁新增"下载"按钮,点击弹出模态框,支持导出 PDF 和 JSON:
|
||||||
|
- PDF:复用现有 `printDocument()`,传入自定义文件名
|
||||||
|
- JSON:通过 `Blob` + `URL.createObjectURL` 实现下载
|
||||||
|
- 默认文件名格式:`图文报告-{手术名称}-{患者}-{住院号}-{下载时间}.pdf/.json`
|
||||||
|
|
||||||
|
### 需求 4:修复右对齐时签名与图片框分离
|
||||||
|
当编辑器中设置右对齐时,"手术者签名:"文字与 `class="image-placeholder"` 图片框被拆分为两行。根本原因是 `display: inline-flex` 在右对齐布局下容易触发换行。需将运行时插入的占位符以及默认模板中的占位符 `display` 属性从 `inline-flex` 改为 `inline-block`,并配合 `line-height` 垂直居中。
|
||||||
|
|
||||||
|
### 需求 5:默认模板手术者签名右对齐
|
||||||
|
将 `defaultContent.ts` 中「手术者签名」行默认设为 `text-align: right`,并应用需求 4 中的 `inline-block` 修复。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/pages/ReportManage.tsx`(需求 1)
|
||||||
|
- `src/types.ts`(需求 2:FormField 扩展)
|
||||||
|
- `src/utils/print.ts`(需求 2、3:打印样式 + 文件名支持)
|
||||||
|
- `src/pages/TemplateManage.tsx`(需求 2、3、4)
|
||||||
|
- `src/pages/ReportEditor.tsx`(需求 3、4)
|
||||||
|
- `src/utils/defaultContent.ts`(需求 4、5)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 报告管理列表展示
|
||||||
|
- 模板字段配置体系
|
||||||
|
- 编辑器/模板管理器工具栏交互
|
||||||
|
- 打印输出样式
|
||||||
|
- 文件导出功能
|
||||||
Reference in New Issue
Block a user