2026-04-17-21-32-27 - 时间日期字段格式配置与撰写时间动态字段

This commit is contained in:
2026-04-17 21:42:51 +08:00
parent 8f746c25f3
commit 8beb534abb
8 changed files with 703 additions and 17 deletions

View File

@@ -254,6 +254,52 @@ export default function ReportEditor() {
};
}, [saveDraftToStorage]);
// Auto-fill current time for fields with timeDefault === 'current'
useEffect(() => {
if (formFields.length === 0) return;
let hasChange = false;
const updates: any = {};
formFields.forEach(field => {
if (field.timeDefault !== 'current') return;
if (field.type === 'date') {
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]) {
updates[field.key] = `${hh}:${mm}`;
hasChange = true;
}
}
}
});
if (hasChange) {
setReportData(prev => {
const next = { ...prev, ...updates };
stateRef.current = { ...stateRef.current, reportData: next };
return next;
});
}
}, [formFields]);
useEffect(() => {
if (!editorRef.current) return;
const observer = new MutationObserver(() => {
@@ -823,8 +869,44 @@ export default function ReportEditor() {
}, []);
const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
const hour12Options = Array.from({ length: 12 }, (_, i) => ((i + 1).toString().padStart(2, '0')));
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
const formatDateDisplay = (isoDate: string, fmt?: string): string => {
if (!isoDate) return '';
if (fmt === 'YYYY年MM月DD日') {
const [y, m, d] = isoDate.split('-');
if (y && m && d) return `${y}${m}${d}`;
}
return isoDate;
};
const formatTimeDisplay = (timeStr: string, fmt?: string): string => {
if (!timeStr) return '';
if (fmt === '12h') {
const [hStr, mStr] = timeStr.split(':');
let h = parseInt(hStr);
const ampm = h >= 12 ? '下午' : '上午';
h = h % 12;
if (h === 0) h = 12;
return `${String(h).padStart(2, '0')}:${mStr} ${ampm}`;
}
return timeStr;
};
const to24h = (h12: number, isPM: boolean): number => {
if (isPM && h12 !== 12) return h12 + 12;
if (!isPM && h12 === 12) return 0;
return h12;
};
const from24h = (h24: number): { h: number; isPM: boolean } => {
const isPM = h24 >= 12;
let h = h24 % 12;
if (h === 0) h = 12;
return { h, isPM };
};
const addTag = (field: string, value: string) => {
const current = (reportData as any)[field] || [];
if (!current.includes(value)) {
@@ -959,23 +1041,60 @@ export default function ReportEditor() {
const fieldKey = target.getAttribute('data-bind')!;
const newValue = target.innerText;
const fieldDef = formFields.find(f => f.key === fieldKey);
if (fieldKey === 'startTime') {
const parts = newValue.split(':');
let raw = newValue;
if (fieldDef?.timeFormat === '12h') {
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]}`;
}
}
const parts = raw.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(':');
let raw = newValue;
if (fieldDef?.timeFormat === '12h') {
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]}`;
}
}
const parts = raw.split(':');
setReportData((prev) => {
const next = { ...prev, endHour: parts[0] || '', endMinute: parts[1] || '' };
stateRef.current = { ...stateRef.current, reportData: next };
return next;
});
} else {
let raw = newValue;
if (fieldDef?.type === 'date' && fieldDef.timeFormat === 'YYYY年MM月DD日') {
const m = newValue.match(/(\d{4})年(\d{2})月(\d{2})日/);
if (m) raw = `${m[1]}-${m[2]}-${m[3]}`;
} else if (fieldDef?.type === 'time' && fieldDef.timeFormat === '12h') {
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) => {
const next = { ...prev, [fieldKey]: newValue };
const next = { ...prev, [fieldKey]: raw };
stateRef.current = { ...stateRef.current, reportData: next };
return next;
});
@@ -1012,12 +1131,15 @@ export default function ReportEditor() {
}
let newValue = '';
const fieldDef = formFields.find(f => f.key === fieldKey);
if (fieldKey === 'startTime') {
newValue = `${reportData.startHour || ''}:${reportData.startMinute || ''}`;
if (newValue === ':') newValue = '';
newValue = formatTimeDisplay(newValue, fieldDef?.timeFormat);
} else if (fieldKey === 'endTime') {
newValue = `${reportData.endHour || ''}:${reportData.endMinute || ''}`;
if (newValue === ':') newValue = '';
newValue = formatTimeDisplay(newValue, fieldDef?.timeFormat);
} else {
const rawValue = (reportData as any)[fieldKey];
if (Array.isArray(rawValue)) {
@@ -1025,6 +1147,11 @@ export default function ReportEditor() {
} else if (rawValue !== undefined && rawValue !== null) {
newValue = String(rawValue);
}
if (fieldDef?.type === 'date') {
newValue = formatDateDisplay(newValue, fieldDef.timeFormat);
} else if (fieldDef?.type === 'time') {
newValue = formatTimeDisplay(newValue, fieldDef.timeFormat);
}
}
if (el.innerText !== newValue) {
@@ -1400,29 +1527,119 @@ export default function ReportEditor() {
}
if (field.type === 'time') {
const hourKey = field.key === 'startTime' ? 'startHour' : 'endHour';
const minuteKey = field.key === 'startTime' ? 'startMinute' : 'endMinute';
const is12h = field.timeFormat === '12h';
if (field.key === 'startTime' || field.key === 'endTime') {
const hourKey = field.key === 'startTime' ? 'startHour' : 'endHour';
const minuteKey = field.key === 'startTime' ? 'startMinute' : 'endMinute';
const h24val = parseInt((reportData as any)[hourKey]) || 0;
const m = (reportData as any)[minuteKey] || '';
const { h: h12, isPM } = from24h(h24val);
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={is12h ? String(h12).padStart(2, '0') : ((reportData as any)[hourKey] || '')}
onChange={(e) => {
let h24new = parseInt(e.target.value) || 0;
if (is12h) {
const currentPM = from24h(parseInt((reportData as any)[hourKey]) || 0).isPM;
h24new = to24h(h24new, currentPM);
}
const next = { ...reportData, [hourKey]: String(h24new).padStart(2, '0') };
setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage();
}}
className="input-minimal bg-white flex-1"
>
<option value="">--</option>
{(is12h ? hour12Options : hourOptions).map(h => <option key={h} value={h}>{h}</option>)}
</select>
<span className="text-text-muted">:</span>
<select
value={m}
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(mo => <option key={mo} value={mo}>{mo}</option>)}
</select>
{is12h && (
<select
value={isPM ? '下午' : '上午'}
onChange={(e) => {
const isPMnew = e.target.value === '下午';
const currentH12 = from24h(parseInt((reportData as any)[hourKey]) || 0).h;
const h24new = to24h(currentH12, isPMnew);
const next = { ...reportData, [hourKey]: String(h24new).padStart(2, '0') };
setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage();
}}
className="input-minimal bg-white flex-1"
>
<option value="上午"></option>
<option value="下午"></option>
</select>
)}
</div>
</div>
);
}
// 通用 time 字段
const timeVal = (reportData as any)[field.key] || '';
const [h24str, mstr] = timeVal.split(':');
const h24 = parseInt(h24str) || 0;
const m = mstr || '';
const { h: h12g, isPM: isPMg } = from24h(h24);
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(); }}
value={is12h ? String(h12g).padStart(2, '0') : (h24str || '')}
onChange={(e) => {
let h24new = parseInt(e.target.value) || 0;
if (is12h) h24new = to24h(h24new, isPMg);
const nextVal = `${String(h24new).padStart(2, '0')}:${m || '00'}`;
const next = { ...reportData, [field.key]: nextVal };
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>)}
{(is12h ? hour12Options : 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(); }}
value={m}
onChange={(e) => {
const nextVal = `${String(h24).padStart(2, '0')}:${e.target.value}`;
const next = { ...reportData, [field.key]: nextVal };
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>)}
{minuteOptions.map(mo => <option key={mo} value={mo}>{mo}</option>)}
</select>
{is12h && (
<select
value={isPMg ? '下午' : '上午'}
onChange={(e) => {
const isPMnew = e.target.value === '下午';
const h24new = to24h(h12g, isPMnew);
const nextVal = `${String(h24new).padStart(2, '0')}:${m || '00'}`;
const next = { ...reportData, [field.key]: nextVal };
setReportData(next); stateRef.current = { ...stateRef.current, reportData: next }; saveDraftToStorage();
}}
className="input-minimal bg-white flex-1"
>
<option value="上午"></option>
<option value="下午"></option>
</select>
)}
</div>
</div>
);

View File

@@ -31,6 +31,10 @@ export default function TemplateManage() {
const [editingFieldKey, setEditingFieldKey] = useState<string | null>(null);
const [editFieldLabel, setEditFieldLabel] = useState('');
const [editFieldOptions, setEditFieldOptions] = useState('');
const [editFieldTimeFormat, setEditFieldTimeFormat] = useState('');
const [editFieldTimeDefault, setEditFieldTimeDefault] = useState<'current' | 'specific'>('specific');
const [newFieldTimeFormat, setNewFieldTimeFormat] = useState('YYYY-MM-DD');
const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific');
const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]);
const updatePageHeight = () => {
@@ -422,6 +426,10 @@ export default function TemplateManage() {
if (['单选', '多选', '图片'].includes(f.category)) {
next.options = editFieldOptions.split(/[,]/).map(s => s.trim()).filter(Boolean);
}
if (f.category === '时间') {
next.timeFormat = editFieldTimeFormat;
next.timeDefault = editFieldTimeDefault;
}
return next;
});
setFormFields(updated);
@@ -443,11 +451,17 @@ export default function TemplateManage() {
? newFieldOptions.split(/[,]/).map(s => s.trim()).filter(Boolean)
: undefined
};
if (newFieldForm.category === '时间') {
newField.timeFormat = newFieldTimeFormat;
newField.timeDefault = newFieldTimeDefault;
}
const updated = [...formFields, newField];
setFormFields(updated);
storage.set('formFieldsConfig', updated);
setNewFieldForm({ label: '', category: '填空', type: 'text' });
setNewFieldOptions('');
setNewFieldTimeFormat('YYYY-MM-DD');
setNewFieldTimeDefault('specific');
};
const handleAssetUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -858,6 +872,8 @@ export default function TemplateManage() {
setEditingFieldKey(field.key);
setEditFieldLabel(field.label);
setEditFieldOptions((field.options || []).join(', '));
setEditFieldTimeFormat(field.timeFormat || '');
setEditFieldTimeDefault(field.timeDefault || 'specific');
}}
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'}`}
>
@@ -883,6 +899,36 @@ export default function TemplateManage() {
placeholder="选项,用逗号分隔"
/>
)}
{field.category === '时间' && (
<div className="space-y-1">
<select
value={editFieldTimeDefault}
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"
>
<option value="specific"></option>
<option value="current"></option>
</select>
<select
value={editFieldTimeFormat}
onChange={(e) => setEditFieldTimeFormat(e.target.value)}
className="w-full px-1.5 py-1 text-xs border border-border rounded bg-white"
>
{field.type === 'date' && (
<>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
<option value="YYYY年MM月DD日">YYYY年MM月DD日</option>
</>
)}
{field.type === 'time' && (
<>
<option value="24h">24</option>
<option value="12h">12</option>
</>
)}
</select>
</div>
)}
<div className="flex gap-2">
<button
onClick={() => saveFieldEdit(field.key)}
@@ -980,7 +1026,13 @@ export default function TemplateManage() {
</select>
<select
value={newFieldForm.type}
onChange={(e) => setNewFieldForm({ ...newFieldForm, type: e.target.value as FieldType })}
onChange={(e) => {
const t = e.target.value as FieldType;
setNewFieldForm({ ...newFieldForm, type: t });
if (newFieldForm.category === '时间') {
setNewFieldTimeFormat(t === 'date' ? 'YYYY-MM-DD' : '24h');
}
}}
className="flex-1 px-2 py-1.5 text-xs border border-border rounded focus:outline-hidden focus:border-accent bg-white"
>
{newFieldForm.category === '填空' && <option value="text"></option>}
@@ -989,6 +1041,36 @@ export default function TemplateManage() {
{newFieldForm.category === '时间' && <><option value="date"></option><option value="time"></option></>}
</select>
</div>
{newFieldForm.category === '时间' && (
<div className="space-y-1">
<select
value={newFieldTimeDefault}
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"
>
<option value="specific"></option>
<option value="current"></option>
</select>
<select
value={newFieldTimeFormat}
onChange={(e) => setNewFieldTimeFormat(e.target.value)}
className="w-full px-2 py-1.5 text-xs border border-border rounded bg-white"
>
{newFieldForm.type === 'date' && (
<>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
<option value="YYYY年MM月DD日">YYYY年MM月DD日</option>
</>
)}
{newFieldForm.type === 'time' && (
<>
<option value="24h">24</option>
<option value="12h">12</option>
</>
)}
</select>
</div>
)}
{['单选', '多选'].includes(newFieldForm.category) && (
<input
type="text"

View File

@@ -113,6 +113,8 @@ export interface FormField {
visibleInForm: boolean;
isSystemLocked: boolean;
options?: string[];
timeFormat?: string;
timeDefault?: 'current' | 'specific';
}
export const DEFAULT_FORM_FIELDS: FormField[] = [
@@ -123,9 +125,10 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
{ 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: 'surgeryDate', label: '手术日期', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: false, timeFormat: 'YYYY-MM-DD', timeDefault: 'specific' },
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: false, timeFormat: '24h', timeDefault: 'specific' },
{ 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: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['赵医生', '钱医生', '孙医生'] },
{ key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['周医生', '吴医生', '郑医生'] },

View File

@@ -154,8 +154,8 @@ export const defaultReportContent = `
手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" 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>
</p>
<p style="text-align: right; font-family: SimSun; color: #bdbdbd;">
年 月 日
<p style="text-align: right; font-family: SimSun;">
撰写时间:${smartField('reportDate')}
</p>
</div>
`;