2026-04-17-00-13-09 - 手术时间方框联动、动态字段分类管理体系、字段显隐控制、UI紧凑化优化
This commit is contained in:
@@ -100,21 +100,27 @@
|
||||
.smart-field-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 4px;
|
||||
vertical-align: middle;
|
||||
margin: 0 2px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.smart-field-wrapper .field-label {
|
||||
color: #64748b;
|
||||
user-select: none;
|
||||
}
|
||||
.smart-field-wrapper .field-value {
|
||||
min-width: 60px;
|
||||
min-width: 32px;
|
||||
padding: 0 4px;
|
||||
margin: 0 2px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
background: #fff;
|
||||
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;
|
||||
}
|
||||
.smart-field-wrapper .field-value:empty::before {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { User, Template, SystemSettings } from '../types';
|
||||
import { User, Template, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types';
|
||||
import { defaultReportContent } from '../utils/defaultContent';
|
||||
import { storage } from '../utils/storage';
|
||||
import { User as UserIcon, Lock } from 'lucide-react';
|
||||
@@ -41,6 +41,11 @@ export default function Login() {
|
||||
console.log('Default users initialized');
|
||||
}
|
||||
|
||||
const fieldsConfig = storage.get<FormField[]>('formFieldsConfig', []);
|
||||
if (fieldsConfig.length === 0) {
|
||||
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
||||
}
|
||||
|
||||
const settingsRaw = storage.get<SystemSettings>('systemSettings', {} as SystemSettings);
|
||||
if (!settingsRaw.frameCount) {
|
||||
const round1 = (n: number) => Math.round(n * 10) / 10;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
AlignLeft, AlignCenter, AlignRight, Table, Image as ImageIcon,
|
||||
Video, Play, Pause, Plus, X, ChevronLeft
|
||||
} from 'lucide-react';
|
||||
import { User, Report, Template, CapturedFrame, SystemSettings } from '../types';
|
||||
import { User, Report, Template, CapturedFrame, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types';
|
||||
import { defaultReportContent } from '../utils/defaultContent';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { storage } from '../utils/storage';
|
||||
@@ -61,6 +61,7 @@ export default function ReportEditor() {
|
||||
const [anesthesiaOptions, setAnesthesiaOptions] = useState<string[]>(['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉']);
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
||||
const [formFields, setFormFields] = useState<FormField[]>([]);
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
@@ -108,6 +109,14 @@ export default function ReportEditor() {
|
||||
const savedAnesthesia = storage.get<string[] | null>('anesthesiaOptions', null);
|
||||
if (savedAnesthesia) setAnesthesiaOptions(savedAnesthesia);
|
||||
|
||||
const savedFields = storage.get<FormField[]>('formFieldsConfig', []);
|
||||
if (savedFields.length > 0) {
|
||||
setFormFields(savedFields);
|
||||
} else {
|
||||
setFormFields(DEFAULT_FORM_FIELDS);
|
||||
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
||||
}
|
||||
|
||||
const allTemplates = storage.get<Template[]>('templates', []);
|
||||
const visibleTplIds = Array.isArray(user.visibleTemplates) ? user.visibleTemplates : allTemplates.map(t => t.id);
|
||||
const filteredTemplates = allTemplates.filter(t => visibleTplIds.includes(t.id));
|
||||
@@ -776,8 +785,8 @@ export default function ReportEditor() {
|
||||
const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
||||
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
|
||||
|
||||
const addTag = (field: 'surgeon' | 'assistant' | 'anesthesiologist', value: string) => {
|
||||
const current = reportData[field] || [];
|
||||
const addTag = (field: string, value: string) => {
|
||||
const current = (reportData as any)[field] || [];
|
||||
if (!current.includes(value)) {
|
||||
const next = { ...reportData, [field]: [...current, value] };
|
||||
setReportData(next);
|
||||
@@ -791,21 +800,35 @@ export default function ReportEditor() {
|
||||
setMultiSelectOptions(next);
|
||||
storage.set('multiSelectOptions', next);
|
||||
}
|
||||
// Sync to formFieldsConfig
|
||||
const fieldDef = formFields.find(f => f.key === field);
|
||||
if (fieldDef && fieldDef.options && !fieldDef.options.includes(value)) {
|
||||
const updatedFields = formFields.map(f => f.key === field ? { ...f, options: [...(f.options || []), value] } : f);
|
||||
setFormFields(updatedFields);
|
||||
storage.set('formFieldsConfig', updatedFields);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (field: 'surgeon' | 'assistant' | 'anesthesiologist', value: string) => {
|
||||
const current = reportData[field] || [];
|
||||
const next = { ...reportData, [field]: current.filter(v => v !== value) };
|
||||
const removeTag = (field: string, value: string) => {
|
||||
const current = (reportData as any)[field] || [];
|
||||
const next = { ...reportData, [field]: current.filter((v: string) => v !== value) };
|
||||
setReportData(next);
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
saveDraftToStorage();
|
||||
};
|
||||
|
||||
const removeMultiOption = (field: 'surgeon' | 'assistant' | 'anesthesiologist', value: string) => {
|
||||
const removeMultiOption = (field: string, value: string) => {
|
||||
const current = multiSelectOptions[field] || [];
|
||||
const next = { ...multiSelectOptions, [field]: current.filter(v => v !== value) };
|
||||
setMultiSelectOptions(next);
|
||||
storage.set('multiSelectOptions', next);
|
||||
// Sync to formFieldsConfig
|
||||
const fieldDef = formFields.find(f => f.key === field);
|
||||
if (fieldDef && fieldDef.options) {
|
||||
const updatedFields = formFields.map(f => f.key === field ? { ...f, options: (f.options || []).filter(v => v !== value) } : f);
|
||||
setFormFields(updatedFields);
|
||||
storage.set('formFieldsConfig', updatedFields);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAnesthesiaOption = (value: string) => {
|
||||
@@ -885,11 +908,27 @@ export default function ReportEditor() {
|
||||
const fieldKey = target.getAttribute('data-bind')!;
|
||||
const newValue = target.innerText;
|
||||
|
||||
setReportData((prev) => {
|
||||
const next = { ...prev, [fieldKey]: newValue };
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
return next;
|
||||
});
|
||||
if (fieldKey === 'startTime') {
|
||||
const parts = newValue.split(':');
|
||||
setReportData((prev) => {
|
||||
const next = { ...prev, startHour: parts[0] || '', startMinute: parts[1] || '' };
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
return next;
|
||||
});
|
||||
} else if (fieldKey === 'endTime') {
|
||||
const parts = newValue.split(':');
|
||||
setReportData((prev) => {
|
||||
const next = { ...prev, endHour: parts[0] || '', endMinute: parts[1] || '' };
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setReportData((prev) => {
|
||||
const next = { ...prev, [fieldKey]: newValue };
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -900,13 +939,21 @@ export default function ReportEditor() {
|
||||
bindNodes.forEach((node) => {
|
||||
const el = node as HTMLElement;
|
||||
const fieldKey = el.getAttribute('data-bind')!;
|
||||
const rawValue = (reportData as any)[fieldKey];
|
||||
|
||||
let newValue = '';
|
||||
if (Array.isArray(rawValue)) {
|
||||
newValue = rawValue.join(', ');
|
||||
} else if (rawValue !== undefined && rawValue !== null) {
|
||||
newValue = String(rawValue);
|
||||
if (fieldKey === 'startTime') {
|
||||
newValue = `${reportData.startHour || ''}:${reportData.startMinute || ''}`;
|
||||
if (newValue === ':') newValue = '';
|
||||
} else if (fieldKey === 'endTime') {
|
||||
newValue = `${reportData.endHour || ''}:${reportData.endMinute || ''}`;
|
||||
if (newValue === ':') newValue = '';
|
||||
} else {
|
||||
const rawValue = (reportData as any)[fieldKey];
|
||||
if (Array.isArray(rawValue)) {
|
||||
newValue = rawValue.join(', ');
|
||||
} else if (rawValue !== undefined && rawValue !== null) {
|
||||
newValue = String(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (el.innerText !== newValue) {
|
||||
@@ -1075,256 +1122,190 @@ export default function ReportEditor() {
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
{activeTab === 'info' && (
|
||||
<div className="report-info-form space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">患者姓名 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.patientName}
|
||||
onChange={(e) => { const next = {...reportData, patientName: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
onBlur={() => setTouched(t => ({ ...t, patientName: true }))}
|
||||
className={`input-minimal ${touched.patientName && !reportData.patientName ? 'border-red-500' : ''}`}
|
||||
placeholder="患者姓名"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">住院号 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.hospitalId}
|
||||
onChange={(e) => { const next = {...reportData, hospitalId: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
onBlur={() => setTouched(t => ({ ...t, hospitalId: true }))}
|
||||
className={`input-minimal ${touched.hospitalId && !reportData.hospitalId ? 'border-red-500' : ''}`}
|
||||
placeholder="住院号"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{formFields.filter(f => f.visibleInForm).map(field => {
|
||||
const isRequired = field.isSystemLocked;
|
||||
const hasError = isRequired && touched[field.key] && !(reportData as any)[field.key];
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">手术名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.title}
|
||||
onChange={(e) => { const next = {...reportData, title: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal"
|
||||
placeholder="请输入手术名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">患者性别</label>
|
||||
<select
|
||||
value={reportData.patientGender}
|
||||
onChange={(e) => { const next = {...reportData, patientGender: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white"
|
||||
>
|
||||
<option value="">请选择</option>
|
||||
<option value="男">男</option>
|
||||
<option value="女">女</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">患者年龄</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.patientAge}
|
||||
onChange={(e) => { const next = {...reportData, patientAge: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal"
|
||||
placeholder="年龄"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">科别</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.department}
|
||||
onChange={(e) => { const next = {...reportData, department: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal"
|
||||
placeholder="科室"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">床号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reportData.bedNumber}
|
||||
onChange={(e) => { const next = {...reportData, bedNumber: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal"
|
||||
placeholder="床号"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">手术日期</label>
|
||||
<input
|
||||
type="date"
|
||||
value={reportData.surgeryDate}
|
||||
onChange={(e) => { const next = {...reportData, surgeryDate: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">手术开始时间</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={reportData.startHour}
|
||||
onChange={(e) => { const next = {...reportData, startHour: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{hourOptions.map(h => <option key={h} value={h}>{h}</option>)}
|
||||
</select>
|
||||
<span className="text-text-muted">:</span>
|
||||
<select
|
||||
value={reportData.startMinute}
|
||||
onChange={(e) => { const next = {...reportData, startMinute: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{minuteOptions.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">手术终止时间</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={reportData.endHour}
|
||||
onChange={(e) => { const next = {...reportData, endHour: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{hourOptions.map(h => <option key={h} value={h}>{h}</option>)}
|
||||
</select>
|
||||
<span className="text-text-muted">:</span>
|
||||
<select
|
||||
value={reportData.endMinute}
|
||||
onChange={(e) => { const next = {...reportData, endMinute: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{minuteOptions.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(['surgeon', 'assistant', 'anesthesiologist'] as const).map((field) => {
|
||||
const labels = { surgeon: '手术者', assistant: '助手', anesthesiologist: '麻醉师' };
|
||||
const isOpen = openDropdown === field;
|
||||
return (
|
||||
<div key={field} className="space-y-1 select-dropdown-root relative">
|
||||
<label className="block text-xs font-bold text-text-main">{labels[field]}</label>
|
||||
<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"
|
||||
onClick={() => setOpenDropdown(field)}
|
||||
>
|
||||
{(reportData[field] || []).map(tag => (
|
||||
<span key={tag} className="px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 flex items-center gap-1">
|
||||
{tag}
|
||||
<span className="cursor-pointer hover:text-amber-900" onClick={(e) => { e.stopPropagation(); removeTag(field, tag); }}>×</span>
|
||||
</span>
|
||||
))}
|
||||
if (field.type === 'text' || field.type === 'date') {
|
||||
const inputType = field.type === 'date' ? 'date' : 'text';
|
||||
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'}>
|
||||
<label className="block text-xs font-bold text-text-main">
|
||||
{field.label} {isRequired && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="outline-none text-sm min-w-[60px] flex-1 bg-transparent"
|
||||
placeholder="输入或选择"
|
||||
onFocus={() => setOpenDropdown(field)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (val) { addTag(field, val); (e.target as HTMLInputElement).value = ''; }
|
||||
}
|
||||
}}
|
||||
type={inputType}
|
||||
value={(reportData as any)[field.key] || ''}
|
||||
onChange={(e) => { const next = { ...reportData, [field.key]: e.target.value }; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
onBlur={() => setTouched(t => ({ ...t, [field.key]: true }))}
|
||||
className={`input-minimal ${hasError ? 'border-red-500' : ''}`}
|
||||
placeholder={field.label}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded-lg shadow-lg max-h-[150px] overflow-y-auto">
|
||||
{(multiSelectOptions[field] || []).map(opt => (
|
||||
<div
|
||||
key={opt}
|
||||
className="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex justify-between items-center"
|
||||
onClick={() => { addTag(field, opt); }}
|
||||
>
|
||||
<span>{opt}</span>
|
||||
<span
|
||||
className="text-red-500 text-xs hover:bg-red-50 px-1 rounded"
|
||||
onClick={(e) => { e.stopPropagation(); removeMultiOption(field, opt); }}
|
||||
>×</span>
|
||||
</div>
|
||||
))}
|
||||
{(multiSelectOptions[field] || []).length === 0 && (
|
||||
<div className="px-3 py-2 text-xs text-text-muted">暂无选项</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
}
|
||||
|
||||
<div className="space-y-1 select-dropdown-root relative">
|
||||
<label className="block text-xs font-bold text-text-main">麻醉方式</label>
|
||||
<div
|
||||
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text"
|
||||
onClick={() => setOpenDropdown('anesthesia')}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="outline-none text-sm flex-1 bg-transparent"
|
||||
placeholder="输入或选择麻醉方式"
|
||||
value={reportData.anesthesiaType || ''}
|
||||
onChange={(e) => { const next = {...reportData, anesthesiaType: e.target.value}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
onFocus={() => setOpenDropdown('anesthesia')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (val) {
|
||||
const next = {...reportData, anesthesiaType: val};
|
||||
setReportData(next);
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
saveDraftToStorage();
|
||||
if (!anesthesiaOptions.includes(val)) {
|
||||
const next = [...anesthesiaOptions, val];
|
||||
setAnesthesiaOptions(next);
|
||||
storage.set('anesthesiaOptions', next);
|
||||
}
|
||||
setOpenDropdown(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{openDropdown === 'anesthesia' && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded-lg shadow-lg max-h-[150px] overflow-y-auto">
|
||||
{anesthesiaOptions.map(opt => (
|
||||
<div
|
||||
key={opt}
|
||||
className="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex justify-between items-center"
|
||||
onClick={() => { const next = {...reportData, anesthesiaType: opt}; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); setOpenDropdown(null); }}
|
||||
if (field.type === 'single_select') {
|
||||
const isOpen = openDropdown === field.key;
|
||||
const opts = field.options || (field.key === 'anesthesiaType' ? anesthesiaOptions : []);
|
||||
return (
|
||||
<div key={field.key} className="space-y-1 select-dropdown-root relative">
|
||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||
<div
|
||||
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text"
|
||||
onClick={() => setOpenDropdown(field.key)}
|
||||
>
|
||||
<span>{opt}</span>
|
||||
<span
|
||||
className="text-red-500 text-xs hover:bg-red-50 px-1 rounded"
|
||||
onClick={(e) => { e.stopPropagation(); removeAnesthesiaOption(opt); }}
|
||||
>×</span>
|
||||
<input
|
||||
type="text"
|
||||
className="outline-none text-sm flex-1 bg-transparent"
|
||||
placeholder={`输入或选择${field.label}`}
|
||||
value={(reportData as any)[field.key] || ''}
|
||||
onChange={(e) => { const next = { ...reportData, [field.key]: e.target.value }; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
onFocus={() => setOpenDropdown(field.key)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (val) {
|
||||
const next = { ...reportData, [field.key]: val };
|
||||
setReportData(next);
|
||||
stateRef.current = { ...stateRef.current, reportData: next };
|
||||
saveDraftToStorage();
|
||||
if (!opts.includes(val)) {
|
||||
const updatedOpts = [...opts, val];
|
||||
if (field.key === 'anesthesiaType') {
|
||||
setAnesthesiaOptions(updatedOpts);
|
||||
storage.set('anesthesiaOptions', updatedOpts);
|
||||
}
|
||||
const updatedFields = formFields.map(f => f.key === field.key ? { ...f, options: updatedOpts } : f);
|
||||
setFormFields(updatedFields);
|
||||
storage.set('formFieldsConfig', updatedFields);
|
||||
}
|
||||
setOpenDropdown(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{anesthesiaOptions.length === 0 && (
|
||||
<div className="px-3 py-2 text-xs text-text-muted">暂无选项</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded-lg shadow-lg max-h-[150px] overflow-y-auto">
|
||||
{opts.map(opt => (
|
||||
<div
|
||||
key={opt}
|
||||
className="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex justify-between items-center"
|
||||
onClick={() => { const next = { ...reportData, [field.key]: opt }; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); setOpenDropdown(null); }}
|
||||
>
|
||||
<span>{opt}</span>
|
||||
<span
|
||||
className="text-red-500 text-xs hover:bg-red-50 px-1 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const updatedOpts = opts.filter(v => v !== opt);
|
||||
if (field.key === 'anesthesiaType') {
|
||||
setAnesthesiaOptions(updatedOpts);
|
||||
storage.set('anesthesiaOptions', updatedOpts);
|
||||
}
|
||||
const updatedFields = formFields.map(f => f.key === field.key ? { ...f, options: updatedOpts } : f);
|
||||
setFormFields(updatedFields);
|
||||
storage.set('formFieldsConfig', updatedFields);
|
||||
}}
|
||||
>×</span>
|
||||
</div>
|
||||
))}
|
||||
{opts.length === 0 && (
|
||||
<div className="px-3 py-2 text-xs text-text-muted">暂无选项</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'multi_select') {
|
||||
const isOpen = openDropdown === field.key;
|
||||
const opts = field.options || multiSelectOptions[field.key] || [];
|
||||
return (
|
||||
<div key={field.key} className="space-y-1 select-dropdown-root relative">
|
||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||
<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"
|
||||
onClick={() => setOpenDropdown(field.key)}
|
||||
>
|
||||
{((reportData as any)[field.key] || []).map((tag: string) => (
|
||||
<span key={tag} className="px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 flex items-center gap-1">
|
||||
{tag}
|
||||
<span className="cursor-pointer hover:text-amber-900" onClick={(e) => { e.stopPropagation(); removeTag(field.key, tag); }}>×</span>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="outline-none text-sm min-w-[60px] flex-1 bg-transparent"
|
||||
placeholder="输入或选择"
|
||||
onFocus={() => setOpenDropdown(field.key)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (val) { addTag(field.key, val); (e.target as HTMLInputElement).value = ''; }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded-lg shadow-lg max-h-[150px] overflow-y-auto">
|
||||
{opts.map(opt => (
|
||||
<div
|
||||
key={opt}
|
||||
className="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex justify-between items-center"
|
||||
onClick={() => { addTag(field.key, opt); }}
|
||||
>
|
||||
<span>{opt}</span>
|
||||
<span
|
||||
className="text-red-500 text-xs hover:bg-red-50 px-1 rounded"
|
||||
onClick={(e) => { e.stopPropagation(); removeMultiOption(field.key, opt); }}
|
||||
>×</span>
|
||||
</div>
|
||||
))}
|
||||
{opts.length === 0 && (
|
||||
<div className="px-3 py-2 text-xs text-text-muted">暂无选项</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'time') {
|
||||
const hourKey = field.key === 'startTime' ? 'startHour' : 'endHour';
|
||||
const minuteKey = field.key === 'startTime' ? 'startMinute' : 'endMinute';
|
||||
return (
|
||||
<div key={field.key} className="space-y-1">
|
||||
<label className="block text-xs font-bold text-text-main">{field.label}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={(reportData as any)[hourKey] || ''}
|
||||
onChange={(e) => { const next = { ...reportData, [hourKey]: e.target.value }; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{hourOptions.map(h => <option key={h} value={h}>{h}</option>)}
|
||||
</select>
|
||||
<span className="text-text-muted">:</span>
|
||||
<select
|
||||
value={(reportData as any)[minuteKey] || ''}
|
||||
onChange={(e) => { const next = { ...reportData, [minuteKey]: e.target.value }; setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage(); }}
|
||||
className="input-minimal bg-white flex-1"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{minuteOptions.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 { User, Template, BINDABLE_FIELDS } from '../types';
|
||||
import { User, Template, FormField, FieldType, DEFAULT_FORM_FIELDS } from '../types';
|
||||
import { defaultReportContent } from '../utils/defaultContent';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { storage } from '../utils/storage';
|
||||
@@ -18,6 +18,10 @@ export default function TemplateManage() {
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const savedRangeRef = useRef<Range | null>(null);
|
||||
const [fieldLibTab, setFieldLibTab] = useState<'insert' | 'manage'>('insert');
|
||||
const [formFields, setFormFields] = useState<FormField[]>([]);
|
||||
const [newFieldForm, setNewFieldForm] = useState({ label: '', category: '填空', type: 'text' as FieldType });
|
||||
const [newFieldOptions, setNewFieldOptions] = useState('');
|
||||
|
||||
const updatePageHeight = () => {
|
||||
if (!editorRef.current) return;
|
||||
@@ -36,6 +40,14 @@ export default function TemplateManage() {
|
||||
}
|
||||
setCurrentUser(user);
|
||||
|
||||
const savedFields = storage.get<FormField[]>('formFieldsConfig', []);
|
||||
if (savedFields.length > 0) {
|
||||
setFormFields(savedFields);
|
||||
} else {
|
||||
setFormFields(DEFAULT_FORM_FIELDS);
|
||||
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
|
||||
}
|
||||
|
||||
const savedTemplates = storage.get<Template[]>('templates', []);
|
||||
if (savedTemplates.length === 0) {
|
||||
const initial: Template = {
|
||||
@@ -156,15 +168,14 @@ export default function TemplateManage() {
|
||||
editorRef.current?.focus();
|
||||
};
|
||||
|
||||
const insertSmartField = (field: typeof BINDABLE_FIELDS[0]) => {
|
||||
const insertSmartField = (field: FormField) => {
|
||||
editorRef.current?.focus();
|
||||
const html = `
|
||||
<span class="smart-field-wrapper" contenteditable="false">
|
||||
<span class="field-label">${field.label}:</span>
|
||||
<span class="field-value"
|
||||
data-bind="${field.key}"
|
||||
contenteditable="true"
|
||||
style="min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a;">
|
||||
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;">
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
@@ -172,6 +183,39 @@ export default function TemplateManage() {
|
||||
editorRef.current?.focus();
|
||||
};
|
||||
|
||||
const toggleFieldVisible = (key: string) => {
|
||||
const updated = formFields.map(f => f.key === key ? { ...f, visibleInForm: !f.visibleInForm } : f);
|
||||
setFormFields(updated);
|
||||
storage.set('formFieldsConfig', updated);
|
||||
};
|
||||
|
||||
const deleteField = (key: string) => {
|
||||
const updated = formFields.filter(f => f.key !== key);
|
||||
setFormFields(updated);
|
||||
storage.set('formFieldsConfig', updated);
|
||||
};
|
||||
|
||||
const addField = () => {
|
||||
if (!newFieldForm.label.trim()) return;
|
||||
const key = 'custom_' + Date.now();
|
||||
const newField: FormField = {
|
||||
key,
|
||||
label: newFieldForm.label.trim(),
|
||||
category: newFieldForm.category,
|
||||
type: newFieldForm.type,
|
||||
visibleInForm: true,
|
||||
isSystemLocked: false,
|
||||
options: ['单选', '多选'].includes(newFieldForm.category) && newFieldOptions.trim()
|
||||
? newFieldOptions.split(/[,,]/).map(s => s.trim()).filter(Boolean)
|
||||
: undefined
|
||||
};
|
||||
const updated = [...formFields, newField];
|
||||
setFormFields(updated);
|
||||
storage.set('formFieldsConfig', updated);
|
||||
setNewFieldForm({ label: '', category: '填空', type: 'text' });
|
||||
setNewFieldOptions('');
|
||||
};
|
||||
|
||||
const insertTable = () => {
|
||||
const rowsStr = prompt('请输入行数:', '2');
|
||||
const colsStr = prompt('请输入列数:', '3');
|
||||
@@ -469,28 +513,126 @@ export default function TemplateManage() {
|
||||
</div>
|
||||
|
||||
{/* Right: Field Library */}
|
||||
<aside className="w-[220px] bg-sidebar-bg border-l border-border flex flex-col shrink-0 overflow-hidden">
|
||||
<div className="p-4 border-b border-border">
|
||||
<span className="text-sm font-bold text-text-main uppercase tracking-wider">字段库</span>
|
||||
<aside className="w-[240px] bg-sidebar-bg border-l border-border flex flex-col shrink-0 overflow-hidden">
|
||||
<div className="flex border-b border-border">
|
||||
{(['insert', 'manage'] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setFieldLibTab(tab)}
|
||||
className={`flex-1 py-3 text-xs font-bold transition-all border-b-2 uppercase tracking-wider ${
|
||||
fieldLibTab === tab ? 'text-accent border-accent' : 'text-text-muted border-transparent hover:text-text-main'
|
||||
}`}
|
||||
>
|
||||
{tab === 'insert' ? '插入字段' : '字段管理'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<div className="card-minimal p-3">
|
||||
<h3 className="text-xs font-semibold text-primary mb-2">表单字段</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{BINDABLE_FIELDS.map((field) => (
|
||||
<button
|
||||
key={field.key}
|
||||
type="button"
|
||||
onClick={() => insertSmartField(field)}
|
||||
className="px-2 py-1 text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
|
||||
title={`插入 ${field.label}`}
|
||||
>
|
||||
{field.label}
|
||||
</button>
|
||||
))}
|
||||
{fieldLibTab === 'insert' && (
|
||||
<div className="space-y-4">
|
||||
{['填空', '单选', '多选', '时间'].map(cat => {
|
||||
const catFields = formFields.filter(f => f.category === cat);
|
||||
if (catFields.length === 0) return null;
|
||||
return (
|
||||
<div key={cat}>
|
||||
<div className="text-[10px] text-slate-400 mb-1.5 font-medium">{cat}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{catFields.map(field => (
|
||||
<button
|
||||
key={field.key}
|
||||
type="button"
|
||||
onClick={() => insertSmartField(field)}
|
||||
className="px-2 py-1 text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
|
||||
title={`插入 ${field.label}`}
|
||||
>
|
||||
{field.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 mt-2 leading-tight">点击插入智能占位方格,Label 锁定,Value 可输入并与报告基本信息联动。</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fieldLibTab === 'manage' && (
|
||||
<div className="space-y-3">
|
||||
{formFields.filter(f => !f.isSystemLocked).map(field => (
|
||||
<div key={field.key} className="flex items-center justify-between p-2 bg-slate-50 rounded border border-slate-200">
|
||||
<div className="text-xs">
|
||||
<div className="font-medium text-text-main">{field.label}</div>
|
||||
<div className="text-[10px] text-slate-400">{field.category} · {field.type}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-[10px] text-slate-600 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.visibleInForm}
|
||||
onChange={() => toggleFieldVisible(field.key)}
|
||||
/>
|
||||
显示
|
||||
</label>
|
||||
<button onClick={() => deleteField(field.key)} className="text-red-500 text-[10px] hover:underline">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-3 border-t border-slate-200 space-y-2">
|
||||
<div className="text-xs font-semibold text-text-main">新增字段</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newFieldForm.label}
|
||||
onChange={(e) => setNewFieldForm({ ...newFieldForm, label: e.target.value })}
|
||||
placeholder="字段名称"
|
||||
className="w-full px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={newFieldForm.category}
|
||||
onChange={(e) => {
|
||||
const cat = e.target.value;
|
||||
let t: FieldType = 'text';
|
||||
if (cat === '单选') t = 'single_select';
|
||||
else if (cat === '多选') t = 'multi_select';
|
||||
else if (cat === '时间') t = 'date';
|
||||
setNewFieldForm({ ...newFieldForm, category: cat, type: t });
|
||||
}}
|
||||
className="flex-1 px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent bg-white"
|
||||
>
|
||||
<option value="填空">填空</option>
|
||||
<option value="单选">单选</option>
|
||||
<option value="多选">多选</option>
|
||||
<option value="时间">时间</option>
|
||||
</select>
|
||||
<select
|
||||
value={newFieldForm.type}
|
||||
onChange={(e) => setNewFieldForm({ ...newFieldForm, type: e.target.value as FieldType })}
|
||||
className="flex-1 px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent bg-white"
|
||||
>
|
||||
<option value="text">文本</option>
|
||||
{newFieldForm.category === '单选' && <option value="single_select">下拉单选</option>}
|
||||
{newFieldForm.category === '多选' && <option value="multi_select">标签多选</option>}
|
||||
{newFieldForm.category === '时间' && <><option value="date">日期</option><option value="time">时分</option></>}
|
||||
</select>
|
||||
</div>
|
||||
{['单选', '多选'].includes(newFieldForm.category) && (
|
||||
<input
|
||||
type="text"
|
||||
value={newFieldOptions}
|
||||
onChange={(e) => setNewFieldOptions(e.target.value)}
|
||||
placeholder="选项,用逗号分隔"
|
||||
className="w-full px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={addField}
|
||||
className="w-full py-1.5 bg-accent text-white text-xs font-semibold rounded hover:opacity-90 transition-colors"
|
||||
>
|
||||
添加字段
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
31
src/types.ts
31
src/types.ts
@@ -94,8 +94,39 @@ export const BINDABLE_FIELDS: BindableField[] = [
|
||||
{ key: 'hospitalId', label: '住院号' },
|
||||
{ key: 'surgeryDate', label: '手术日期' },
|
||||
{ key: 'title', label: '手术名称' },
|
||||
{ key: 'startTime', label: '手术开始时间' },
|
||||
{ key: 'endTime', label: '手术终止时间' },
|
||||
{ key: 'surgeon', label: '手术者' },
|
||||
{ key: 'assistant', label: '助手' },
|
||||
{ key: 'anesthesiologist', label: '麻醉师' },
|
||||
{ key: 'anesthesiaType', label: '麻醉方式' },
|
||||
];
|
||||
|
||||
export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date';
|
||||
|
||||
export interface FormField {
|
||||
key: string;
|
||||
label: string;
|
||||
category: string;
|
||||
type: FieldType;
|
||||
visibleInForm: boolean;
|
||||
isSystemLocked: boolean;
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_FORM_FIELDS: FormField[] = [
|
||||
{ key: 'patientName', label: '患者姓名', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true },
|
||||
{ key: 'hospitalId', label: '住院号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: true },
|
||||
{ key: 'title', label: '手术名称', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'patientGender', label: '患者性别', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['男', '女'] },
|
||||
{ key: 'patientAge', label: '患者年龄', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'department', label: '科别', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'bedNumber', label: '床号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'surgeryDate', label: '手术日期', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false },
|
||||
{ key: 'surgeon', label: '手术者', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['张医生', '李医生', '王医生'] },
|
||||
{ key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['赵医生', '钱医生', '孙医生'] },
|
||||
{ key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['周医生', '吴医生', '郑医生'] },
|
||||
{ key: 'anesthesiaType', label: '麻醉方式', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉'] },
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ const smartField = (key: string) => `
|
||||
<span class="field-value"
|
||||
data-bind="${key}"
|
||||
contenteditable="true"
|
||||
style="min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a;">
|
||||
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;">
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
@@ -49,8 +49,8 @@ export const defaultReportContent = `
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
手术开始时间:<span style="color: #bdbdbd;">时 分</span>
|
||||
手术终止时间:<span style="color: #bdbdbd;">时 分</span>
|
||||
手术开始时间:${smartField('startTime')}
|
||||
手术终止时间:${smartField('endTime')}
|
||||
</p>
|
||||
|
||||
<p style="font-family: SimSun;">
|
||||
|
||||
@@ -34,9 +34,9 @@ export const printDocument = (htmlContent: string) => {
|
||||
.image-placeholder .delete-btn { display: none !important; }
|
||||
.image-placeholder:not(.has-image) { display: none !important; }
|
||||
.template-info-section { position: relative; margin-bottom: 16px; }
|
||||
.smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 4px; vertical-align: middle; }
|
||||
.smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 2px; vertical-align: text-bottom; }
|
||||
.smart-field-wrapper .field-label { color: #64748b; user-select: none; }
|
||||
.smart-field-wrapper .field-value { min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a; outline: none; }
|
||||
.smart-field-wrapper .field-value { 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; }
|
||||
@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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user