Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32f8b2a7ec | ||
|
|
519cc6fc82 | ||
|
|
4a7051b6db | ||
|
|
5f4ae1ff29 | ||
|
|
db1c11f7eb | ||
|
|
55ce78d898 | ||
|
|
e1dc961ecf | ||
|
|
67fb2c9080 | ||
|
|
a46ecffadf |
@@ -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,11 +48,13 @@ 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);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'info' | 'video'>('info');
|
const [activeTab, setActiveTab] = useState<'info' | 'video'>('info');
|
||||||
|
const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);
|
||||||
const [multiSelectOptions, setMultiSelectOptions] = useState<Record<string, string[]>>({
|
const [multiSelectOptions, setMultiSelectOptions] = useState<Record<string, string[]>>({
|
||||||
surgeon: ['张医生', '李医生', '王医生'],
|
surgeon: ['张医生', '李医生', '王医生'],
|
||||||
assistant: ['赵医生', '钱医生', '孙医生'],
|
assistant: ['赵医生', '钱医生', '孙医生'],
|
||||||
@@ -377,6 +379,28 @@ export default function ReportEditor() {
|
|||||||
const targetEl = node as HTMLElement | null;
|
const targetEl = node as HTMLElement | null;
|
||||||
if (!targetEl) return;
|
if (!targetEl) return;
|
||||||
|
|
||||||
|
// Handle click on field-value: switch to info tab, highlight and focus corresponding input
|
||||||
|
const fieldValue = targetEl.closest('.field-value') as HTMLElement | null;
|
||||||
|
if (fieldValue) {
|
||||||
|
const bindKey = fieldValue.getAttribute('data-bind');
|
||||||
|
if (bindKey) {
|
||||||
|
setActiveTab('info');
|
||||||
|
stateRef.current = { ...stateRef.current, activeTab: 'info' };
|
||||||
|
setActiveFieldKey(bindKey);
|
||||||
|
setTimeout(() => {
|
||||||
|
const inputEl = document.getElementById(`input-${bindKey}`);
|
||||||
|
if (inputEl) {
|
||||||
|
inputEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
const focusable = inputEl.querySelector('input, select') as HTMLElement | null;
|
||||||
|
if (focusable) {
|
||||||
|
focusable.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const placeholder = targetEl.closest('.image-placeholder') as HTMLElement | null;
|
const placeholder = targetEl.closest('.image-placeholder') as HTMLElement | null;
|
||||||
if (!placeholder) return;
|
if (!placeholder) return;
|
||||||
|
|
||||||
@@ -465,13 +489,24 @@ export default function ReportEditor() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
||||||
|
const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
|
||||||
|
const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
|
||||||
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%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
|
<img src="${src}" style="max-width:${mw};max-height:${mh};display:block;object-fit:contain;object-position:left top;" draggable="false">
|
||||||
`;
|
`;
|
||||||
placeholder.classList.add('has-image');
|
placeholder.classList.add('has-image');
|
||||||
placeholder.style.border = 'none';
|
placeholder.style.border = 'none';
|
||||||
placeholder.style.background = 'transparent';
|
placeholder.style.background = 'transparent';
|
||||||
|
placeholder.style.width = 'auto';
|
||||||
|
placeholder.style.height = 'auto';
|
||||||
|
placeholder.style.lineHeight = 'normal';
|
||||||
|
placeholder.style.maxWidth = mw;
|
||||||
|
placeholder.style.maxHeight = mh;
|
||||||
|
placeholder.style.textAlign = 'left';
|
||||||
|
placeholder.style.verticalAlign = 'top';
|
||||||
|
placeholder.style.justifyContent = 'flex-start';
|
||||||
|
placeholder.style.alignItems = 'flex-start';
|
||||||
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
saveDraftToStorage();
|
saveDraftToStorage();
|
||||||
};
|
};
|
||||||
@@ -484,6 +519,19 @@ export default function ReportEditor() {
|
|||||||
saveDraftToStorage();
|
saveDraftToStorage();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeLineHeight = (height: string) => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || !sel.rangeCount) return;
|
||||||
|
let node = sel.getRangeAt(0).commonAncestorContainer;
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode as Node;
|
||||||
|
const block = (node as HTMLElement).closest('p, div, td, h1, h2, h3, li');
|
||||||
|
if (block) {
|
||||||
|
(block as HTMLElement).style.lineHeight = height;
|
||||||
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
|
saveDraftToStorage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const insertTable = () => {
|
const insertTable = () => {
|
||||||
editorRef.current?.focus();
|
editorRef.current?.focus();
|
||||||
setTableModal({ isOpen: true, rows: '2', cols: '3' });
|
setTableModal({ isOpen: true, rows: '2', cols: '3' });
|
||||||
@@ -624,11 +672,20 @@ export default function ReportEditor() {
|
|||||||
if (emptyPlaceholder) {
|
if (emptyPlaceholder) {
|
||||||
emptyPlaceholder.innerHTML = `
|
emptyPlaceholder.innerHTML = `
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<span class="delete-btn" contenteditable="false">×</span>
|
||||||
<img src="${newFrame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
|
<img src="${newFrame.dataUrl}" style="max-width:${emptyPlaceholder.style.maxWidth || emptyPlaceholder.style.width || '200px'};max-height:${emptyPlaceholder.style.maxHeight || emptyPlaceholder.style.height || '200px'};display:block;object-fit:contain;object-position:left top;" draggable="false">
|
||||||
`;
|
`;
|
||||||
emptyPlaceholder.classList.add('has-image');
|
emptyPlaceholder.classList.add('has-image');
|
||||||
emptyPlaceholder.style.border = 'none';
|
emptyPlaceholder.style.border = 'none';
|
||||||
emptyPlaceholder.style.background = 'transparent';
|
emptyPlaceholder.style.background = 'transparent';
|
||||||
|
emptyPlaceholder.style.width = 'auto';
|
||||||
|
emptyPlaceholder.style.height = 'auto';
|
||||||
|
emptyPlaceholder.style.lineHeight = 'normal';
|
||||||
|
emptyPlaceholder.style.maxWidth = emptyPlaceholder.style.maxWidth || emptyPlaceholder.style.width || '200px';
|
||||||
|
emptyPlaceholder.style.maxHeight = emptyPlaceholder.style.maxHeight || emptyPlaceholder.style.height || '200px';
|
||||||
|
emptyPlaceholder.style.textAlign = 'left';
|
||||||
|
emptyPlaceholder.style.verticalAlign = 'top';
|
||||||
|
emptyPlaceholder.style.justifyContent = 'flex-start';
|
||||||
|
emptyPlaceholder.style.alignItems = 'flex-start';
|
||||||
contentRef.current = editorRef.current.innerHTML;
|
contentRef.current = editorRef.current.innerHTML;
|
||||||
saveDraftToStorage();
|
saveDraftToStorage();
|
||||||
}
|
}
|
||||||
@@ -653,13 +710,24 @@ export default function ReportEditor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
|
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
|
||||||
|
const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
|
||||||
|
const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
|
||||||
placeholder.innerHTML = `
|
placeholder.innerHTML = `
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<span class="delete-btn" contenteditable="false">×</span>
|
||||||
<img src="${frame.dataUrl}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
|
<img src="${frame.dataUrl}" style="max-width:${mw};max-height:${mh};display:block;object-fit:contain;object-position:left top;" draggable="false">
|
||||||
`;
|
`;
|
||||||
placeholder.classList.add('has-image');
|
placeholder.classList.add('has-image');
|
||||||
placeholder.style.border = 'none';
|
placeholder.style.border = 'none';
|
||||||
placeholder.style.background = 'transparent';
|
placeholder.style.background = 'transparent';
|
||||||
|
placeholder.style.width = 'auto';
|
||||||
|
placeholder.style.height = 'auto';
|
||||||
|
placeholder.style.lineHeight = 'normal';
|
||||||
|
placeholder.style.maxWidth = mw;
|
||||||
|
placeholder.style.maxHeight = mh;
|
||||||
|
placeholder.style.textAlign = 'left';
|
||||||
|
placeholder.style.verticalAlign = 'top';
|
||||||
|
placeholder.style.justifyContent = 'flex-start';
|
||||||
|
placeholder.style.alignItems = 'flex-start';
|
||||||
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
saveDraftToStorage();
|
saveDraftToStorage();
|
||||||
};
|
};
|
||||||
@@ -1272,6 +1340,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"
|
||||||
@@ -1292,6 +1367,7 @@ export default function ReportEditor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
||||||
<select
|
<select
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onChange={(e) => { execCmd('fontName', e.target.value); e.target.value = ''; }}
|
onChange={(e) => { execCmd('fontName', e.target.value); e.target.value = ''; }}
|
||||||
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
||||||
>
|
>
|
||||||
@@ -1301,6 +1377,27 @@ export default function ReportEditor() {
|
|||||||
<option value="SimHei">黑体</option>
|
<option value="SimHei">黑体</option>
|
||||||
<option value="KaiTi">楷体</option>
|
<option value="KaiTi">楷体</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onChange={(e) => { if (e.target.value) { execCmd('fontSize', e.target.value); } e.target.value = ''; }}
|
||||||
|
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
||||||
|
>
|
||||||
|
<option value="">字号</option>
|
||||||
|
<option value="3">12pt</option>
|
||||||
|
<option value="4">14pt</option>
|
||||||
|
<option value="5">18pt</option>
|
||||||
|
<option value="6">24pt</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onChange={(e) => { if (e.target.value) { changeLineHeight(e.target.value); } e.target.value = ''; }}
|
||||||
|
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
||||||
|
>
|
||||||
|
<option value="">行距</option>
|
||||||
|
<option value="1">1.0</option>
|
||||||
|
<option value="1.5">1.5</option>
|
||||||
|
<option value="2">2.0</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
||||||
<button onClick={() => execCmd('bold')} className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-white text-text-muted hover:text-text-main transition-colors" title="粗体"><Bold size={16} /></button>
|
<button onClick={() => execCmd('bold')} className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-white text-text-muted hover:text-text-main transition-colors" title="粗体"><Bold size={16} /></button>
|
||||||
@@ -1366,14 +1463,30 @@ export default function ReportEditor() {
|
|||||||
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||||
{activeTab === 'info' && (
|
{activeTab === 'info' && (
|
||||||
<div className="report-info-form space-y-4">
|
<div className="report-info-form space-y-4">
|
||||||
{formFields.filter(f => f.visibleInForm).map(field => {
|
{(() => {
|
||||||
|
const topKeys = ['patientName', 'hospitalId', 'title'];
|
||||||
|
const contentHtml = contentRef.current || editorRef.current?.innerHTML || '';
|
||||||
|
return [...formFields.filter(f => f.visibleInForm)].sort((a, b) => {
|
||||||
|
const aTop = topKeys.indexOf(a.key);
|
||||||
|
const bTop = topKeys.indexOf(b.key);
|
||||||
|
if (aTop !== -1 && bTop !== -1) return aTop - bTop;
|
||||||
|
if (aTop !== -1) return -1;
|
||||||
|
if (bTop !== -1) return 1;
|
||||||
|
const aIndex = contentHtml.indexOf(`data-bind="${a.key}"`);
|
||||||
|
const bIndex = contentHtml.indexOf(`data-bind="${b.key}"`);
|
||||||
|
if (aIndex === -1 && bIndex === -1) return 0;
|
||||||
|
if (aIndex === -1) return 1;
|
||||||
|
if (bIndex === -1) return -1;
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
})().map(field => {
|
||||||
const isRequired = field.isSystemLocked;
|
const isRequired = field.isSystemLocked;
|
||||||
const hasError = isRequired && touched[field.key] && !(reportData as any)[field.key];
|
const hasError = isRequired && touched[field.key] && !(reportData as any)[field.key];
|
||||||
|
|
||||||
if (field.type === 'text' || field.type === 'date') {
|
if (field.type === 'text' || field.type === 'date') {
|
||||||
const inputType = field.type === 'date' ? 'date' : 'text';
|
const inputType = field.type === 'date' ? 'date' : 'text';
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className={field.category === '填空' && formFields.filter(f2 => f2.visibleInForm && f2.type === 'text' && f2.isSystemLocked).length > 1 && (field.key === 'patientName' || field.key === 'hospitalId') ? 'flex-1 space-y-1' : 'space-y-1'}>
|
<div key={field.key} id={`input-${field.key}`} className={`${field.category === '填空' && formFields.filter(f2 => f2.visibleInForm && f2.type === 'text' && f2.isSystemLocked).length > 1 && (field.key === 'patientName' || field.key === 'hospitalId') ? 'flex-1 space-y-1' : 'space-y-1'} p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
|
||||||
<label className="block text-xs font-bold text-text-main">
|
<label className="block text-xs font-bold text-text-main">
|
||||||
{field.label} {isRequired && <span className="text-red-500">*</span>}
|
{field.label} {isRequired && <span className="text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
@@ -1393,7 +1506,7 @@ export default function ReportEditor() {
|
|||||||
const isOpen = openDropdown === field.key;
|
const isOpen = openDropdown === field.key;
|
||||||
const opts = field.options || (field.key === 'anesthesiaType' ? anesthesiaOptions : []);
|
const opts = field.options || (field.key === 'anesthesiaType' ? anesthesiaOptions : []);
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className="space-y-1 select-dropdown-root relative">
|
<div key={field.key} id={`input-${field.key}`} className={`space-y-1 select-dropdown-root relative p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
|
||||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||||
<div
|
<div
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text"
|
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text"
|
||||||
@@ -1502,7 +1615,7 @@ export default function ReportEditor() {
|
|||||||
const currentInputText = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText;
|
const currentInputText = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className="space-y-1 select-dropdown-root relative">
|
<div key={field.key} id={`input-${field.key}`} className={`space-y-1 select-dropdown-root relative p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
|
||||||
<label className="block text-xs font-bold text-text-main">{field.label}(可多选)</label>
|
<label className="block text-xs font-bold text-text-main">{field.label}(可多选)</label>
|
||||||
<div
|
<div
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text"
|
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text"
|
||||||
@@ -1583,7 +1696,7 @@ export default function ReportEditor() {
|
|||||||
const { h: h12, isPM } = from24h(h24val);
|
const { h: h12, isPM } = from24h(h24val);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className="space-y-1">
|
<div key={field.key} id={`input-${field.key}`} className={`space-y-1 p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
|
||||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
@@ -1640,7 +1753,7 @@ export default function ReportEditor() {
|
|||||||
const { h: h12g, isPM: isPMg } = from24h(h24);
|
const { h: h12g, isPM: isPMg } = from24h(h24);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className="space-y-1">
|
<div key={field.key} id={`input-${field.key}`} className="space-y-1">
|
||||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
@@ -1697,7 +1810,7 @@ export default function ReportEditor() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'video' && (
|
{activeTab === 'video' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<input
|
<input
|
||||||
ref={videoInputRef}
|
ref={videoInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1706,49 +1819,44 @@ export default function ReportEditor() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleVideoUpload}
|
onChange={handleVideoUpload}
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
onClick={() => videoInputRef.current?.click()}
|
|
||||||
className="w-full flex items-center justify-center gap-2 p-3 border border-dashed border-border rounded-lg hover:border-accent hover:bg-slate-50 transition-all"
|
|
||||||
>
|
|
||||||
<Video size={18} />
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-xs font-bold text-text-main">点击上传手术视频</p>
|
|
||||||
<p className="text-[10px] text-text-muted">支持 MP4, MOV 格式</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{videos.length > 0 && (
|
<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar items-center">
|
||||||
<div className="space-y-4">
|
<button
|
||||||
<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar">
|
onClick={() => videoInputRef.current?.click()}
|
||||||
{videos.map((v, i) => (
|
className="shrink-0 w-24 h-[68px] flex flex-col items-center justify-center gap-1 border-2 border-dashed border-border rounded-xl hover:border-accent hover:bg-slate-50 transition-all text-text-muted hover:text-accent"
|
||||||
<div
|
>
|
||||||
key={v.id}
|
<Video size={18} />
|
||||||
className={`shrink-0 w-24 p-1.5 border-2 rounded-xl cursor-pointer transition-all relative group ${
|
<span className="text-[10px] font-bold">上传视频</span>
|
||||||
currentVideoIndex === i ? 'border-accent bg-white shadow-sm' : 'border-transparent'
|
</button>
|
||||||
}`}
|
{videos.map((v, i) => (
|
||||||
>
|
<div
|
||||||
<div
|
key={v.id}
|
||||||
onClick={() => selectVideo(i)}
|
className={`shrink-0 w-24 p-1.5 border-2 rounded-xl cursor-pointer transition-all relative group ${
|
||||||
className="aspect-video bg-slate-900 rounded-lg flex items-center justify-center text-white"
|
currentVideoIndex === i ? 'border-accent bg-white shadow-sm' : 'border-transparent'
|
||||||
>
|
}`}
|
||||||
<Play size={16} />
|
>
|
||||||
</div>
|
<div
|
||||||
<div
|
onClick={() => selectVideo(i)}
|
||||||
onClick={() => selectVideo(i)}
|
className="aspect-video bg-slate-900 rounded-lg flex items-center justify-center text-white"
|
||||||
className="text-[9px] font-bold text-text-main truncate mt-1.5 px-1"
|
>
|
||||||
>{v.name}</div>
|
<Play size={16} />
|
||||||
<button
|
</div>
|
||||||
onClick={() => removeVideo(v.id)}
|
<div
|
||||||
className="absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-[10px] opacity-0 group-hover:opacity-100 transition-all shadow-sm"
|
onClick={() => selectVideo(i)}
|
||||||
>
|
className="text-[9px] font-bold text-text-main truncate mt-1.5 px-1"
|
||||||
<X size={12} />
|
>{v.name}</div>
|
||||||
</button>
|
<button
|
||||||
</div>
|
onClick={() => removeVideo(v.id)}
|
||||||
))}
|
className="absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-[10px] opacity-0 group-hover:opacity-100 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{currentVideoIndex !== -1 && (
|
{currentVideoIndex !== -1 && videos.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<div className="relative bg-slate-900 rounded-2xl overflow-hidden aspect-video shadow-lg">
|
<div className="relative bg-slate-900 rounded-2xl overflow-hidden aspect-video shadow-lg">
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
@@ -1786,7 +1894,7 @@ export default function ReportEditor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2">
|
<div className="flex justify-between items-center pt-1 border-t border-border">
|
||||||
<span className="text-[10px] font-bold text-text-main uppercase tracking-wider">关键帧摘取</span>
|
<span className="text-[10px] font-bold text-text-main uppercase tracking-wider">关键帧摘取</span>
|
||||||
<button
|
<button
|
||||||
onClick={captureFrame}
|
onClick={captureFrame}
|
||||||
@@ -1837,8 +1945,6 @@ export default function ReportEditor() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1893,11 +1999,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;max-width:${w}px;max-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});
|
||||||
@@ -1949,6 +2055,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);
|
||||||
@@ -140,13 +143,24 @@ export default function TemplateManage() {
|
|||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
|
||||||
|
const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
|
||||||
|
const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
|
||||||
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%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
|
<img src="${src}" style="max-width:${mw};max-height:${mh};display:block;object-fit:contain;object-position:left top;" draggable="false">
|
||||||
`;
|
`;
|
||||||
placeholder.classList.add('has-image');
|
placeholder.classList.add('has-image');
|
||||||
placeholder.style.border = 'none';
|
placeholder.style.border = 'none';
|
||||||
placeholder.style.background = 'transparent';
|
placeholder.style.background = 'transparent';
|
||||||
|
placeholder.style.width = 'auto';
|
||||||
|
placeholder.style.height = 'auto';
|
||||||
|
placeholder.style.lineHeight = 'normal';
|
||||||
|
placeholder.style.maxWidth = mw;
|
||||||
|
placeholder.style.maxHeight = mh;
|
||||||
|
placeholder.style.textAlign = 'left';
|
||||||
|
placeholder.style.verticalAlign = 'top';
|
||||||
|
placeholder.style.justifyContent = 'flex-start';
|
||||||
|
placeholder.style.alignItems = 'flex-start';
|
||||||
saveTemplateContent();
|
saveTemplateContent();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -359,6 +373,18 @@ export default function TemplateManage() {
|
|||||||
editorRef.current?.focus();
|
editorRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeLineHeight = (height: string) => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || !sel.rangeCount) return;
|
||||||
|
let node = sel.getRangeAt(0).commonAncestorContainer;
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode as Node;
|
||||||
|
const block = (node as HTMLElement).closest('p, div, td, h1, h2, h3, li');
|
||||||
|
if (block) {
|
||||||
|
(block as HTMLElement).style.lineHeight = height;
|
||||||
|
saveTemplateContent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveTemplateContent = () => {
|
const saveTemplateContent = () => {
|
||||||
if (!currentTemplateId || !editorRef.current) return;
|
if (!currentTemplateId || !editorRef.current) return;
|
||||||
const allTemplates = storage.get<Template[]>('templates', []);
|
const allTemplates = storage.get<Template[]>('templates', []);
|
||||||
@@ -378,7 +404,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) {
|
||||||
@@ -447,6 +474,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);
|
||||||
@@ -464,6 +492,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
|
||||||
@@ -481,6 +510,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>) => {
|
||||||
@@ -708,6 +738,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"
|
||||||
@@ -739,6 +776,27 @@ export default function TemplateManage() {
|
|||||||
<option value="SimHei">黑体</option>
|
<option value="SimHei">黑体</option>
|
||||||
<option value="KaiTi">楷体</option>
|
<option value="KaiTi">楷体</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onChange={(e) => { if (e.target.value) { execCmd('fontSize', e.target.value); } e.target.value = ''; }}
|
||||||
|
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
||||||
|
>
|
||||||
|
<option value="">字号</option>
|
||||||
|
<option value="3">12pt</option>
|
||||||
|
<option value="4">14pt</option>
|
||||||
|
<option value="5">18pt</option>
|
||||||
|
<option value="6">24pt</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onChange={(e) => { if (e.target.value) { changeLineHeight(e.target.value); } e.target.value = ''; }}
|
||||||
|
className="h-9 px-3 border border-border rounded-lg text-xs bg-white cursor-pointer focus:outline-hidden focus:border-accent"
|
||||||
|
>
|
||||||
|
<option value="">行距</option>
|
||||||
|
<option value="1">1.0</option>
|
||||||
|
<option value="1.5">1.5</option>
|
||||||
|
<option value="2">2.0</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
<div className="flex gap-1 pr-3 mr-3 border-r border-border">
|
||||||
<button onMouseDown={(e) => e.preventDefault()} onClick={() => execCmd('bold')} className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-white text-text-muted hover:text-text-main transition-colors" title="粗体"><Bold size={16} /></button>
|
<button onMouseDown={(e) => e.preventDefault()} onClick={() => execCmd('bold')} className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-white text-text-muted hover:text-text-main transition-colors" title="粗体"><Bold size={16} /></button>
|
||||||
@@ -854,6 +912,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' });
|
||||||
@@ -958,6 +1017,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)}
|
||||||
@@ -1154,6 +1217,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"
|
||||||
@@ -1168,6 +1235,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">
|
||||||
@@ -1260,13 +1366,35 @@ 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;max-width:${w}px;max-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);
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = html;
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
while (wrapper.firstChild) {
|
||||||
|
fragment.appendChild(wrapper.firstChild);
|
||||||
|
}
|
||||||
|
const sel2 = window.getSelection();
|
||||||
|
if (sel2 && sel2.rangeCount > 0) {
|
||||||
|
const range = sel2.getRangeAt(0);
|
||||||
|
range.deleteContents();
|
||||||
|
range.insertNode(fragment);
|
||||||
|
const lastNode = fragment.lastChild;
|
||||||
|
if (lastNode) {
|
||||||
|
range.setStartAfter(lastNode);
|
||||||
|
range.collapse(true);
|
||||||
|
sel2.removeAllRanges();
|
||||||
|
sel2.addRange(range);
|
||||||
|
}
|
||||||
|
} else if (editorRef.current) {
|
||||||
|
editorRef.current.appendChild(fragment);
|
||||||
|
}
|
||||||
|
editorRef.current?.focus();
|
||||||
|
saveTemplateContent();
|
||||||
setPlaceholderModal({...placeholderModal, isOpen: false});
|
setPlaceholderModal({...placeholderModal, isOpen: false});
|
||||||
}} className="px-4 py-2 bg-accent text-white rounded text-sm">确认插入</button>
|
}} className="px-4 py-2 bg-accent text-white rounded text-sm">确认插入</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -1,85 +1,79 @@
|
|||||||
const smartField = (key: string) => `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${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 noUnderlineKeys = ['patientName', 'patientGender', 'patientAge', 'department', 'bedNumber', 'hospitalId'];
|
||||||
|
const smartField = (key: string) => {
|
||||||
|
const noUlClass = noUnderlineKeys.includes(key) ? ' no-underline' : '';
|
||||||
|
return `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value${noUlClass}" data-bind="${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>​`;
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultReportContent = `
|
export const defaultReportContent = `
|
||||||
<!-- 医院Logo -->
|
<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 4px;">
|
||||||
<p style="text-align: center; margin-bottom: 16px;" contenteditable="false">
|
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="position:relative;display:inline-flex;align-items:center;justify-content:center;width:65px;height:65px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" 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>
|
||||||
<span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入图片</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>
|
||||||
</p>
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 14pt; font-family: SimSun; border-bottom: 1px solid #000; padding-bottom: 0; margin-bottom: 8px; display: inline-block; line-height: 1;">西 安 交 通 大 学 第 一 附 属 医 院</div>
|
||||||
<!-- 医院名称 -->
|
<div style="font-size: 16pt; font-family: SimSun;">手术记录</div>
|
||||||
<p style="text-align: center; font-family: SimSun; margin-bottom: 8px;" contenteditable="false">
|
</div>
|
||||||
<strong><u>西 安 交 通 大 学 第 一 附 属 医 院</u></strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- 报告标题 -->
|
|
||||||
<h1 style="font-family: SimSun; font-size: 20px; margin: 16px 0; text-align: center;" contenteditable="false">手术记录</h1>
|
|
||||||
|
|
||||||
<div class="template-info-section">
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
姓名:${smartField('patientName')}
|
|
||||||
性别:${smartField('patientGender')}
|
|
||||||
年龄:${smartField('patientAge')}
|
|
||||||
科别:${smartField('department')}
|
|
||||||
床号:${smartField('bedNumber')}
|
|
||||||
住院号:${smartField('hospitalId')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
<strong>手术日期:</strong>${smartField('surgeryDate')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
<strong>术前诊断:</strong>${smartField('preoperativeDiagnosis')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
<strong>术后诊断:</strong>${smartField('postoperativeDiagnosis')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
<strong>手术名称:</strong>${smartField('title')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
手术开始时间:${smartField('startTime')}
|
|
||||||
手术终止时间:${smartField('endTime')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
手术者:${smartField('surgeon')}
|
|
||||||
助手:${smartField('assistant')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
|
||||||
麻醉师:${smartField('anesthesiologist')}
|
|
||||||
麻醉方式:${smartField('anesthesiaType')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 11pt; font-weight: normal; margin: 0; padding: 0 0 1px 0; line-height: 1.2; border-bottom: 1px solid #000;">
|
||||||
|
姓名:${smartField('patientName')}
|
||||||
|
性别:${smartField('patientGender')}
|
||||||
|
年龄:${smartField('patientAge')}
|
||||||
|
科别:${smartField('department')}
|
||||||
|
床号:${smartField('bedNumber')}
|
||||||
|
住院号:${smartField('hospitalId')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
<strong>手术日期:</strong>${smartField('surgeryDate')}
|
||||||
|
</p>
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
<strong>术前诊断:</strong>${smartField('preoperativeDiagnosis')}
|
||||||
|
</p>
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
<strong>术中诊断:</strong>${smartField('postoperativeDiagnosis')}
|
||||||
|
</p>
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
<strong>手术名称:</strong>${smartField('title')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table style="width: 100%; border: none; font-family: SimSun; font-size: 12pt; margin-top: 0; margin-bottom: 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; padding: 0; width: 50%; line-height: 1.5;">手术开始时间:${smartField('startTime')}</td>
|
||||||
|
<td style="border: none; padding: 0; width: 50%; line-height: 1.5;">手术终止时间:${smartField('endTime')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; padding: 0; line-height: 1.5;">手术者:${smartField('surgeon')}</td>
|
||||||
|
<td style="border: none; padding: 0; line-height: 1.5;">助手:${smartField('assistant')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; padding: 0; line-height: 1.5;">麻醉师:${smartField('anesthesiologist')}</td>
|
||||||
|
<td style="border: none; padding: 0; line-height: 1.5;">麻醉方式:${smartField('anesthesiaType')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
<strong>手术步骤、术中出现的情况及处理:</strong>
|
<strong>手术步骤、术中出现的情况及处理:</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
1.患者仰卧位,麻醉成功后,常规消毒术野、铺无菌巾,于脐下穿刺建立CO2气腹,气腹压力为12mmHg,进镜探查无穿刺损伤,分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。
|
1.患者仰卧位,麻醉成功后,常规消毒术野、铺无菌巾,于脐下穿刺建立CO2气腹,气腹压力为12mmHg,进镜探查无穿刺损伤,分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
2.腹腔镜探查:腹腔内无腹水形成,无明显粘连,肝脏色红质软,无明显结节硬化改变,胆囊大小约 cm× cm× cm,壁轻度水肿,张力可,胆囊三角解剖关系清楚,胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断:胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。
|
2.腹腔镜探查:腹腔内无腹水形成,无明显粘连,肝脏色红质软,无明显结节硬化改变,胆囊大小约 cm× cm× cm,壁轻度水肿,张力可,胆囊三角解剖关系清楚,胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断:胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
3.切除胆囊:钳夹胆囊颈部并解剖胆囊三角,游离出胆囊动脉及胆囊管,明确胆囊与胆总管的关系,距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。
|
3.切除胆囊:钳夹胆囊颈部并解剖胆囊三角,游离出胆囊动脉及胆囊管,明确胆囊与胆总管的关系,距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。
|
4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml,术中输血成分,输血量,是否有输血不良反应。
|
5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml,术中输血成分,输血量,是否有输血不良反应。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -87,74 +81,76 @@ export const defaultReportContent = `
|
|||||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; table-layout: fixed;">
|
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; table-layout: fixed;">
|
||||||
<tbody><tr>
|
<tbody><tr>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图A 腹腔镜探查</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图A 腹腔镜探查</p>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图B 胆囊管夹闭与离断</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图B 胆囊管夹闭与离断</p>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图C 胆囊动脉夹闭与离断</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图C 胆囊动脉夹闭与离断</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图D 胆囊剥离与床面止血</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图D 胆囊剥离与床面止血</p>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图E 胆囊取出与钛夹确认</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图E 胆囊取出与钛夹确认</p>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
<td style="width: 33%; text-align: center; padding: 10px; vertical-align: top; border: 1px solid #e2e8f0;">
|
||||||
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="display:inline-flex;align-items:center;justify-content:center;width:100%;height:150px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;cursor:pointer;">
|
<div class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="frame" style="border: 1px dashed #cbd5e1; background: #f8fafc; width: 100%; height: 100%; max-width: 200px; max-height: 200px; min-height: 60px; margin: 0px auto; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||||
<span class="delete-btn" contenteditable="false">×</span>
|
<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 class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span>
|
||||||
</span>
|
</div>
|
||||||
<p style="color: #64748b; font-size: 13px; margin: 0;">图F 止血材料覆盖及检查</p>
|
<p style="color: #64748b; font-size: 13px; margin: 0; padding: 0; line-height: 1.5;">图F 止血材料覆盖及检查</p>
|
||||||
</td>
|
</td>
|
||||||
</tr></tbody>
|
</tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="template-info-section">
|
<div class="template-info-section">
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
<strong>手术后情况</strong>:${smartField('postOpCondition')}
|
<strong>手术后情况</strong>:${smartField('postOpCondition')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
<strong>切除标本描述</strong>:${smartField('specimenDescription')}
|
<strong>切除标本描述</strong>:${smartField('specimenDescription')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
<strong>是否送病理检查</strong>:${smartField('pathologyCheck')}
|
<strong>是否送病理检查</strong>:${smartField('pathologyCheck')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
<strong>冰冻病理结果</strong>:${smartField('frozenPathology')}
|
<strong>冰冻病理结果</strong>:${smartField('frozenPathology')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-family: SimSun;">
|
<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;">
|
<p style="margin: 0; padding: 0; line-height: 1.5;"> </p>
|
||||||
|
|
||||||
|
<p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
${smartField('reportDate')}
|
${smartField('reportDate')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -23,7 +23,7 @@ export const printDocument = (htmlContent: string) => {
|
|||||||
body { margin: 0; padding: 0; font-family: SimSun, "Microsoft YaHei", serif; color: #1E293B; background: white; }
|
body { margin: 0; padding: 0; font-family: SimSun, "Microsoft YaHei", serif; color: #1E293B; background: white; }
|
||||||
.content { width: 100%; min-height: 277mm; margin: 0 auto; }
|
.content { width: 100%; min-height: 277mm; margin: 0 auto; }
|
||||||
img { max-width: 100%; height: auto; display: block; margin: 8px auto; }
|
img { max-width: 100%; height: auto; display: block; margin: 8px auto; }
|
||||||
p { margin: 0; padding: 4px 0; line-height: 1.6; }
|
p { margin: 0; padding: 0; line-height: 1.5; }
|
||||||
h1 { font-size: 20px; margin: 16px 0 12px; font-weight: 600; text-align: center; }
|
h1 { font-size: 20px; margin: 16px 0 12px; font-weight: 600; text-align: center; }
|
||||||
strong, b { font-weight: 600; }
|
strong, b { font-weight: 600; }
|
||||||
u { text-decoration: underline; }
|
u { text-decoration: underline; }
|
||||||
@@ -31,7 +31,7 @@ export const printDocument = (htmlContent: string) => {
|
|||||||
td { padding: 8px; border: 1px solid #e2e8f0; vertical-align: top; }
|
td { padding: 8px; border: 1px solid #e2e8f0; vertical-align: top; }
|
||||||
.image-placeholder { border: 2px dashed #cbd5e1; border-radius: 8px; padding: 16px; margin-bottom: 8px; background: #f8fafc; min-height: 70px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
|
.image-placeholder { border: 2px dashed #cbd5e1; border-radius: 8px; padding: 16px; margin-bottom: 8px; background: #f8fafc; min-height: 70px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
|
||||||
.image-placeholder.has-image { border: none; background: transparent; padding: 0; min-height: 0; }
|
.image-placeholder.has-image { border: none; background: transparent; padding: 0; min-height: 0; }
|
||||||
.image-placeholder .delete-btn { display: none !important; }
|
.delete-btn { display: none !important; }
|
||||||
.image-placeholder:not(.has-image) { display: none !important; }
|
.image-placeholder:not(.has-image) { display: none !important; }
|
||||||
.template-info-section { position: relative; margin-bottom: 16px; }
|
.template-info-section { position: relative; margin-bottom: 16px; }
|
||||||
.smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 2px; vertical-align: text-bottom; }
|
.smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 2px; vertical-align: text-bottom; }
|
||||||
@@ -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>
|
||||||
|
|||||||
83
工程分析/实现方案-2026-04-18-16-45-02.md
Normal file
83
工程分析/实现方案-2026-04-18-16-45-02.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-16-45-02
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
建立一套标准化、可复用的代码编纂工作流,确保后续所有项目修改需求都能按统一流程执行,减少遗漏和错误。
|
||||||
|
|
||||||
|
## 方案内容
|
||||||
|
|
||||||
|
### 阶段一:工程分析文件夹确认(步骤 1)
|
||||||
|
1. 检查 `.\工程分析` 文件夹是否存在。
|
||||||
|
2. 若不存在则创建;若存在则确认其包含以下文件类型:
|
||||||
|
- `需求分析-*.md`
|
||||||
|
- `实现方案-*.md`
|
||||||
|
- `测试方案-*.md`
|
||||||
|
- `经验记录.md`
|
||||||
|
|
||||||
|
### 阶段二:需求分析文档生成(步骤 2)
|
||||||
|
每次用户提出修改需求时:
|
||||||
|
1. 记录开始时间 `{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}`。
|
||||||
|
2. 创建 `.\工程分析\需求分析-{时间}.md`。
|
||||||
|
3. 文档内容包含:
|
||||||
|
- 需求来源
|
||||||
|
- 需求概述(一句话描述)
|
||||||
|
- 功能详细描述
|
||||||
|
- 涉及文件/模块清单
|
||||||
|
- 需求影响范围
|
||||||
|
|
||||||
|
### 阶段三:实现方案文档生成与用户审核(步骤 3)
|
||||||
|
1. 基于需求分析,编写 `.\工程分析\实现方案-{时间}.md`。
|
||||||
|
2. 文档内容包含:
|
||||||
|
- 方案目标
|
||||||
|
- 具体实现步骤(分阶段)
|
||||||
|
- 涉及文件及修改点
|
||||||
|
- 风险与注意事项
|
||||||
|
3. **此处为强制审核节点**:文档生成后停止执行,等待用户确认"方案无误,继续执行"。
|
||||||
|
4. 用户确认后方可进入下一阶段。
|
||||||
|
|
||||||
|
### 阶段四:测试方案文档生成与用户审核(步骤 4)
|
||||||
|
1. 基于实现方案,编写 `.\工程分析\测试方案-{时间}.md`。
|
||||||
|
2. 文档内容包含:
|
||||||
|
- 测试目标
|
||||||
|
- 测试用例清单(编号、操作步骤、预期结果)
|
||||||
|
- 回归测试范围
|
||||||
|
3. **此处为强制审核节点**:文档生成后停止执行,等待用户确认"方案无误,继续执行"。
|
||||||
|
4. 用户确认后方可进入下一阶段。
|
||||||
|
|
||||||
|
### 阶段五:经验记录阅读与执行(步骤 5)
|
||||||
|
1. **执行前**:读取 `.\工程分析\经验记录.md`,提取与本次修改相关的经验条目。
|
||||||
|
2. **执行中**:按照已审核的实现方案修改代码。
|
||||||
|
3. **执行后**:若过程中遇到新的关键问题,按四段式追加到 `.\工程分析\经验记录.md`:
|
||||||
|
- A. 具体问题
|
||||||
|
- B. 产生问题原因
|
||||||
|
- C. 解决问题方案
|
||||||
|
- D. 后续如何避免问题
|
||||||
|
|
||||||
|
### 阶段六:Gitea 备份(步骤 6)
|
||||||
|
1. 执行以下 Git 操作:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec} - {简要描述}"
|
||||||
|
git push origin main
|
||||||
|
git tag -a v{版本号} -m "{版本描述}"
|
||||||
|
git push origin v{版本号}
|
||||||
|
```
|
||||||
|
2. 向用户汇报备份完成。
|
||||||
|
|
||||||
|
### 阶段七:重新部署(步骤 7)
|
||||||
|
1. 执行 `npm run build` 构建生产版本。
|
||||||
|
2. 验证构建产物 `dist/` 已生成。
|
||||||
|
3. 启动预览服务 `npm run preview`(或用户指定的部署方式)。
|
||||||
|
|
||||||
|
## 工作流强制审核点
|
||||||
|
| 审核点 | 触发条件 | 用户操作 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| 实现方案审核 | 实现方案文档生成完毕 | 用户阅读并回复"确认" |
|
||||||
|
| 测试方案审核 | 测试方案文档生成完毕 | 用户阅读并回复"确认" |
|
||||||
|
|
||||||
|
## 本次(建立工作流)的执行差异
|
||||||
|
由于本次需求不涉及业务代码修改,阶段五(代码执行)、阶段六(备份)、阶段七(部署)均跳过或简化处理。重点在于将工作流规范文档化并确认用户理解。
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. 用户必须理解两个强制审核节点(实现方案、测试方案),不可跳过。
|
||||||
|
2. 若用户在审核阶段提出修改意见,需重新生成对应文档并再次等待确认。
|
||||||
|
3. 经验记录文档需持续维护,成为项目知识库的核心资产。
|
||||||
56
工程分析/实现方案-2026-04-18-16-55-47.md
Normal file
56
工程分析/实现方案-2026-04-18-16-55-47.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-16-55-47
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
实现 report-editor 中正文与侧边栏的点击联动、字段动态排序、以及默认模板的手术图片表格替换。
|
||||||
|
|
||||||
|
## 需求 1:点击 field-value 联动右侧基本信息
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. **修改 `handleEditorClick`**:在 `ReportEditor.tsx` 的 `handleEditorClick` 函数中,增加对 `.field-value` 的点击捕获。
|
||||||
|
- 通过 `e.target.closest('.field-value')` 获取被点击的 field-value 元素。
|
||||||
|
- 读取其 `data-bind` 属性值(如 `patientName`)。
|
||||||
|
2. **切换 Tab**:调用 `setActiveTab('info')` 将右侧面板切回「基本信息」。
|
||||||
|
3. **聚焦与滚动**:
|
||||||
|
- 为右侧表单中的每个输入组件增加 `id={\`input-\${field.key}\`}`。
|
||||||
|
- 使用 `setTimeout` 等待 React DOM 渲染完成后,通过 `document.getElementById(\`input-\${bindKey}\`)` 获取对应元素。
|
||||||
|
- 调用 `scrollIntoView({ behavior: 'smooth', block: 'center' })` 并 `focus()`。
|
||||||
|
|
||||||
|
## 需求 2:右侧基本信息字段按正文出现顺序动态排序
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. **提取正文字段顺序**:
|
||||||
|
- 使用 `contentRef.current`(当前编辑器 HTML 字符串)或 `editorRef.current?.innerHTML`。
|
||||||
|
- 对 `formFields.filter(f => f.visibleInForm)` 中的每个非置顶字段,计算 `data-bind="${field.key}"` 在 HTML 中的首次出现位置(`indexOf`)。
|
||||||
|
2. **排序策略**:
|
||||||
|
- **置顶组**:`const topKeys = ['patientName', 'hospitalId', 'title'];`,按此固定顺序排列。
|
||||||
|
- **正文组**:非置顶字段,按 `indexOf` 升序排列(越早出现越靠前)。
|
||||||
|
- **末尾组**:正文中未出现的字段(`indexOf === -1`),统一排在最后,保持原有相对顺序。
|
||||||
|
3. **渲染表单**:将排序后的字段数组直接用于右侧表单 `.map()` 渲染。
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
- 使用 `useMemo` 缓存排序结果,仅在 `formFields` 或编辑器内容变化时重新计算。
|
||||||
|
- 排序逻辑放在 `useMemo` 中,避免每次渲染重复计算。
|
||||||
|
|
||||||
|
## 需求 3:替换默认手术图片说明表格
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. 定位 `src/utils/defaultContent.ts` 中的 `defaultReportContent`。
|
||||||
|
2. 找到 `<!-- 手术图片说明表格 -->` 注释所在的 `<table>` 区域。
|
||||||
|
3. 替换为用户提供的 HTML 代码:
|
||||||
|
- 2 行 × 3 列布局
|
||||||
|
- 每格包含 `.image-placeholder`(表格内模式:`<div>` 块级容器,`width:100%; height:100%; max-width:200px; max-height:200px;`)
|
||||||
|
- 每格底部含图注(图A~图F)
|
||||||
|
- 保留 `data-placeholder="true"` 和 `contenteditable="false"`
|
||||||
|
4. 清理复制时产生的冗余内联样式(如 `background-image: initial` 等),保留功能必需的样式。
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/pages/ReportEditor.tsx` | `handleEditorClick` 增加 field-value 点击捕获;表单渲染增加 `id`;右侧字段排序逻辑 |
|
||||||
|
| `src/utils/defaultContent.ts` | 替换手术图片说明表格 HTML |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `contentRef.current` 在组件首次挂载前可能为空,排序逻辑需做空值保护。
|
||||||
|
2. `setActiveTab` 后 DOM 切换有短暂延迟,`scrollIntoView` 需包裹在 `setTimeout` 中。
|
||||||
|
3. 默认模板替换后,需验证新建报告时表格渲染是否正常、占位符点击事件是否生效。
|
||||||
|
4. 置顶字段的 `key` 名称需与 `DEFAULT_FORM_FIELDS` 中严格一致。
|
||||||
82
工程分析/实现方案-2026-04-18-17-27-51.md
Normal file
82
工程分析/实现方案-2026-04-18-17-27-51.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-17-27-51
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
修复 TemplateManage 静态占位符插入 Bug,重构默认报告模板顶部排版,修复 Logo 删除按钮交互。
|
||||||
|
|
||||||
|
## 需求 1:修复静态图片占位符插入不显示
|
||||||
|
|
||||||
|
### 问题根因
|
||||||
|
`TemplateManage.tsx` 中 `insertImage()` 使用 `document.execCommand('insertHTML', false, html)`。现代浏览器对含 `contenteditable="false"` 的复杂嵌套标签会自动修正/拍平,导致外层 `.image-placeholder` 容器丢失,DOM 仅剩零散子元素,视觉上不可见。
|
||||||
|
|
||||||
|
### 解决步骤
|
||||||
|
1. **定位 `insertImage` 函数**:找到 `TemplateManage.tsx` 中通过 `document.execCommand('insertHTML')` 插入占位符的逻辑。
|
||||||
|
2. **替换为 `Range.insertNode`**:
|
||||||
|
- 创建临时 `div`,将 HTML 字符串写入 `innerHTML`。
|
||||||
|
- 将子节点逐个移入 `DocumentFragment`。
|
||||||
|
- 获取当前 `Selection` 的 `RangeAt(0)`。
|
||||||
|
- 调用 `range.deleteContents()` 清空当前选区。
|
||||||
|
- 调用 `range.insertNode(fragment)` 精确插入。
|
||||||
|
- 将光标移动到插入内容之后。
|
||||||
|
3. **保持原有弹窗逻辑不变**:Modal 中的模式选择(frame/manual)、宽高输入等逻辑不受影响。
|
||||||
|
|
||||||
|
## 需求 2:重构默认报告模板排版
|
||||||
|
|
||||||
|
### 排版设计
|
||||||
|
|
||||||
|
#### 页眉(Logo + 医院名 + 标题)
|
||||||
|
使用 3 列 `<table>`(左 20%、中 60%、右 20%),中间列绝对居中:
|
||||||
|
- 左列:Logo 占位符(65×65,`data-mode="manual"`,`position:relative`)
|
||||||
|
- 中列:
|
||||||
|
- 第一行:14pt SimSun「西 安 交 通 大 学 第 一 附 属 医 院」(带 `border-bottom: 1px solid #000` 下划线,使用 `display: inline-block`)
|
||||||
|
- 第二行:16pt SimSun「手术记录」
|
||||||
|
- 右列:留空
|
||||||
|
|
||||||
|
#### 基本信息栏(下划线贯穿)
|
||||||
|
使用 `<div style="border-bottom: 1px solid #000; padding-bottom: 4px; margin-bottom: 12px;">` 包裹一行:
|
||||||
|
- 11pt SimSun,不加粗
|
||||||
|
- 姓名、性别、年龄、科别、床号、住院号,用 ` ` 间隔
|
||||||
|
|
||||||
|
#### 诊断/手术信息(单行加粗)
|
||||||
|
每项独立 `<p>`:
|
||||||
|
- 12pt SimSun,`font-weight: bold`
|
||||||
|
- 手术日期、术前诊断、术中诊断、手术名称
|
||||||
|
|
||||||
|
#### 双列信息(两项一行,不加粗)
|
||||||
|
使用 `<table style="width: 100%; border: none;">`:
|
||||||
|
- 三行两列,每列 50%
|
||||||
|
- 12pt SimSun,不加粗
|
||||||
|
- 手术开始/终止时间、手术者/助手、麻醉师/麻醉方式
|
||||||
|
|
||||||
|
#### 手术步骤标题
|
||||||
|
- 12pt SimSun,`font-weight: bold`
|
||||||
|
- 「手术步骤、术中出现的情况及处理:」
|
||||||
|
|
||||||
|
#### 保留内容
|
||||||
|
- 5 条手术步骤段落文字(不变)
|
||||||
|
- 手术图片说明表格(需求 3 中已替换的最新 6 图格表格)
|
||||||
|
- 手术后情况段落(术后诊断、标本描述、病理检查、冰冻病理)
|
||||||
|
- 手术者签名占位符 + 撰写时间字段
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
`src/utils/defaultContent.ts` —— 完全重写 `defaultReportContent` 变量。
|
||||||
|
|
||||||
|
## 需求 3:修复顶部 Logo 删除按钮
|
||||||
|
|
||||||
|
### 解决步骤
|
||||||
|
在 `defaultContent.ts` 中 Logo 占位符的 `style` 属性中增加 `position: relative;`,使绝对定位的 `.delete-btn` 相对于占位符自身定位,而非向外层逃逸。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="position:relative;display:inline-flex;...">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/pages/TemplateManage.tsx` | `insertImage` 中 `execCommand('insertHTML')` → `Range.insertNode` |
|
||||||
|
| `src/utils/defaultContent.ts` | 完全重写顶部排版;Logo 增加 `position:relative`;保留手术步骤/表格/底部段落 |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `Range.insertNode` 要求编辑器有有效光标/选区。若编辑器未聚焦或选区不在编辑器内,需增加保护逻辑(fallback 到 `editorRef.current.appendChild`)。
|
||||||
|
2. 默认模板重写后,需验证 `smartField()` 生成的所有字段占位符在新排版中是否正确渲染。
|
||||||
|
3. 打印时需确认新排版的下划线、表格边框在 `@media print` 中正常显示。
|
||||||
|
4. ` ` 分隔的基本信息栏在打印时可能换行,需测试实际打印效果。
|
||||||
100
工程分析/实现方案-2026-04-18-17-48-59.md
Normal file
100
工程分析/实现方案-2026-04-18-17-48-59.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-17-48-59
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
修复默认模板排版细节和打印样式问题,提升报告的视觉一致性和打印输出质量。
|
||||||
|
|
||||||
|
## 需求 1:缩减基本信息栏字段间空格
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将基本信息栏 `<p>` 中字段之间的 ` ` 替换为单个 ` `。
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```html
|
||||||
|
姓名:${smartField('patientName')}
|
||||||
|
性别:${smartField('patientGender')}
|
||||||
|
年龄:${smartField('patientAge')}
|
||||||
|
科别:${smartField('department')}
|
||||||
|
床号:${smartField('bedNumber')}
|
||||||
|
住院号:${smartField('hospitalId')}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```html
|
||||||
|
姓名:${smartField('patientName')}
|
||||||
|
性别:${smartField('patientGender')}
|
||||||
|
年龄:${smartField('patientAge')}
|
||||||
|
科别:${smartField('department')}
|
||||||
|
床号:${smartField('bedNumber')}
|
||||||
|
住院号:${smartField('hospitalId')}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 2:Logo 与医院名/标题靠拢并整体居中
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将顶部 3 列 `<table>` 替换为 `<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 16px;">`。
|
||||||
|
- Logo 占位符放在 flex 子 div 中
|
||||||
|
- 医院名和标题放在另一个 flex 子 div 中(`text-align: center`)
|
||||||
|
- 整体通过 `justify-content: center` 实现居中
|
||||||
|
- `gap: 12px` 或 `margin-right: 12px` 控制 Logo 与文字间距
|
||||||
|
|
||||||
|
## 需求 3:打印时隐藏所有「×」删除按钮
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/print.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
在 `print.ts` 生成的 `<style>` 标签中,将 `.image-placeholder .delete-btn { display: none !important; }` 扩展为全局规则:
|
||||||
|
```css
|
||||||
|
.delete-btn { display: none !important; }
|
||||||
|
```
|
||||||
|
|
||||||
|
这样无论删除按钮位于 `.image-placeholder` 内还是 `.smart-field-wrapper` 内,打印时均不可见。
|
||||||
|
|
||||||
|
## 需求 4:统一全文行距为 1.5,消除段前段后间距
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts` 和 `src/utils/print.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
1. **`defaultContent.ts`**:将所有 `<p>` 标签的内联样式统一为 `line-height: 1.5; margin: 0; padding: 0;`。移除原有的 `line-height: 1.8`、默认 margin 等不一致设置。
|
||||||
|
2. **`print.ts`**:将全局 `p` 样式从 `margin: 0; padding: 4px 0; line-height: 1.6;` 修改为 `margin: 0; padding: 0; line-height: 1.5;`。
|
||||||
|
|
||||||
|
## 需求 5:下划线紧贴文字底部
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
1. **医院名称下划线**:将包裹医院名的 `div` 的 `padding-bottom: 4px` 移除或改为 `0`。同时在该 `div` 上增加 `line-height: 1`,消除中文字体自带的底部留白,使 `border-bottom` 紧贴文字。
|
||||||
|
2. **基本信息栏下划线**:将外层 `<div style="border-bottom: 1px solid #000; ...">` 的 `padding-bottom: 4px` 移除。内部 `<p>` 的 `line-height` 已统一为 1.5(需求 4),若仍有间距问题,可进一步在该 `<p>` 上设置 `line-height: 1.2` 或让下划线直接由 `<p>` 的 `border-bottom` 实现。
|
||||||
|
|
||||||
|
### 优化策略
|
||||||
|
更简洁的做法:让下划线直接由承载文字的 `<p>` 元素生成,而非由外层 `<div>` 生成。例如:
|
||||||
|
```html
|
||||||
|
<p style="font-family: SimSun; font-size: 11pt; font-weight: normal; margin: 0; padding: 0 0 2px 0; line-height: 1.2; border-bottom: 1px solid #000;">
|
||||||
|
姓名:... 性别:...
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
这样文字底部与下划线之间仅由 `padding-bottom: 2px` 或 `line-height` 控制,可精确调整。
|
||||||
|
|
||||||
|
对于医院名称,同理:
|
||||||
|
```html
|
||||||
|
<div style="font-size: 14pt; font-family: SimSun; line-height: 1; border-bottom: 1px solid #000; padding-bottom: 0; margin-bottom: 8px; display: inline-block;">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/utils/defaultContent.ts` | 缩减空格;改 Flex 抬头;统一 line-height/margin/padding;调整下划线贴底 |
|
||||||
|
| `src/utils/print.ts` | 全局隐藏 `.delete-btn`;统一 p 标签 line-height/margin/padding |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `print.ts` 的全局 `.delete-btn { display: none !important; }` 会覆盖所有删除按钮,包括未来可能新增的其他类型。这是预期行为(打印时不应显示任何交互按钮)。
|
||||||
|
2. `line-height: 1` 在部分中文字体下可能导致字符上下紧贴甚至重叠,需在实际打印中验证。若出现问题,可微调为 `line-height: 1.1`。
|
||||||
|
3. 修改 `defaultContent.ts` 后,新建报告会加载新模板,但已有报告(保存在 localStorage 中)不会自动更新。这是预期行为。
|
||||||
91
工程分析/实现方案-2026-04-18-18-08-37.md
Normal file
91
工程分析/实现方案-2026-04-18-18-08-37.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-18-08-37
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
修复并增强编辑器工具栏的字体/字号/行距功能,调整默认模板排版细节。
|
||||||
|
|
||||||
|
## 需求 1:修复字体选择并新增字号、行距功能
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx` 和 `src/pages/TemplateManage.tsx`
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
1. **修复字体选择**:确保工具栏中的字体选择 `<select>` 使用 `execCmd('fontName', value)`。若失效,检查是否有全局 CSS `font-family: !important` 覆盖。如有,在打印样式中保留覆盖,但在编辑器样式中移除。
|
||||||
|
2. **新增字号选择**:在工具栏字体选择旁边增加 `<select>`:
|
||||||
|
```tsx
|
||||||
|
<select onChange={e => { if (e.target.value) { execCmd('fontSize', e.target.value); } e.target.value = ''; }}>
|
||||||
|
<option value="">字号</option>
|
||||||
|
<option value="3">12pt</option>
|
||||||
|
<option value="4">14pt</option>
|
||||||
|
<option value="5">18pt</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
`execCommand('fontSize')` 使用 1-7 的相对字号,3 对应 12pt,4 对应 14pt,5 对应 18pt。
|
||||||
|
3. **新增行距选择**:`execCommand` 不支持行距,需手写 `changeLineHeight` 函数:
|
||||||
|
```tsx
|
||||||
|
const changeLineHeight = (height: string) => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || !sel.rangeCount) return;
|
||||||
|
let node = sel.getRangeAt(0).commonAncestorContainer;
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode as Node;
|
||||||
|
const block = (node as HTMLElement).closest('p, div, td, h1, h2, h3');
|
||||||
|
if (block) {
|
||||||
|
(block as HTMLElement).style.lineHeight = height;
|
||||||
|
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
|
||||||
|
saveDraftToStorage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
在工具栏增加行距 `<select>` 绑定此函数。
|
||||||
|
|
||||||
|
## 需求 2:修复手术者签名右对齐时图片框换行
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将「手术者签名」所在 `<p>` 增加 `white-space: nowrap;`,并将图片占位符的 `display` 改为 `inline-block`:
|
||||||
|
```html
|
||||||
|
<p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0; white-space: nowrap;">
|
||||||
|
手术者签名:<span class="image-placeholder" ... style="display:inline-block; vertical-align:middle; ...">...</span>
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 3:缩减「手术记录」与「姓名」之间的距离
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将顶部 Flex 容器的 `margin-bottom: 16px` 缩小为 `margin-bottom: 4px`。
|
||||||
|
|
||||||
|
## 需求 4:消除「手术名称」与「手术开始时间」之间的多余间距
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将双列信息 `<table>` 的 `margin-bottom: 12pt` 改为 `margin-bottom: 0; margin-top: 0;`。同时确保「手术名称」`<p>` 的 `margin: 0; padding: 0;`。
|
||||||
|
|
||||||
|
## 需求 5:统一「手术日期」及以下内容为 12pt、1.5 行距、无段间距
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
为所有手术步骤段落(1~5)以及手术后情况段落补充 `font-size: 12pt;`:
|
||||||
|
```html
|
||||||
|
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/pages/ReportEditor.tsx` | 工具栏新增字号选择、行距选择;修复字体选择 |
|
||||||
|
| `src/pages/TemplateManage.tsx` | 工具栏新增字号选择、行距选择;修复字体选择 |
|
||||||
|
| `src/utils/defaultContent.ts` | 签名行 `white-space: nowrap`; 顶部 `margin-bottom: 4px`; 表格 `margin: 0`; 补全 `font-size: 12pt` |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `execCommand('fontSize')` 生成的是 `<font size="N">` 标签,与现代 HTML5 规范不完全兼容,但在 `contentEditable` 中是浏览器广泛支持的方式。
|
||||||
|
2. `changeLineHeight` 直接操作 DOM style,在 `ReportEditor` 中修改后需同步 `contentRef.current` 和调用 `saveDraftToStorage()`。
|
||||||
|
3. `TemplateManage` 中修改行距后需调用 `saveTemplateContent()`。
|
||||||
|
4. `white-space: nowrap` 在签名行可能导致超长内容溢出,但考虑到签名行通常较短,风险可控。
|
||||||
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 报告),需分别处理。
|
||||||
97
工程分析/实现方案-2026-04-18-19-08-43.md
Normal file
97
工程分析/实现方案-2026-04-18-19-08-43.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-19-08-43
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
优化编辑器交互体验和模板排版细节,提升视频面板空间利用率和图片占位符自适应能力。
|
||||||
|
|
||||||
|
## 需求 1:基础信息字段默认无下划线
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
修改 `smartField()` 函数,对 6 个基础字段自动附加 `.no-underline` 类:
|
||||||
|
```ts
|
||||||
|
const noUnderlineKeys = ['patientName', 'patientGender', 'patientAge', 'department', 'bedNumber', 'hospitalId'];
|
||||||
|
const noUlClass = noUnderlineKeys.includes(key) ? ' no-underline' : '';
|
||||||
|
```
|
||||||
|
在生成的 HTML 中,`.field-value` 的 class 变为 `field-value${noUlClass}`。
|
||||||
|
|
||||||
|
## 需求 2:字段联动高亮并居中滚动
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
1. **新增状态**:`const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);`
|
||||||
|
2. **修改点击处理**:在 `handleEditorClick` 的 `.field-value` 点击分支中,增加 `setActiveFieldKey(bindKey)`。
|
||||||
|
3. **修改滚动逻辑**:将 `scrollIntoView` 的 `block` 从 `'center'` 改为更精确的控制(`block: 'center'` 本身就是居中,满足 1/3~2/3 需求)。
|
||||||
|
4. **高亮样式**:在右侧表单渲染中,为每个字段容器 `div` 增加动态类名:
|
||||||
|
```tsx
|
||||||
|
className={`space-y-1 p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 3:视频上传按钮整合进缩略图列表
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
1. 删除原本独立的「上传视频」大按钮区域。
|
||||||
|
2. 在 `videos.map()` 所在的滚动容器 `<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar">` 的第一个位置,插入缩小版的上传按钮:
|
||||||
|
```tsx
|
||||||
|
<button
|
||||||
|
onClick={() => videoInputRef.current?.click()}
|
||||||
|
className="shrink-0 w-24 h-[68px] flex flex-col items-center justify-center gap-1 border-2 border-dashed border-border rounded-xl hover:border-accent hover:bg-slate-50 transition-all text-text-muted hover:text-accent"
|
||||||
|
>
|
||||||
|
<Video size={18} />
|
||||||
|
<span className="text-[10px] font-bold">上传视频</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 4:视频模块间距紧凑化
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
1. 最外层容器从 `space-y-4` 改为 `space-y-2`。
|
||||||
|
2. 视频播放器与控制按钮之间从 `space-y-4` 改为 `space-y-2`。
|
||||||
|
3. 控制按钮区域(播放/暂停/进度条等)的 `gap` 或 `margin` 适当缩减。
|
||||||
|
4. 「关键帧摘取」标题区域的 `padding-top` 缩减,可增加 `border-t` 作为视觉分隔。
|
||||||
|
|
||||||
|
## 需求 5:签名与日期之间增加空行
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/utils/defaultContent.ts`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
在「手术者签名」`<p>` 和「撰写时间」`<p>` 之间插入:
|
||||||
|
```html
|
||||||
|
<p style="margin: 0; padding: 0; line-height: 1.5;"> </p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 6:图片占位符填充后高度自适应
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx` 和 `src/pages/TemplateManage.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
在所有填充图片的逻辑中(`fillPlaceholderSrc`、`handleDrop`、`autoCaptureFrames` 等),在 `placeholder.classList.add('has-image')` 之后,增加:
|
||||||
|
```ts
|
||||||
|
placeholder.style.height = 'auto';
|
||||||
|
placeholder.style.width = 'auto';
|
||||||
|
placeholder.style.lineHeight = 'normal';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 涉及文件及修改点
|
||||||
|
| 文件 | 修改点 |
|
||||||
|
|------|--------|
|
||||||
|
| `src/utils/defaultContent.ts` | `smartField()` 注入 `.no-underline`;签名与日期之间插入空行 |
|
||||||
|
| `src/pages/ReportEditor.tsx` | `activeFieldKey` 状态 + 高亮样式;视频上传按钮整合;视频面板间距缩减;占位符自适应样式 |
|
||||||
|
| `src/pages/TemplateManage.tsx` | 占位符自适应样式 |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. `smartField()` 中硬编码的 6 个 key 需与 `DEFAULT_FORM_FIELDS` 严格一致。
|
||||||
|
2. `activeFieldKey` 高亮样式使用 `transition-all duration-300`,需确保不会与现有样式冲突。
|
||||||
|
3. 视频上传按钮移入缩略图列表后,需确保 `videoInputRef` 的点击触发逻辑不受影响。
|
||||||
|
4. 占位符 `height: auto` 后,需验证图片在表格内(table cell)和正文中的显示是否正常。
|
||||||
82
工程分析/实现方案-2026-04-18-19-23-31.md
Normal file
82
工程分析/实现方案-2026-04-18-19-23-31.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 实现方案 —— 2026-04-18-19-23-31
|
||||||
|
|
||||||
|
## 方案目标
|
||||||
|
修复视频分析模块空白问题,重构图片占位符的填充后尺寸逻辑。
|
||||||
|
|
||||||
|
## 需求 1:修复视频分析模块空白
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
`src/pages/ReportEditor.tsx`
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
将「上传视频」按钮和视频缩略图列表从 `videos.length > 0` 条件内部移出,使其始终渲染。仅保留视频播放器和关键帧网格在 `currentVideoIndex !== -1 && videos.length > 0` 条件下渲染。
|
||||||
|
|
||||||
|
修改后结构:
|
||||||
|
```tsx
|
||||||
|
{activeTab === 'video' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input ref={videoInputRef} ... />
|
||||||
|
|
||||||
|
{/* 始终可见:上传按钮 + 视频缩略图列表 */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar items-center">
|
||||||
|
<button>上传视频</button>
|
||||||
|
{videos.map(...)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 条件渲染:视频播放器和关键帧 */}
|
||||||
|
{currentVideoIndex !== -1 && videos.length > 0 && (
|
||||||
|
<div className="space-y-2">...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 2:图片占位符尺寸自适应
|
||||||
|
|
||||||
|
### 核心逻辑
|
||||||
|
1. **插入占位符时**:在 `style` 中注入 `max-width` 和 `max-height`,与 `width`/`height` 相同,便于后续读取限制值。
|
||||||
|
2. **填充图片时**:
|
||||||
|
- 读取占位符当前的 `max-width` / `max-height`(或回退到 `width` / `height`)
|
||||||
|
- 将这两个值赋给内部 `<img>` 的 `max-width` / `max-height`
|
||||||
|
- 设置 `object-fit: contain; object-position: left top`
|
||||||
|
- 将占位符外壳的 `width`、`height`、`line-height` 设为 `auto` / `normal`
|
||||||
|
- 保留 `max-width`、`max-height` 作为硬限制
|
||||||
|
- 设置 `text-align: left; vertical-align: top`
|
||||||
|
|
||||||
|
### 修改文件及位置
|
||||||
|
| 文件 | 函数/位置 | 修改内容 |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `src/pages/ReportEditor.tsx` | `fillPlaceholderSrc` | 填充后读取限制值,设置 img 和外壳样式 |
|
||||||
|
| `src/pages/ReportEditor.tsx` | `fillPlaceholder` | 同上 |
|
||||||
|
| `src/pages/ReportEditor.tsx` | `autoCaptureFrames` | 同上 |
|
||||||
|
| `src/pages/ReportEditor.tsx` | placeholderModal 确认插入 | style 中增加 `max-width` / `max-height` |
|
||||||
|
| `src/pages/TemplateManage.tsx` | `fillPlaceholderSrc` | 同上 |
|
||||||
|
| `src/pages/TemplateManage.tsx` | placeholderModal 确认插入 | style 中增加 `max-width` / `max-height` |
|
||||||
|
|
||||||
|
### 样式值示例
|
||||||
|
```ts
|
||||||
|
const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
|
||||||
|
const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
|
||||||
|
|
||||||
|
placeholder.innerHTML = `
|
||||||
|
<span class="delete-btn" contenteditable="false">×</span>
|
||||||
|
<img src="${src}" style="max-width:100%;max-height:${mh};display:block;object-fit:contain;object-position:left top;" draggable="false">
|
||||||
|
`;
|
||||||
|
|
||||||
|
placeholder.style.width = 'auto';
|
||||||
|
placeholder.style.height = 'auto';
|
||||||
|
placeholder.style.maxWidth = mw;
|
||||||
|
placeholder.style.maxHeight = mh;
|
||||||
|
placeholder.style.lineHeight = 'normal';
|
||||||
|
placeholder.style.textAlign = 'left';
|
||||||
|
placeholder.style.verticalAlign = 'top';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需求 3:Logo 框大小保持 65px × 65px
|
||||||
|
默认模板中 Logo 占位符的 `width:65px;height:65px` 保持不变。此需求通过不修改 Logo 占位符相关代码即可满足。
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
1. 视频按钮移出条件渲染后,需确保 `videoInputRef` 的引用始终有效。
|
||||||
|
2. 占位符 `width:auto` 后,在表格单元格(`td`)内的表现需要验证,确保不会超出单元格。
|
||||||
|
3. `object-position: left top` 仅在 `object-fit: contain` 时生效。
|
||||||
|
4. 需确保 `max-width` / `max-height` 在打印样式中不会被 `@media print` 规则覆盖。
|
||||||
96
工程分析/测试方案-2026-04-18-16-45-02.md
Normal file
96
工程分析/测试方案-2026-04-18-16-45-02.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-16-45-02
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证代码编纂工作流是否能够正确运行,确保后续所有项目修改需求都能按规范流程执行,无步骤遗漏。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:工作流文档完整性验证
|
||||||
|
**前置条件**:用户已提出建立工作流的需求
|
||||||
|
**操作步骤**:
|
||||||
|
1. 检查 `.\工程分析` 文件夹是否存在
|
||||||
|
2. 检查 `需求分析-2026-04-18-16-45-02.md` 是否生成
|
||||||
|
3. 检查 `实现方案-2026-04-18-16-45-02.md` 是否生成
|
||||||
|
4. 检查 `测试方案-2026-04-18-16-45-02.md` 是否生成
|
||||||
|
5. 检查 `经验记录.md` 是否存在
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- `.\工程分析` 文件夹存在
|
||||||
|
- 需求分析、实现方案、测试方案文档均已生成,内容完整
|
||||||
|
- `经验记录.md` 存在且格式正确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:实现方案审核节点验证
|
||||||
|
**前置条件**:实现方案文档已生成
|
||||||
|
**操作步骤**:
|
||||||
|
1. AI 展示实现方案文档内容
|
||||||
|
2. 用户阅读文档
|
||||||
|
3. 用户回复"确认"或提出修改意见
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- AI 在实现方案生成后主动停止,等待用户输入
|
||||||
|
- 用户未确认前,AI 不进入下一阶段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:测试方案审核节点验证
|
||||||
|
**前置条件**:测试方案文档已生成,且实现方案已通过用户审核
|
||||||
|
**操作步骤**:
|
||||||
|
1. AI 展示测试方案文档内容
|
||||||
|
2. 用户阅读文档
|
||||||
|
3. 用户回复"确认"或提出修改意见
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- AI 在测试方案生成后主动停止,等待用户输入
|
||||||
|
- 用户未确认前,AI 不进入下一阶段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:经验记录读取验证
|
||||||
|
**前置条件**:存在历史经验记录文档
|
||||||
|
**操作步骤**:
|
||||||
|
1. AI 读取 `.\工程分析\经验记录.md`
|
||||||
|
2. 检查是否能正确解析四段式格式
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- AI 能正确读取并理解经验记录内容
|
||||||
|
- 执行代码修改前能引用相关经验防止重复犯错
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:Gitea 备份验证(后续真实需求执行时)
|
||||||
|
**前置条件**:代码修改已完成
|
||||||
|
**操作步骤**:
|
||||||
|
1. AI 执行 `git add .`
|
||||||
|
2. AI 执行 `git commit -m "{时间戳} - {描述}"`
|
||||||
|
3. AI 执行 `git push origin main`
|
||||||
|
4. AI 执行 `git tag` 并推送
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- Commit 成功推送到远程 main 分支
|
||||||
|
- 标签成功推送到远程
|
||||||
|
- AI 向用户汇报备份完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:重新部署验证(后续真实需求执行时)
|
||||||
|
**前置条件**:代码修改已提交
|
||||||
|
**操作步骤**:
|
||||||
|
1. AI 执行 `npm run build`
|
||||||
|
2. 检查 `dist/` 目录是否存在且包含构建产物
|
||||||
|
3. AI 执行 `npm run preview` 或等效部署命令
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 构建成功,无报错
|
||||||
|
- `dist/` 目录包含 `index.html` 和 `assets/`
|
||||||
|
- 预览服务正常运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 无业务代码变更,不涉及回归测试
|
||||||
|
- 需确认 `.\工程分析` 目录下的新文档不会影响项目构建(即 `.gitignore` 或构建配置不会误处理 `.md` 文件)
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
本次测试的核心是验证"工作流机制本身是否成立"。由于不涉及业务代码修改,TC-05 和 TC-06 将在后续真实需求中实际执行并验证。
|
||||||
123
工程分析/测试方案-2026-04-18-16-55-47.md
Normal file
123
工程分析/测试方案-2026-04-18-16-55-47.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-16-55-47
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证 report-editor 的三项改进是否正确实现:field-value 点击联动、右侧字段动态排序、默认模板表格替换。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:点击正文 field-value 切换至基本信息 Tab 并聚焦
|
||||||
|
**前置条件**:进入 /report-editor,加载默认模板,右侧当前在「视频分析」Tab
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击报告正文中「姓名」后的 field-value 方格
|
||||||
|
2. 观察右侧 Tab 切换
|
||||||
|
3. 观察页面滚动位置
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 右侧 Tab 自动从「视频分析」切换为「基本信息」
|
||||||
|
- 页面平滑滚动至「患者姓名」输入框位置
|
||||||
|
- 「患者姓名」输入框获得焦点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:点击不同 field-value 聚焦对应不同表单字段
|
||||||
|
**前置条件**:report-editor 已加载模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击正文中的「住院号」field-value
|
||||||
|
2. 点击正文中的「手术名称」field-value
|
||||||
|
3. 点击正文中的「手术日期」field-value
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 每次点击后右侧均切换至「基本信息」Tab
|
||||||
|
- 对应字段输入框均被聚焦并滚动至可视区域
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:置顶字段顺序验证
|
||||||
|
**前置条件**:report-editor 右侧显示基本信息表单
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看右侧表单字段的从上到下顺序
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 第1个字段为「患者姓名」
|
||||||
|
- 第2个字段为「住院号」
|
||||||
|
- 第3个字段为「手术名称」
|
||||||
|
- 这三个字段始终固定在最上方
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:动态排序验证——按正文出现顺序
|
||||||
|
**前置条件**:默认模板中正文字段有固定出现顺序
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看右侧表单第4个及之后的字段顺序
|
||||||
|
2. 对比正文中 `data-bind` 的首次出现顺序
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 右侧第4个及之后的字段顺序与正文中 `data-bind` 首次出现的先后顺序一致
|
||||||
|
- 正文中越靠前的字段,在右侧表单中也越靠前
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:动态排序验证——修改正文后排序更新
|
||||||
|
**前置条件**:report-editor 中已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 将正文中某个靠后的字段(如「术后诊断」)剪切并粘贴到正文开头
|
||||||
|
2. 观察右侧表单字段顺序变化
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 「术后诊断」在右侧表单中的位置相应提前
|
||||||
|
- 排序随正文内容变化实时更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:默认模板手术图片表格验证
|
||||||
|
**前置条件**:新建报告或重置系统后进入 report-editor
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看编辑器中的「手术图片说明表格」
|
||||||
|
2. 检查每个单元格内容
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 表格为 2 行 × 3 列布局
|
||||||
|
- 每格包含 `image-placeholder` 占位符
|
||||||
|
- 每格底部有对应图注(图A~图F)
|
||||||
|
- 占位符可正常点击上传图片
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-07:表格内占位符图片上传
|
||||||
|
**前置条件**:默认模板已加载
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击表格中某个 `image-placeholder`
|
||||||
|
2. 在弹窗中选择本地上传一张图片
|
||||||
|
3. 确认图片正确填充到占位符中
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 弹窗正常出现(三选一:本地上传/我的签名/系统素材)
|
||||||
|
- 图片正确显示在占位符内
|
||||||
|
- 图片不溢出单元格边界
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-08:新建报告默认内容完整性
|
||||||
|
**前置条件**:退出并重新登录,确保系统使用默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 进入 /report-editor(新建报告)
|
||||||
|
2. 检查整个报告内容
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 报告头部 Logo 和标题正常
|
||||||
|
- 基本信息段落正常
|
||||||
|
- 手术步骤段落正常
|
||||||
|
- 手术图片说明表格为新模板
|
||||||
|
- 手术后情况段落正常
|
||||||
|
- 底部撰写时间字段正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 验证 `image-placeholder` 的拖拽填充、点击上传、删除功能不受影响
|
||||||
|
- 验证右侧 Tab 手动切换(「基本信息」↔「视频分析」)正常
|
||||||
|
- 验证 `smart-field-wrapper` 的双向绑定(表单→正文、正文→表单)正常
|
||||||
|
- 验证打印功能中表格和图片正常显示
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
以上 TC-01~TC-08 全部通过,即可确认三项需求均正确实现。
|
||||||
155
工程分析/测试方案-2026-04-18-17-27-51.md
Normal file
155
工程分析/测试方案-2026-04-18-17-27-51.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-17-27-51
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证 TemplateManage 静态占位符插入修复、默认模板排版重构、Logo 删除按钮修复。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:TemplateManage 插入静态图片占位符
|
||||||
|
**前置条件**:进入 /template-manage,编辑器有焦点
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击工具栏「插入图片占位符」
|
||||||
|
2. 在弹窗中选择「静态图片占位」
|
||||||
|
3. 输入宽度 200,高度 200
|
||||||
|
4. 点击「确认插入」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 编辑器中出现虚线边框的占位符框
|
||||||
|
- 占位符带有 `class="image-placeholder"` 和 `data-mode="manual"`
|
||||||
|
- 占位符内部显示「插入/点击放置图片」文字
|
||||||
|
- 占位符右上角显示红色「×」删除按钮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:TemplateManage 插入手术影像占位符
|
||||||
|
**前置条件**:进入 /template-manage
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击工具栏「插入图片占位符」
|
||||||
|
2. 选择「手术影像占位」
|
||||||
|
3. 点击「确认插入」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 占位符正常显示
|
||||||
|
- 带有 `data-mode="frame"`
|
||||||
|
- 可接受关键帧拖拽填充
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:TemplateManage 占位符删除按钮
|
||||||
|
**前置条件**:已插入占位符
|
||||||
|
**操作步骤**:
|
||||||
|
1. 鼠标悬浮在占位符上
|
||||||
|
2. 点击右上角的红色「×」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 占位符被删除
|
||||||
|
- 撤销按钮可恢复该占位符
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:新建报告默认模板排版——抬头
|
||||||
|
**前置条件**:退出重新登录,进入 /report-editor(新建报告)
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看报告顶部
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 左侧有 65×65 的 Logo 占位符(虚线框)
|
||||||
|
- 中间偏右有 14pt 下划线文字「西 安 交 通 大 学 第 一 附 属 医 院」
|
||||||
|
- 下方有 16pt 文字「手术记录」
|
||||||
|
- 整体居中对齐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:新建报告默认模板排版——基本信息栏
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看抬头下方的基本信息行
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 一行显示:姓名、性别、年龄、科别、床号、住院号
|
||||||
|
- 字体 11pt,不加粗
|
||||||
|
- 整行下方有一条黑色贯穿下划线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:新建报告默认模板排版——诊断信息
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看手术日期、术前诊断、术中诊断、手术名称
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 每项独立一行
|
||||||
|
- 12pt 字体,加粗
|
||||||
|
- 格式为:「手术日期:」+ smartField 占位符
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-07:新建报告默认模板排版——双列信息
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看时间、人员、麻醉信息
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 手术开始/终止时间在同一行,左右各 50%
|
||||||
|
- 手术者/助手在同一行
|
||||||
|
- 麻醉师/麻醉方式在同一行
|
||||||
|
- 12pt 字体,不加粗
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-08:新建报告默认模板排版——手术步骤标题
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「手术步骤、术中出现的情况及处理:」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 12pt 字体,加粗
|
||||||
|
- 位于双列信息下方
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-09:Logo 占位符删除按钮可点击
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 鼠标悬浮在顶部 Logo 占位符上
|
||||||
|
2. 点击右上角的红色「×」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- Logo 占位符被删除
|
||||||
|
- 可撤销恢复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-10:Logo 占位符图片上传
|
||||||
|
**前置条件**:新建报告已加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击顶部 Logo 占位符
|
||||||
|
2. 选择本地上传一张图片
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 图片正确显示在 65×65 区域内
|
||||||
|
- 图片不溢出占位符
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-11:打印效果验证
|
||||||
|
**前置条件**:新建报告,填写部分内容
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击打印按钮
|
||||||
|
2. 检查打印预览
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 抬头排版正确(Logo + 医院名 + 标题)
|
||||||
|
- 基本信息下划线可见
|
||||||
|
- 双列信息左右对齐
|
||||||
|
- 无多余虚线边框(placeholder 填充后 border 应消失)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 验证 `ReportEditor` 中已有的 `image-placeholder` 点击上传、拖拽填充功能不受影响
|
||||||
|
- 验证 `TemplateManage` 中智能字段插入、删除、撤销/重做功能正常
|
||||||
|
- 验证 `smart-field-wrapper` 双向绑定正常工作
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
TC-01~TC-11 全部通过,即可确认三项需求均正确实现。
|
||||||
111
工程分析/测试方案-2026-04-18-17-48-59.md
Normal file
111
工程分析/测试方案-2026-04-18-17-48-59.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-17-48-59
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证默认模板排版微调和打印样式修复是否正确生效。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:基本信息栏字段间距
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「姓名:性别:年龄:科别:床号:住院号:」一行
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 各字段之间仅有一个空格间距
|
||||||
|
- 字段分布紧凑,不会过度分散
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:抬头整体居中
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看报告最顶部
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- Logo 与「西 安 交 通 大 学 第 一 附 属 医 院 + 手术记录」作为一个整体水平居中
|
||||||
|
- Logo 与文字之间间距较小(约 12px)
|
||||||
|
- 不会出现 Logo 偏左、文字偏右的分离感
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:打印时不显示删除按钮
|
||||||
|
**前置条件**:新建报告,填写部分字段内容
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击打印按钮
|
||||||
|
2. 检查打印预览
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 所有红色「×」删除按钮均不可见
|
||||||
|
- `.image-placeholder` 中的 × 不可见
|
||||||
|
- `.smart-field-wrapper` 中的 × 不可见
|
||||||
|
- 已填充的图片占位符正常显示图片
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:全文行距统一
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看手术日期、术前诊断等段落
|
||||||
|
2. 查看手术步骤段落
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 所有段落行距一致,为 1.5 倍
|
||||||
|
- 段落之间无额外 margin/padding 间距
|
||||||
|
- 整体排版紧凑均匀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:打印行距验证
|
||||||
|
**前置条件**:报告有内容
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击打印
|
||||||
|
2. 检查打印预览中的段落间距
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 打印输出行距为 1.5 倍
|
||||||
|
- 无段前段后 padding
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:医院名称下划线贴底
|
||||||
|
**前置条件**:新建报告
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「西 安 交 通 大 学 第 一 附 属 医 院」下方横线
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 下边框紧贴文字底部
|
||||||
|
- 无明显的 padding-bottom 间隙
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-07:基本信息栏下划线贴底
|
||||||
|
**前置条件**:新建报告
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「姓名:...住院号:」整行下方的横线
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 下边框紧贴文字底部
|
||||||
|
- 无明显的 padding-bottom 间隙
|
||||||
|
- 横线与文字之间仅有极小间距(≤ 2px)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-08:打印下划线验证
|
||||||
|
**前置条件**:报告有内容
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击打印
|
||||||
|
2. 检查医院名和基本信息栏的下划线位置
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 打印时下边框紧贴文字底部
|
||||||
|
- 与屏幕预览一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 验证 smart-field-wrapper 的双向绑定(表单→正文、正文→表单)正常工作
|
||||||
|
- 验证 image-placeholder 的点击上传、拖拽填充、删除功能不受影响
|
||||||
|
- 验证手术图片说明表格的 6 图格布局正常
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
TC-01~TC-08 全部通过,即可确认五项排版优化均正确实现。
|
||||||
134
工程分析/测试方案-2026-04-18-18-08-37.md
Normal file
134
工程分析/测试方案-2026-04-18-18-08-37.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-18-08-37
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证编辑器工具栏字号/行距功能、字体选择修复,以及默认模板排版调整。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-01:ReportEditor 字体选择修复
|
||||||
|
**前置条件**:进入 /report-editor,编辑器中有文字
|
||||||
|
**操作步骤**:
|
||||||
|
1. 选中一段文字
|
||||||
|
2. 从工具栏字体下拉框选择「微软雅黑」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 选中的文字字体变为微软雅黑
|
||||||
|
- 编辑器未失去焦点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-02:ReportEditor 字号选择
|
||||||
|
**前置条件**:进入 /report-editor,编辑器中有文字
|
||||||
|
**操作步骤**:
|
||||||
|
1. 选中一段文字
|
||||||
|
2. 从工具栏字号下拉框选择「14pt」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 选中的文字字号变大
|
||||||
|
- 编辑器未失去焦点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-03:ReportEditor 行距选择
|
||||||
|
**前置条件**:进入 /report-editor,编辑器中有多行文字
|
||||||
|
**操作步骤**:
|
||||||
|
1. 将光标放在某一段落内
|
||||||
|
2. 从工具栏行距下拉框选择「2.0」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 当前段落行距变为 2.0
|
||||||
|
- 其他段落不受影响
|
||||||
|
- 草稿自动保存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-04:TemplateManage 工具栏功能
|
||||||
|
**前置条件**:进入 /template-manage
|
||||||
|
**操作步骤**:
|
||||||
|
1. 分别测试字体、字号、行距选择功能
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 字体选择生效
|
||||||
|
- 字号选择生效
|
||||||
|
- 行距选择生效
|
||||||
|
- 撤销/重做能恢复行距修改
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-05:手术者签名右对齐不换行
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 找到「手术者签名」行
|
||||||
|
2. 将光标放在该行,点击工具栏「右对齐」
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 「手术者签名:」文字和图片占位符在同一行
|
||||||
|
- 两者一起靠右对齐
|
||||||
|
- 图片框不会单独换到下一行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-06:手术记录与姓名间距
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「手术记录」标题与「姓名:」之间的间距
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 间距明显缩小(约 4px)
|
||||||
|
- 不再有过大的空白区域
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-07:手术名称与手术开始时间间距
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「手术名称」与「手术开始时间」之间的间距
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 两者间距仅为 1.5 行距的自然间距
|
||||||
|
- 无额外 margin/padding 造成的空白
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-08:手术步骤段落字体统一
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看手术步骤 1~5 的字体大小
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 所有手术步骤段落均为 12pt 字体
|
||||||
|
- 与上方「手术日期」等诊断信息字体大小一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-09:手术后情况段落字体
|
||||||
|
**前置条件**:新建报告,加载默认模板
|
||||||
|
**操作步骤**:
|
||||||
|
1. 查看「手术后情况」「切除标本描述」等段落的字体大小
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 均为 12pt 字体
|
||||||
|
- 行距 1.5,无段前段后间距
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TC-10:打印效果验证
|
||||||
|
**前置条件**:报告有内容
|
||||||
|
**操作步骤**:
|
||||||
|
1. 点击打印
|
||||||
|
2. 检查打印预览
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 字体、字号、行距设置正确反映在打印输出中
|
||||||
|
- 所有删除按钮(×)不可见
|
||||||
|
- 排版紧凑一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试范围
|
||||||
|
- 验证 `smart-field-wrapper` 双向绑定正常工作
|
||||||
|
- 验证 `image-placeholder` 点击上传、拖拽填充功能正常
|
||||||
|
- 验证手术图片说明表格布局未受影响
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
TC-01~TC-10 全部通过,即可确认所有需求均正确实现。
|
||||||
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 全部通过,即可确认所有需求均正确实现。
|
||||||
64
工程分析/测试方案-2026-04-18-19-08-43.md
Normal file
64
工程分析/测试方案-2026-04-18-19-08-43.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-19-08-43
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证六项需求修改的正确性和稳定性。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-1:基础信息字段打印无下划线
|
||||||
|
**前置条件**:新建报告,默认模板已加载。
|
||||||
|
**步骤**:
|
||||||
|
1. 点击「打印预览」或「下载 PDF」。
|
||||||
|
2. 检查「姓名、性别、年龄、科别、床号、住院号」区域。
|
||||||
|
**预期结果**:这 6 个字段不显示下划线,其他字段(如手术名称、诊断等)正常显示下划线。
|
||||||
|
|
||||||
|
### TC-2:点击 field-value 联动高亮并居中滚动
|
||||||
|
**前置条件**:编辑器已加载默认模板,右侧基本信息 Tab 可见。
|
||||||
|
**步骤**:
|
||||||
|
1. 点击正文中任意 `class="field-value"`(如「手术名称」)。
|
||||||
|
2. 观察右侧对应输入框。
|
||||||
|
**预期结果**:
|
||||||
|
- 对应输入框出现蓝色背景高亮(`bg-blue-50 ring-1 ring-accent`)。
|
||||||
|
- 页面平滑滚动,使该输入框位于可视区域中部。
|
||||||
|
- 输入框获得焦点。
|
||||||
|
|
||||||
|
### TC-3:视频上传按钮整合进缩略图列表
|
||||||
|
**前置条件**:已上传至少一个视频。
|
||||||
|
**步骤**:
|
||||||
|
1. 切换到「视频分析」Tab。
|
||||||
|
2. 观察视频缩略图列表。
|
||||||
|
**预期结果**:
|
||||||
|
- 列表第一个位置是一个缩小版「上传视频」按钮,尺寸与视频卡片一致(约 `w-24`)。
|
||||||
|
- 点击该按钮能正常打开文件选择器。
|
||||||
|
- 原本独立的「点击上传手术视频」大按钮已消失。
|
||||||
|
|
||||||
|
### TC-4:视频模块间距紧凑化
|
||||||
|
**前置条件**:视频分析面板展开,有视频和关键帧。
|
||||||
|
**步骤**:
|
||||||
|
1. 观察缩略图列表与播放器之间的间距。
|
||||||
|
2. 观察播放器与控制按钮之间的间距。
|
||||||
|
3. 观察控制按钮与「关键帧摘取」标题之间的间距。
|
||||||
|
**预期结果**:各项间距明显缩小,下方关键帧列表获得更多展示空间。
|
||||||
|
|
||||||
|
### TC-5:签名与日期之间增加空行
|
||||||
|
**前置条件**:默认模板已加载。
|
||||||
|
**步骤**:
|
||||||
|
1. 滚动到模板底部,查看「手术者签名」与「撰写时间」之间。
|
||||||
|
**预期结果**:两者之间有一个空行(约一行高度的空白)。
|
||||||
|
|
||||||
|
### TC-6:图片占位符填充后高度自适应
|
||||||
|
**前置条件**:模板中有空图片占位符,有较小的图片(高度 < 200px)。
|
||||||
|
**步骤**:
|
||||||
|
1. 将图片插入占位符(通过上传、拖拽或自动摘取)。
|
||||||
|
2. 观察占位符区域。
|
||||||
|
**预期结果**:
|
||||||
|
- 占位符高度随图片实际尺寸自适应,不再保留 200px 固定高度。
|
||||||
|
- 图片下方不会出现大片空白。
|
||||||
|
|
||||||
|
## 回归测试
|
||||||
|
- 确保打印功能(PDF 导出)正常工作。
|
||||||
|
- 确保视频播放、关键帧摘取、拖拽插入功能正常。
|
||||||
|
- 确保 `template-manage` 中的图片占位符同样支持高度自适应。
|
||||||
|
|
||||||
|
## 测试通过标准
|
||||||
|
所有用例均通过,无控制台报错,打印样式正常。
|
||||||
54
工程分析/测试方案-2026-04-18-19-23-31.md
Normal file
54
工程分析/测试方案-2026-04-18-19-23-31.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 测试方案 —— 2026-04-18-19-23-31
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
验证视频分析模块空白修复和图片占位符自适应逻辑。
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### TC-1:视频分析模块无视频时显示上传按钮
|
||||||
|
**前置条件**:新建报告,切换到「视频分析」Tab,尚未上传任何视频。
|
||||||
|
**步骤**:
|
||||||
|
1. 点击「视频分析」Tab。
|
||||||
|
**预期结果**:
|
||||||
|
- 面板显示「上传视频」按钮(缩小版,在水平滚动区域首位)。
|
||||||
|
- 面板不显示视频播放器和关键帧区域。
|
||||||
|
- 点击上传按钮可正常打开文件选择器。
|
||||||
|
|
||||||
|
### TC-2:视频分析模块有视频时正常显示
|
||||||
|
**前置条件**:已上传至少一个视频。
|
||||||
|
**步骤**:
|
||||||
|
1. 切换到「视频分析」Tab。
|
||||||
|
**预期结果**:
|
||||||
|
- 上传按钮和视频缩略图列表均可见。
|
||||||
|
- 选中视频后,播放器和关键帧区域正常显示。
|
||||||
|
|
||||||
|
### TC-3:图片占位符填充后尺寸自适应(小图片)
|
||||||
|
**前置条件**:模板中有 200×200 的图片占位符,准备一张 100×80 的小图片。
|
||||||
|
**步骤**:
|
||||||
|
1. 将小图片插入占位符。
|
||||||
|
**预期结果**:
|
||||||
|
- 占位符宽度收缩为约 100px,高度收缩为约 80px。
|
||||||
|
- 图片靠左上方放置,无多余空白。
|
||||||
|
|
||||||
|
### TC-4:图片占位符填充后尺寸自适应(大图片)
|
||||||
|
**前置条件**:模板中有 200×200 的图片占位符,准备一张 800×600 的大图片。
|
||||||
|
**步骤**:
|
||||||
|
1. 将大图片插入占位符。
|
||||||
|
**预期结果**:
|
||||||
|
- 图片等比例缩小,最大不超过 200×200。
|
||||||
|
- 占位符宽度收缩为缩小后的图片宽度(≤200px),高度同理。
|
||||||
|
- 图片靠左上方放置。
|
||||||
|
|
||||||
|
### TC-5:Logo 占位符大小保持 65px × 65px
|
||||||
|
**前置条件**:默认模板已加载。
|
||||||
|
**步骤**:
|
||||||
|
1. 检查顶部 Logo 占位符。
|
||||||
|
**预期结果**:占位符尺寸为 65px × 65px,不受本次修改影响。
|
||||||
|
|
||||||
|
## 回归测试
|
||||||
|
- 确保视频播放、关键帧摘取、拖拽插入功能正常。
|
||||||
|
- 确保 `template-manage` 中的图片占位符同样支持尺寸自适应。
|
||||||
|
- 确保打印样式正常,图片不会被截断。
|
||||||
|
|
||||||
|
## 测试通过标准
|
||||||
|
所有用例均通过,无控制台报错,视频模块和图片占位符行为符合预期。
|
||||||
29
工程分析/经验记录.md
29
工程分析/经验记录.md
@@ -942,3 +942,32 @@ if ((settings.autoInsertDelay || 0) > 0) {
|
|||||||
**D. 后续如何避免问题**
|
**D. 后续如何避免问题**
|
||||||
- 当为 `image-placeholder` 引入新的核心属性(如 `data-mode`、`data-allow-source`)时,必须同步检索 `defaultContent.ts` 和任何预置模板文件,确保静态模板中的占位符结构与运行时插入逻辑保持一致。
|
- 当为 `image-placeholder` 引入新的核心属性(如 `data-mode`、`data-allow-source`)时,必须同步检索 `defaultContent.ts` 和任何预置模板文件,确保静态模板中的占位符结构与运行时插入逻辑保持一致。
|
||||||
- 默认模板修改后,应通过「新建报告 → 检查 DOM」快速验证所有占位符是否携带了最新属性。
|
- 默认模板修改后,应通过「新建报告 → 检查 DOM」快速验证所有占位符是否携带了最新属性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 记录 31:六项 UI/UX 优化集中实施
|
||||||
|
|
||||||
|
**A. 具体问题**
|
||||||
|
用户提出六项体验优化需求:基础信息字段打印无下划线、编辑器字段联动高亮、视频上传按钮整合、视频面板间距紧凑化、签名与日期之间空行、图片占位符填充后高度自适应。
|
||||||
|
|
||||||
|
**B. 产生问题原因**
|
||||||
|
均为长期使用中积累的交互和排版细节问题:
|
||||||
|
1. 默认模板的基础字段(姓名/性别/年龄/科别/床号/住院号)打印时默认带下划线,但临床场景中这些字段通常不需要下划线。
|
||||||
|
2. 编辑器中点击正文 `field-value` 后右侧没有视觉反馈,用户不知道对应哪个输入框。
|
||||||
|
3. 视频上传按钮独立占一行,浪费垂直空间。
|
||||||
|
4. 视频面板各区域间距过大,挤压了关键帧列表的展示空间。
|
||||||
|
5. 签名和日期之间缺少空行,排版拥挤。
|
||||||
|
6. 图片占位符填充后仍保留固定高度(如 200px),导致图片下方出现大片空白。
|
||||||
|
|
||||||
|
**C. 解决问题方案**
|
||||||
|
1. **基础字段无下划线**:在 `defaultContent.ts` 的 `smartField()` 中硬编码 6 个 key(`patientName`, `patientGender`, `patientAge`, `department`, `bedNumber`, `hospitalId`),自动注入 `.no-underline` 类;同时保留 `hasUnderline` 配置机制供 TemplateManage 自定义。
|
||||||
|
2. **字段联动高亮**:新增 `activeFieldKey` 状态;点击 `field-value` 时设置该状态并滚动到对应 `id={`input-${bindKey}`}` 元素;为右侧所有字段类型(text/date/single_select/multi_select/time)的容器统一添加 `p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`。
|
||||||
|
3. **视频按钮整合**:删除独立的大按钮,在缩略图滚动容器的首位插入缩小版按钮(`shrink-0 w-24 h-[68px]`),样式与视频卡片一致。
|
||||||
|
4. **视频间距紧凑**:将 `space-y-4` 逐层改为 `space-y-2`;关键帧摘取标题区域改为 `pt-1 border-t border-border`。
|
||||||
|
5. **签名空行**:在签名 `<p>` 和日期 `<p>` 之间插入 `<p style="margin:0;padding:0;line-height:1.5;"> </p>`。
|
||||||
|
6. **占位符高度自适应**:在 `fillPlaceholderSrc`、`fillPlaceholder`、`autoCaptureFrames`(ReportEditor)以及 `fillPlaceholderSrc`(TemplateManage)中,填充图片后统一设置 `placeholder.style.height = 'auto'; placeholder.style.width = 'auto'; placeholder.style.lineHeight = 'normal';`,并将图片 style 中的 `max-height:100%;object-fit:contain` 改为 `height:auto`。
|
||||||
|
|
||||||
|
**D. 后续如何避免问题**
|
||||||
|
- 当为 `image-placeholder` 修改填充后的样式行为时,必须同步检索所有填充入口(`fillPlaceholderSrc`、`fillPlaceholder`、自动帧插入、拖拽填充等),并同步到 `TemplateManage.tsx`。
|
||||||
|
- 右侧表单字段容器样式如果统一(如高亮背景),应在所有字段类型的渲染分支中同步添加,避免某些类型遗漏。
|
||||||
|
- 默认模板修改后应通过「新建报告 → 检查 DOM 结构」快速验证。
|
||||||
|
|||||||
55
工程分析/需求分析-2026-04-18-16-45-02.md
Normal file
55
工程分析/需求分析-2026-04-18-16-45-02.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-16-45-02
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户明确要求建立一套标准化的代码编纂工作流,用于规范后续所有项目修改需求的处理流程。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
新建一个完整的代码编纂工作流。后续用户提出的任何项目修改相关需求,都必须严格按照该工作流执行。
|
||||||
|
|
||||||
|
## 工作流步骤定义
|
||||||
|
|
||||||
|
### 0. 时间记录
|
||||||
|
每次执行前,记录问题开始的时间,格式为 `{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}`。
|
||||||
|
|
||||||
|
### 1. 工程分析文件夹
|
||||||
|
阅读或创建 `.\工程分析` 文件夹,用于存放对整个工程的整体分析文档。
|
||||||
|
|
||||||
|
### 2. 需求分析文档
|
||||||
|
每次用户提出的需求,都整理写入 `.\工程分析\需求分析-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md` 文档中。
|
||||||
|
|
||||||
|
### 3. 实现方案文档
|
||||||
|
每次将实现方案写入 `.\工程分析\实现方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md` 文档中。
|
||||||
|
**关键约束**:该文档写完后必须经过用户二次人工审核确认,方可进入下一步。
|
||||||
|
|
||||||
|
### 4. 测试方案文档
|
||||||
|
将测试方案写入 `.\工程分析\测试方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md` 文档中。
|
||||||
|
**关键约束**:该文档写完后必须经过用户二次人工审核确认,方可进入下一步。
|
||||||
|
|
||||||
|
### 5. 经验记录与知识库
|
||||||
|
- **执行前**:阅读 `.\工程分析\经验记录.md`,防止重复犯错。
|
||||||
|
- **执行后**:在 `.\工程分析\经验记录.md` 中,将执行过程中遇到的关键问题及解决方案,按以下四段式格式追加记录:
|
||||||
|
- A. 具体问题
|
||||||
|
- B. 产生问题原因
|
||||||
|
- C. 解决问题方案
|
||||||
|
- D. 后续如何避免问题
|
||||||
|
|
||||||
|
### 6. Gitea 备份
|
||||||
|
最终执行完成后,使用 Gitea 对文档进行备份。Commit 信息需包含:
|
||||||
|
- `{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}` 时间戳
|
||||||
|
- 对本次修改的简要描述
|
||||||
|
完成后提醒用户已完成文档备份。
|
||||||
|
|
||||||
|
### 7. 重新部署
|
||||||
|
最终执行完成后,重新执行 `npm` 部署本项目。
|
||||||
|
|
||||||
|
## 本次需求的特殊性
|
||||||
|
本次需求本身即为"建立工作流规范",不涉及具体业务代码的增删改。因此实现内容主要是:
|
||||||
|
1. 确认 `.\工程分析` 目录结构已就绪
|
||||||
|
2. 将工作流规范固化为可重复执行的流程文档
|
||||||
|
3. 确保用户理解每个步骤的审核节点
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 工程分析文档体系
|
||||||
|
- 后续所有项目修改需求的执行方式
|
||||||
|
- Gitea 备份流程
|
||||||
|
- 无业务代码变更
|
||||||
27
工程分析/需求分析-2026-04-18-16-55-47.md
Normal file
27
工程分析/需求分析-2026-04-18-16-55-47.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-16-55-47
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户直接提出三项 report-editor 相关改进需求。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:点击正文 field-value 联动右侧基本信息
|
||||||
|
在 `report-editor` 中,点击报告正文内 `class="field-value"` 的元素时,自动将右侧面板切换至「基本信息」栏目,并聚焦/滚动到该字段对应的表单输入框。
|
||||||
|
|
||||||
|
### 需求 2:右侧基本信息字段按正文出现顺序动态排序
|
||||||
|
右侧「基本信息」栏目中:
|
||||||
|
- **固定置顶**:患者姓名 (`patientName`)、住院号 (`hospitalId`)、手术名称 (`title`) 始终排在最上方,顺序固定。
|
||||||
|
- **动态排序**:其余字段按其在报告正文 HTML 中 `data-bind` 出现的先后顺序排列。
|
||||||
|
- **兜底处理**:正文中未出现的字段排在末尾。
|
||||||
|
|
||||||
|
### 需求 3:替换默认模板中的手术图片说明表格
|
||||||
|
将 `src/utils/defaultContent.ts` 中的 `<!-- 手术图片说明表格 -->` 默认模板替换为用户提供的 6 图格 HTML 代码(含腹腔镜探查、胆囊管夹闭与离断、胆囊动脉夹闭与离断、胆囊剥离与床面止血、胆囊取出与钛夹确认、止血材料覆盖及检查)。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/pages/ReportEditor.tsx`(需求 1、2)
|
||||||
|
- `src/utils/defaultContent.ts`(需求 3)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 报告编辑器交互体验
|
||||||
|
- 右侧基本信息面板渲染逻辑
|
||||||
|
- 默认报告模板内容
|
||||||
29
工程分析/需求分析-2026-04-18-17-27-51.md
Normal file
29
工程分析/需求分析-2026-04-18-17-27-51.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-17-27-51
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户提出 TemplateManage 功能修复与默认报告模板排版重构需求。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:修复 TemplateManage 静态图片占位符插入不显示
|
||||||
|
在 `template-manage` 中通过弹窗选择「静态图片占位」并点击「确认插入」后,编辑器中没有出现 `class="image-placeholder"` 的占位符框。经分析,原因是 `document.execCommand('insertHTML')` 对复杂嵌套 HTML(含 `contenteditable="false"`)的自动修正/过滤行为不可靠。
|
||||||
|
|
||||||
|
### 需求 2:重构默认报告模板顶部排版
|
||||||
|
根据用户提供的视觉参考图片,重写 `defaultContent.ts` 顶部排版:
|
||||||
|
- **抬头**:左侧 Logo(65×65 静态占位),右侧 14 号字体的「西 安 交 通 大 学 第 一 附 属 医 院」(带下划线),下方 16 号字体「手术记录」。
|
||||||
|
- **基本信息栏**:11 号字体、不加粗、带贯穿下划线的一行:姓名、性别、年龄、科别、床号、住院号。
|
||||||
|
- **诊断/手术信息**:12 号字体、加粗的单行:手术日期、术前诊断、术中诊断、手术名称。
|
||||||
|
- **双列信息**:12 号字体、不加粗、两项一行:手术开始/终止时间、手术者/助手、麻醉师/麻醉方式。
|
||||||
|
- **手术步骤标题**:12 号字体、加粗的「手术步骤、术中出现的情况及处理:」。
|
||||||
|
|
||||||
|
### 需求 3:修复顶部 Logo 占位符删除按钮无法点击
|
||||||
|
当前默认模板中 65px×65px 的 Logo 占位符右上角的「×」删除按钮无法点击。原因是占位符缺少 `position: relative`,导致绝对定位的删除按钮点击区域溢出或被遮挡。需保留其「静态图片占位 (`data-mode="manual"`)」逻辑。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/pages/TemplateManage.tsx`(需求 1:修复 insertImage 插入方式)
|
||||||
|
- `src/utils/defaultContent.ts`(需求 2、3:重构模板排版 + Logo 修复)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 模板管理页面的图片占位符插入功能
|
||||||
|
- 新建报告时的默认模板视觉效果
|
||||||
|
- 打印输出时的顶部排版
|
||||||
30
工程分析/需求分析-2026-04-18-17-48-59.md
Normal file
30
工程分析/需求分析-2026-04-18-17-48-59.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-17-48-59
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户基于打印预览效果,提出默认模板排版微调和打印样式修复需求。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:缩减基本信息栏字段间空格
|
||||||
|
当前默认模板中「姓名:性别:年龄:科别:床号:住院号:」之间使用了 ` `(三个不间断空格),间距过大。需缩减为单个空格 ` `。
|
||||||
|
|
||||||
|
### 需求 2:Logo 与医院名/标题靠拢并整体居中
|
||||||
|
当前顶部使用 3 列 table(20%-60%-20%),Logo 固定在左侧 20% 区域,与中间标题距离过远。需改为 Flex 布局,使 Logo 与文字内容作为一个整体水平居中,且两者间距缩小。
|
||||||
|
|
||||||
|
### 需求 3:打印时隐藏所有「×」删除按钮
|
||||||
|
打印预览中,`.smart-field-wrapper` 内的 `.delete-btn`(红色×)仍然可见。`print.ts` 中仅隐藏了 `.image-placeholder .delete-btn`,遗漏了文本字段中的删除按钮。需全局隐藏 `.delete-btn`。
|
||||||
|
|
||||||
|
### 需求 4:统一全文行距为 1.5,消除段前段后间距
|
||||||
|
当前模板中各 `<p>` 标签的 `line-height` 不统一(有 1.8、默认行高等),且部分段落有默认 margin/padding。需统一为 `line-height: 1.5; margin: 0; padding: 0;`。
|
||||||
|
|
||||||
|
### 需求 5:下划线紧贴文字底部
|
||||||
|
「西 安 交 通 大 学 第 一 附 属 医 院」下方的 `border-bottom` 和「姓名:」等基本信息栏下方的 `border-bottom` 与文字间距过大。需移除 `padding-bottom`,并通过 `line-height: 1` 或类似手段消除字体底部留白,使横线紧贴文字底部。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/utils/defaultContent.ts`(需求 1、2、4、5)
|
||||||
|
- `src/utils/print.ts`(需求 3、4)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 默认报告模板的视觉效果
|
||||||
|
- 打印输出样式
|
||||||
|
- 无业务逻辑变更
|
||||||
34
工程分析/需求分析-2026-04-18-18-08-37.md
Normal file
34
工程分析/需求分析-2026-04-18-18-08-37.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-18-08-37
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户提出报告编辑器与模板管理器的工具栏功能增强,以及默认模板排版细节调整。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:修复字体选择并新增字号、行距功能
|
||||||
|
在 `report-editor` 和 `template-manage` 的工具栏中:
|
||||||
|
- **修复字体选择**:当前 `document.execCommand('fontName')` 可能因浏览器兼容性或 CSS 覆盖而失效,需确保字体选择能正确生效。
|
||||||
|
- **新增字号选择**:在工具栏字体选择旁边增加字号下拉框,支持 12pt/14pt/18pt 等常用字号。
|
||||||
|
- **新增行距选择**:在工具栏增加行距下拉框,支持 1.0/1.5/2.0 等行距。由于 `execCommand` 不原生支持行距,需通过直接修改 DOM 元素的 `style.lineHeight` 实现。
|
||||||
|
|
||||||
|
### 需求 2:修复手术者签名右对齐时图片框换行
|
||||||
|
当「手术者签名」所在行设置 `text-align: right` 时,文字跑到最右侧,而图片占位符(`display: inline-flex`)换到了下一行。需确保文字和图片在同一行内保持连续。
|
||||||
|
|
||||||
|
### 需求 3:缩减「手术记录」与「姓名」之间的距离
|
||||||
|
当前顶部 Flex 容器的 `margin-bottom: 16px` 导致标题与基本信息栏间距过大。需缩小该间距。
|
||||||
|
|
||||||
|
### 需求 4:消除「手术名称」与「手术开始时间」之间的多余间距
|
||||||
|
「手术名称」是 `<p>` 标签,「手术开始时间」在 `<table>` 中。`<table>` 的默认 margin 或 `<p>` 的默认间距导致两者距离过远。需消除多余间距,保持 1.5 行距且无段前段后间距。
|
||||||
|
|
||||||
|
### 需求 5:统一「手术日期」及以下内容为 12pt、1.5 行距、无段间距
|
||||||
|
当前手术步骤段落(1~5)缺少 `font-size: 12pt`,导致与上方诊断信息字体大小不一致。需统一从「手术日期」开始往下的所有正文内容为 12pt、1.5 行距、无段前段后间距。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/pages/ReportEditor.tsx`(需求 1:工具栏增强)
|
||||||
|
- `src/pages/TemplateManage.tsx`(需求 1:工具栏增强)
|
||||||
|
- `src/utils/defaultContent.ts`(需求 2~5:模板排版修复)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 编辑器工具栏交互
|
||||||
|
- 默认报告模板视觉效果
|
||||||
|
- 打印输出样式
|
||||||
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)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 报告管理列表展示
|
||||||
|
- 模板字段配置体系
|
||||||
|
- 编辑器/模板管理器工具栏交互
|
||||||
|
- 打印输出样式
|
||||||
|
- 文件导出功能
|
||||||
41
工程分析/需求分析-2026-04-18-19-08-43.md
Normal file
41
工程分析/需求分析-2026-04-18-19-08-43.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-19-08-43
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户基于实际使用体验,提出六项界面交互和排版优化需求。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:基础信息字段默认无下划线
|
||||||
|
默认模板中的「姓名、性别、年龄、科别、床号、住院号」6 个基础信息字段,在打印时默认不显示下划线。通过在 `smartField()` 函数中根据 key 自动注入 `.no-underline` 类实现。
|
||||||
|
|
||||||
|
### 需求 2:点击 field-value 联动高亮并居中滚动
|
||||||
|
在 `report-editor` 中点击正文 `class="field-value"` 时:
|
||||||
|
- 右侧基本信息对应输入框高亮显示蓝色背景(类似 `template-manage` 中的字段高亮效果)
|
||||||
|
- 自动滚动到屏幕可视区域的 1/3~2/3 位置(使用 `block: 'center'`)
|
||||||
|
|
||||||
|
### 需求 3:视频上传按钮整合进缩略图列表
|
||||||
|
将 `report-editor` 右侧「视频分析」Tab 中原本独立占据一行的「上传视频」大按钮,缩小并移入水平滚动的视频缩略图列表首位(`flex gap-2 overflow-x-auto`),尺寸与视频卡片保持一致(约 `w-24 h-[68px]`),节省垂直空间。
|
||||||
|
|
||||||
|
### 需求 4:视频模块间距紧凑化
|
||||||
|
缩减「视频分析」面板中以下间距:
|
||||||
|
- 视频缩略图列表与下方视频播放器之间的距离
|
||||||
|
- 视频播放器与播放控制按钮之间的距离
|
||||||
|
- 播放控制按钮与「关键帧摘取」标题之间的距离
|
||||||
|
为下方关键帧列表腾出更多展示空间。
|
||||||
|
|
||||||
|
### 需求 5:签名与日期之间增加空行
|
||||||
|
在 `defaultContent.ts` 中,「手术者签名」行与「撰写时间」行之间插入一个空段落 `<p> </p>`,使排版更美观。
|
||||||
|
|
||||||
|
### 需求 6:图片占位符填充后高度自适应
|
||||||
|
当图片占位符被填充图片后,若图片实际高度小于占位符预设高度(如 200px),占位符仍保留固定高度导致下方出现大片空白。需在填充图片后,将占位符的 `height`、`width`、`line-height` 重置为 `auto` / `normal`,让高度由图片实际尺寸决定。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/utils/defaultContent.ts`(需求 1、5)
|
||||||
|
- `src/pages/ReportEditor.tsx`(需求 2、3、4、6)
|
||||||
|
- `src/pages/TemplateManage.tsx`(需求 6)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 默认模板打印样式
|
||||||
|
- 编辑器交互体验
|
||||||
|
- 视频分析面板布局
|
||||||
|
- 图片占位符自适应行为
|
||||||
30
工程分析/需求分析-2026-04-18-19-23-31.md
Normal file
30
工程分析/需求分析-2026-04-18-19-23-31.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 需求分析 —— 2026-04-18-19-23-31
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
用户在实际使用中发现两个问题,要求进行修复和优化。
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 需求 1:修复视频分析模块空白问题
|
||||||
|
在 `ReportEditor` 中,上一轮修改将「上传视频」按钮移入了 `videos.length > 0` 的条件渲染内部,导致当没有视频时,整个「视频分析」面板变为空白,用户无法上传第一个视频。
|
||||||
|
|
||||||
|
**预期行为**:无论是否有已上传视频,「上传视频」按钮和缩略图滚动列表都应始终可见。
|
||||||
|
|
||||||
|
### 需求 2:图片占位符尺寸自适应与等比例缩放限制
|
||||||
|
当前图片占位符填充图片后,虽然高度变为 `auto`,但宽度仍保持预设值(如 200px),导致图片在占位符内居中显示,周围仍有大量空白。用户希望:
|
||||||
|
- 预设的宽高仅作为**最大限制**(`max-width` / `max-height`)
|
||||||
|
- 如果图片超出限制,则等比例缩小
|
||||||
|
- 图片靠左上方放置(`object-position: left top`)
|
||||||
|
- 占位符自身的虚线框大小要**紧缩包围(shrink-wrap)**成图片实际缩放后的尺寸
|
||||||
|
|
||||||
|
### 需求 3:Logo 框大小保持 65px × 65px
|
||||||
|
默认模板中顶部医院 Logo 占位符的尺寸应保持 65px × 65px 不变。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
- `src/pages/ReportEditor.tsx`(需求 1、2)
|
||||||
|
- `src/pages/TemplateManage.tsx`(需求 2)
|
||||||
|
|
||||||
|
## 需求影响范围
|
||||||
|
- 视频分析面板的可见性逻辑
|
||||||
|
- 图片占位符的填充后样式行为
|
||||||
|
- 打印/预览时的图片尺寸表现
|
||||||
Reference in New Issue
Block a user