[第五批] 手术图文病历报告系统 - 时间字段增强:自定义格式、固定时间默认值、系统锁定标签

This commit is contained in:
2026-04-17 22:20:33 +08:00
parent 8beb534abb
commit ac6b619549
4 changed files with 191 additions and 111 deletions

View File

@@ -260,34 +260,63 @@ export default function ReportEditor() {
let hasChange = false; let hasChange = false;
const updates: any = {}; const updates: any = {};
formFields.forEach(field => { formFields.forEach(field => {
if (field.timeDefault !== 'current') return; if (field.timeDefault === 'current') {
if (field.type === 'date') { if (field.type === 'date') {
const current = new Date().toISOString().split('T')[0]; const current = new Date().toISOString().split('T')[0];
if (!(reportData as any)[field.key]) {
updates[field.key] = current;
hasChange = true;
}
} else if (field.type === 'time') {
const now = new Date();
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
if (field.key === 'startTime') {
if (!reportData.startHour) {
updates.startHour = hh;
updates.startMinute = mm;
hasChange = true;
}
} else if (field.key === 'endTime') {
if (!reportData.endHour) {
updates.endHour = hh;
updates.endMinute = mm;
hasChange = true;
}
} else {
if (!(reportData as any)[field.key]) { if (!(reportData as any)[field.key]) {
updates[field.key] = `${hh}:${mm}`; updates[field.key] = current;
hasChange = true; hasChange = true;
} }
} else if (field.type === 'time') {
const now = new Date();
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
if (field.key === 'startTime') {
if (!reportData.startHour) {
updates.startHour = hh;
updates.startMinute = mm;
hasChange = true;
}
} else if (field.key === 'endTime') {
if (!reportData.endHour) {
updates.endHour = hh;
updates.endMinute = mm;
hasChange = true;
}
} else {
if (!(reportData as any)[field.key]) {
updates[field.key] = `${hh}:${mm}`;
hasChange = true;
}
}
}
} else if (field.timeDefault === 'specific' && field.fixedTimeValue) {
if (field.type === 'date') {
if (!(reportData as any)[field.key]) {
updates[field.key] = field.fixedTimeValue;
hasChange = true;
}
} else if (field.type === 'time') {
if (field.key === 'startTime') {
if (!reportData.startHour) {
const [hh, mm] = field.fixedTimeValue.split(':');
updates.startHour = hh || '';
updates.startMinute = mm || '';
hasChange = true;
}
} else if (field.key === 'endTime') {
if (!reportData.endHour) {
const [hh, mm] = field.fixedTimeValue.split(':');
updates.endHour = hh || '';
updates.endMinute = mm || '';
hasChange = true;
}
} else {
if (!(reportData as any)[field.key]) {
updates[field.key] = field.fixedTimeValue;
hasChange = true;
}
}
} }
} }
}); });
@@ -873,25 +902,50 @@ export default function ReportEditor() {
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0')); const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
const formatDateDisplay = (isoDate: string, fmt?: string): string => { const formatDateDisplay = (isoDate: string, fmt?: string): string => {
if (!isoDate) return ''; if (!isoDate || !fmt) return isoDate || '';
if (fmt === 'YYYY年MM月DD日') { const [y, m, d] = isoDate.split('-');
const [y, m, d] = isoDate.split('-'); return fmt
if (y && m && d) return `${y}${m}${d}`; .replace(/YYYY/g, y || '')
} .replace(/MM/g, m || '')
return isoDate; .replace(/DD/g, d || '');
}; };
const formatTimeDisplay = (timeStr: string, fmt?: string): string => { const formatTimeDisplay = (timeStr: string, fmt?: string): string => {
if (!timeStr) return ''; if (!timeStr || !fmt) return timeStr || '';
if (fmt === '12h') { const [h24str, mstr] = timeStr.split(':');
const [hStr, mStr] = timeStr.split(':'); const h24 = parseInt(h24str) || 0;
let h = parseInt(hStr); const isPM = h24 >= 12;
const ampm = h >= 12 ? '下午' : '上午'; let h12 = h24 % 12;
h = h % 12; if (h12 === 0) h12 = 12;
if (h === 0) h = 12; return fmt
return `${String(h).padStart(2, '0')}:${mStr} ${ampm}`; .replace(/HH/g, String(h24).padStart(2, '0'))
.replace(/mm/g, mstr || '00')
.replace(/hh/g, String(h12).padStart(2, '0'))
.replace(/A/g, isPM ? '下午' : '上午');
};
const parseDateFromFormat = (text: string, fmt?: string): string => {
if (!text || !fmt) return text;
const nums = text.match(/\d+/g);
if (!nums) return text;
let y = '', m = '', d = '';
if (nums.length >= 3) { y = nums[0].padStart(4, '0'); m = nums[1].padStart(2, '0'); d = nums[2].padStart(2, '0'); }
else if (nums.length === 2) { m = nums[0].padStart(2, '0'); d = nums[1].padStart(2, '0'); y = new Date().getFullYear().toString(); }
return `${y}-${m}-${d}`;
};
const parseTimeFromFormat = (text: string, fmt?: string): string => {
if (!text || !fmt) return text;
const nums = text.match(/\d+/g);
const ampm = text.match(/上午|下午/);
if (!nums || nums.length < 2) return text;
let h = parseInt(nums[0]);
if (ampm) {
const isPM = ampm[0] === '下午';
if (isPM && h !== 12) h += 12;
if (!isPM && h === 12) h = 0;
} }
return timeStr; return `${String(h).padStart(2, '0')}:${nums[1].padStart(2, '0')}`;
}; };
const to24h = (h12: number, isPM: boolean): number => { const to24h = (h12: number, isPM: boolean): number => {
@@ -1044,15 +1098,8 @@ export default function ReportEditor() {
const fieldDef = formFields.find(f => f.key === fieldKey); const fieldDef = formFields.find(f => f.key === fieldKey);
if (fieldKey === 'startTime') { if (fieldKey === 'startTime') {
let raw = newValue; let raw = newValue;
if (fieldDef?.timeFormat === '12h') { if (fieldDef?.timeFormat && (fieldDef.timeFormat.includes('hh') || fieldDef.timeFormat.includes('A'))) {
const m = newValue.match(/(\d{2}):(\d{2})\s*(上午|下午)/); raw = parseTimeFromFormat(newValue, fieldDef.timeFormat);
if (m) {
let h = parseInt(m[1]);
const isPM = m[3] === '下午';
if (isPM && h !== 12) h += 12;
if (!isPM && h === 12) h = 0;
raw = `${String(h).padStart(2, '0')}:${m[2]}`;
}
} }
const parts = raw.split(':'); const parts = raw.split(':');
setReportData((prev) => { setReportData((prev) => {
@@ -1062,15 +1109,8 @@ export default function ReportEditor() {
}); });
} else if (fieldKey === 'endTime') { } else if (fieldKey === 'endTime') {
let raw = newValue; let raw = newValue;
if (fieldDef?.timeFormat === '12h') { if (fieldDef?.timeFormat && (fieldDef.timeFormat.includes('hh') || fieldDef.timeFormat.includes('A'))) {
const m = newValue.match(/(\d{2}):(\d{2})\s*(上午|下午)/); raw = parseTimeFromFormat(newValue, fieldDef.timeFormat);
if (m) {
let h = parseInt(m[1]);
const isPM = m[3] === '下午';
if (isPM && h !== 12) h += 12;
if (!isPM && h === 12) h = 0;
raw = `${String(h).padStart(2, '0')}:${m[2]}`;
}
} }
const parts = raw.split(':'); const parts = raw.split(':');
setReportData((prev) => { setReportData((prev) => {
@@ -1080,18 +1120,10 @@ export default function ReportEditor() {
}); });
} else { } else {
let raw = newValue; let raw = newValue;
if (fieldDef?.type === 'date' && fieldDef.timeFormat === 'YYYY年MM月DD日') { if (fieldDef?.type === 'date') {
const m = newValue.match(/(\d{4})年(\d{2})月(\d{2})日/); raw = parseDateFromFormat(newValue, fieldDef.timeFormat);
if (m) raw = `${m[1]}-${m[2]}-${m[3]}`; } else if (fieldDef?.type === 'time') {
} else if (fieldDef?.type === 'time' && fieldDef.timeFormat === '12h') { raw = parseTimeFromFormat(newValue, fieldDef.timeFormat);
const m = newValue.match(/(\d{2}):(\d{2})\s*(上午|下午)/);
if (m) {
let h = parseInt(m[1]);
const isPM = m[3] === '下午';
if (isPM && h !== 12) h += 12;
if (!isPM && h === 12) h = 0;
raw = `${String(h).padStart(2, '0')}:${m[2]}`;
}
} }
setReportData((prev) => { setReportData((prev) => {
const next = { ...prev, [fieldKey]: raw }; const next = { ...prev, [fieldKey]: raw };

View File

@@ -33,8 +33,11 @@ export default function TemplateManage() {
const [editFieldOptions, setEditFieldOptions] = useState(''); const [editFieldOptions, setEditFieldOptions] = useState('');
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 [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 [customTimeFormats, setCustomTimeFormats] = useState<string[]>([]);
const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]); const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]);
const updatePageHeight = () => { const updatePageHeight = () => {
@@ -81,6 +84,10 @@ export default function TemplateManage() {
.catch(() => setImageAssets([])); .catch(() => setImageAssets([]));
} }
const savedFormats = storage.get<string[]>('customTimeFormats', []);
const defaultFormats = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A'];
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...savedFormats])));
const savedTemplates = storage.get<Template[]>('templates', []); const savedTemplates = storage.get<Template[]>('templates', []);
if (savedTemplates.length === 0) { if (savedTemplates.length === 0) {
const initial: Template = { const initial: Template = {
@@ -429,6 +436,7 @@ export default function TemplateManage() {
if (f.category === '时间') { if (f.category === '时间') {
next.timeFormat = editFieldTimeFormat; next.timeFormat = editFieldTimeFormat;
next.timeDefault = editFieldTimeDefault; next.timeDefault = editFieldTimeDefault;
next.fixedTimeValue = editFieldFixedTimeValue;
} }
return next; return next;
}); });
@@ -454,6 +462,7 @@ export default function TemplateManage() {
if (newFieldForm.category === '时间') { if (newFieldForm.category === '时间') {
newField.timeFormat = newFieldTimeFormat; newField.timeFormat = newFieldTimeFormat;
newField.timeDefault = newFieldTimeDefault; newField.timeDefault = newFieldTimeDefault;
newField.fixedTimeValue = newFieldFixedTimeValue;
} }
const updated = [...formFields, newField]; const updated = [...formFields, newField];
setFormFields(updated); setFormFields(updated);
@@ -462,6 +471,7 @@ export default function TemplateManage() {
setNewFieldOptions(''); setNewFieldOptions('');
setNewFieldTimeFormat('YYYY-MM-DD'); setNewFieldTimeFormat('YYYY-MM-DD');
setNewFieldTimeDefault('specific'); setNewFieldTimeDefault('specific');
setNewFieldFixedTimeValue('');
}; };
const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -874,6 +884,7 @@ export default function TemplateManage() {
setEditFieldOptions((field.options || []).join(', ')); setEditFieldOptions((field.options || []).join(', '));
setEditFieldTimeFormat(field.timeFormat || ''); setEditFieldTimeFormat(field.timeFormat || '');
setEditFieldTimeDefault(field.timeDefault || 'specific'); setEditFieldTimeDefault(field.timeDefault || 'specific');
setEditFieldFixedTimeValue(field.fixedTimeValue || '');
}} }}
className={`cursor-pointer rounded border p-2 transition-all ${activeFieldKey === field.key ? 'border-accent bg-blue-50 ring-1 ring-accent' : 'border-slate-100 bg-slate-50 hover:border-slate-200'}`} className={`cursor-pointer rounded border p-2 transition-all ${activeFieldKey === field.key ? 'border-accent bg-blue-50 ring-1 ring-accent' : 'border-slate-100 bg-slate-50 hover:border-slate-200'}`}
> >
@@ -906,27 +917,45 @@ export default function TemplateManage() {
onChange={(e) => setEditFieldTimeDefault(e.target.value as 'current' | 'specific')} onChange={(e) => setEditFieldTimeDefault(e.target.value as 'current' | 'specific')}
className="w-full px-1.5 py-1 text-xs border border-border rounded bg-white" className="w-full px-1.5 py-1 text-xs border border-border rounded bg-white"
> >
<option value="specific"></option> <option value="specific"></option>
<option value="current"></option> <option value="current"></option>
</select> </select>
<select {editFieldTimeDefault === 'specific' && (
<input
type={field.type === 'date' ? 'date' : 'time'}
value={editFieldFixedTimeValue}
onChange={(e) => setEditFieldFixedTimeValue(e.target.value)}
className="w-full px-1.5 py-1 text-xs border border-border rounded"
/>
)}
<input
list={`edit-format-list-${field.key}`}
value={editFieldTimeFormat} value={editFieldTimeFormat}
onChange={(e) => setEditFieldTimeFormat(e.target.value)} onChange={(e) => setEditFieldTimeFormat(e.target.value)}
className="w-full px-1.5 py-1 text-xs border border-border rounded bg-white" onBlur={(e) => {
> const val = e.target.value.trim();
{field.type === 'date' && ( if (val && !customTimeFormats.includes(val)) {
<> const next = [...customTimeFormats, val];
<option value="YYYY-MM-DD">YYYY-MM-DD</option> setCustomTimeFormats(next);
<option value="YYYY年MM月DD日">YYYY年MM月DD日</option> storage.set('customTimeFormats', next);
</> }
)} }}
{field.type === 'time' && ( onKeyDown={(e) => {
<> if (e.key === 'Enter') {
<option value="24h">24</option> const val = (e.target as HTMLInputElement).value.trim();
<option value="12h">12</option> if (val && !customTimeFormats.includes(val)) {
</> const next = [...customTimeFormats, val];
)} setCustomTimeFormats(next);
</select> storage.set('customTimeFormats', next);
}
}
}}
className="w-full px-1.5 py-1 text-xs border border-border rounded"
placeholder="输入格式,如 YYYY-MM-DD"
/>
<datalist id={`edit-format-list-${field.key}`}>
{customTimeFormats.map(fmt => <option key={fmt} value={fmt} />)}
</datalist>
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
@@ -1048,27 +1077,45 @@ export default function TemplateManage() {
onChange={(e) => setNewFieldTimeDefault(e.target.value as 'current' | 'specific')} onChange={(e) => setNewFieldTimeDefault(e.target.value as 'current' | 'specific')}
className="w-full px-2 py-1.5 text-xs border border-border rounded bg-white" className="w-full px-2 py-1.5 text-xs border border-border rounded bg-white"
> >
<option value="specific"></option> <option value="specific"></option>
<option value="current"></option> <option value="current"></option>
</select> </select>
<select {newFieldTimeDefault === 'specific' && (
<input
type={newFieldForm.type === 'date' ? 'date' : 'time'}
value={newFieldFixedTimeValue}
onChange={(e) => setNewFieldFixedTimeValue(e.target.value)}
className="w-full px-2 py-1.5 text-xs border border-border rounded"
/>
)}
<input
list="new-format-list"
value={newFieldTimeFormat} value={newFieldTimeFormat}
onChange={(e) => setNewFieldTimeFormat(e.target.value)} onChange={(e) => setNewFieldTimeFormat(e.target.value)}
className="w-full px-2 py-1.5 text-xs border border-border rounded bg-white" onBlur={(e) => {
> const val = e.target.value.trim();
{newFieldForm.type === 'date' && ( if (val && !customTimeFormats.includes(val)) {
<> const next = [...customTimeFormats, val];
<option value="YYYY-MM-DD">YYYY-MM-DD</option> setCustomTimeFormats(next);
<option value="YYYY年MM月DD日">YYYY年MM月DD日</option> storage.set('customTimeFormats', next);
</> }
)} }}
{newFieldForm.type === 'time' && ( onKeyDown={(e) => {
<> if (e.key === 'Enter') {
<option value="24h">24</option> const val = (e.target as HTMLInputElement).value.trim();
<option value="12h">12</option> if (val && !customTimeFormats.includes(val)) {
</> const next = [...customTimeFormats, val];
)} setCustomTimeFormats(next);
</select> storage.set('customTimeFormats', next);
}
}
}}
className="w-full px-2 py-1.5 text-xs border border-border rounded"
placeholder="输入格式,如 YYYY-MM-DD"
/>
<datalist id="new-format-list">
{customTimeFormats.map(fmt => <option key={fmt} value={fmt} />)}
</datalist>
</div> </div>
)} )}
{['单选', '多选'].includes(newFieldForm.category) && ( {['单选', '多选'].includes(newFieldForm.category) && (

View File

@@ -115,6 +115,7 @@ export interface FormField {
options?: string[]; options?: string[];
timeFormat?: string; timeFormat?: string;
timeDefault?: 'current' | 'specific'; timeDefault?: 'current' | 'specific';
fixedTimeValue?: string;
} }
export const DEFAULT_FORM_FIELDS: FormField[] = [ export const DEFAULT_FORM_FIELDS: FormField[] = [
@@ -125,13 +126,13 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
{ key: 'patientAge', label: '患者年龄', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false }, { key: 'patientAge', label: '患者年龄', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
{ key: 'department', 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: 'bedNumber', label: '床号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
{ key: 'surgeryDate', label: '手术日期', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: false, timeFormat: 'YYYY-MM-DD', timeDefault: 'specific' }, { key: 'surgeryDate', label: '手术日期', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: true, timeFormat: 'YYYY-MM-DD', timeDefault: 'specific' },
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false, timeFormat: '24h', timeDefault: 'specific' }, { key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false, timeFormat: '24h', timeDefault: 'specific' }, { key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'reportDate', label: '撰写时间', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: true, timeFormat: 'YYYY年MM月DD日', timeDefault: 'current' }, { key: 'reportDate', label: '撰写时间', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: true, timeFormat: 'YYYY年MM月DD日', timeDefault: 'current' },
{ key: 'surgeon', label: '手术者', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['张医生', '李医生', '王医生'] }, { key: 'surgeon', label: '手术者', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: true, options: ['张医生', '李医生', '王医生'] },
{ key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['赵医生', '钱医生', '孙医生'] }, { key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: true, options: ['赵医生', '钱医生', '孙医生'] },
{ key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['周医生', '吴医生', '郑医生'] }, { key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: true, options: ['周医生', '吴医生', '郑医生'] },
{ key: 'anesthesiaType', label: '麻醉方式', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉'] }, { key: 'anesthesiaType', label: '麻醉方式', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉'] },
{ key: 'preoperativeDiagnosis', label: '术前诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] }, { key: 'preoperativeDiagnosis', label: '术前诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] },
{ key: 'postoperativeDiagnosis', label: '术后诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] }, { key: 'postoperativeDiagnosis', label: '术后诊断', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: true, options: ['胆囊结石伴慢性胆囊炎', '急性胆囊炎'] },

View File

@@ -155,7 +155,7 @@ export const defaultReportContent = `
</p> </p>
<p style="text-align: right; font-family: SimSun;"> <p style="text-align: right; font-family: SimSun;">
撰写时间:${smartField('reportDate')} ${smartField('reportDate')}
</p> </p>
</div> </div>
`; `;