2026-04-17-21-32-27 - 时间日期字段格式配置与撰写时间动态字段
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: ['周医生', '吴医生', '郑医生'] },
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
210
工程分析/实现方案-2026-04-17-21-32-27.md
Normal file
210
工程分析/实现方案-2026-04-17-21-32-27.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 实现方案 — 2026-04-17-21-32-27
|
||||
|
||||
## 根因分析
|
||||
|
||||
当前系统的时间/日期字段为「硬编码」形态:
|
||||
- `date` 类型固定使用浏览器原生 `<input type="date">`,smart field 中直接显示原始值。
|
||||
- `time` 类型仅对 `startTime/endTime` 有表单渲染(hour+minute select),且固定为 24 小时制;smart field 中直接拼接 `HH:MM`。
|
||||
- 没有「当前时间自动填充」机制,也没有「显示格式切换」能力。
|
||||
- 模板底部「年 月 日」是写死文本,无法自动关联系统时间。
|
||||
|
||||
## 修改文件清单
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/types.ts` | `FormField` 增加 `timeFormat`/`timeDefault`;更新 `DEFAULT_FORM_FIELDS`;新增 `reportDate` |
|
||||
| `src/utils/defaultContent.ts` | 底部「年 月 日」→「撰写时间:${smartField('reportDate')}」 |
|
||||
| `src/pages/TemplateManage.tsx` | 新增字段/编辑面板增加时间配置 UI;保存逻辑扩展 |
|
||||
| `src/pages/ReportEditor.tsx` | date/time 表单渲染增强;smart field 同步增加格式转换;初始化自动填充 |
|
||||
|
||||
## 具体代码变更
|
||||
|
||||
### 1. types.ts
|
||||
|
||||
```ts
|
||||
export interface FormField {
|
||||
key: string;
|
||||
label: string;
|
||||
category: string;
|
||||
type: FieldType;
|
||||
visibleInForm: boolean;
|
||||
isSystemLocked: boolean;
|
||||
options?: string[];
|
||||
timeFormat?: string; // NEW
|
||||
timeDefault?: 'current' | 'specific'; // NEW
|
||||
}
|
||||
```
|
||||
|
||||
`DEFAULT_FORM_FIELDS` 更新:
|
||||
- `surgeryDate` 增加 `timeFormat: 'YYYY-MM-DD', timeDefault: 'specific'`
|
||||
- `startTime` 增加 `timeFormat: '24h', timeDefault: 'specific'`
|
||||
- `endTime` 增加 `timeFormat: '24h', timeDefault: 'specific'`
|
||||
- 新增:`reportDate`(date, `YYYY年MM月DD日`, `current`, systemLocked)
|
||||
|
||||
### 2. defaultContent.ts
|
||||
|
||||
尾部修改:
|
||||
```html
|
||||
<!-- 删除旧的 "年 月 日" 段落 -->
|
||||
<p style="text-align: right; font-family: SimSun;">
|
||||
撰写时间:${smartField('reportDate')}
|
||||
</p>
|
||||
```
|
||||
|
||||
### 3. TemplateManage.tsx
|
||||
|
||||
#### 新增状态
|
||||
```ts
|
||||
const [newFieldTimeFormat, setNewFieldTimeFormat] = useState('YYYY-MM-DD');
|
||||
const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
||||
const [editFieldTimeFormat, setEditFieldTimeFormat] = useState('');
|
||||
const [editFieldTimeDefault, setEditFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
||||
```
|
||||
|
||||
#### 点击字段进入编辑时
|
||||
```ts
|
||||
setEditFieldTimeFormat(field.timeFormat || '');
|
||||
setEditFieldTimeDefault(field.timeDefault || 'specific');
|
||||
```
|
||||
|
||||
#### 编辑面板(editingFieldKey === field.key)
|
||||
在「选项输入框」之后,增加条件渲染:
|
||||
```tsx
|
||||
{field.category === '时间' && (
|
||||
<div className="space-y-1">
|
||||
<select value={editFieldTimeDefault} onChange={...}>
|
||||
<option value="specific">手动选择</option>
|
||||
<option value="current">当前时间</option>
|
||||
</select>
|
||||
<select value={editFieldTimeFormat} onChange={...}>
|
||||
{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>
|
||||
)}
|
||||
```
|
||||
|
||||
#### saveFieldEdit
|
||||
```ts
|
||||
if (field.category === '时间') {
|
||||
next.timeFormat = editFieldTimeFormat;
|
||||
next.timeDefault = editFieldTimeDefault;
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增字段表单
|
||||
在 category === '时间' 条件下,增加「默认值」和「显示格式」两个 select。
|
||||
|
||||
#### addField
|
||||
```ts
|
||||
if (newFieldForm.category === '时间') {
|
||||
newField.timeFormat = newFieldTimeFormat;
|
||||
newField.timeDefault = newFieldTimeDefault;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. ReportEditor.tsx
|
||||
|
||||
#### 新增辅助函数(组件内)
|
||||
```ts
|
||||
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;
|
||||
};
|
||||
```
|
||||
|
||||
#### date 字段表单渲染
|
||||
保持 `<input type="date">` 不变,值仍为 `YYYY-MM-DD`。
|
||||
|
||||
#### time 字段表单渲染
|
||||
重构为支持通用 time 字段:
|
||||
|
||||
**startTime/endTime(向后兼容)**:
|
||||
- `timeFormat === '24h'`:保持现有 hour(00-23) + minute select
|
||||
- `timeFormat === '12h'`:hour(01-12) + minute + AM/PM select
|
||||
- 存储转换:`to24h(hour12, isPM)` → 写入 startHour/endHour
|
||||
|
||||
**通用 time 字段(非 startTime/endTime)**:
|
||||
- 解析 reportData[field.key](格式 `HH:MM`)→ hour + minute
|
||||
- `timeFormat === '24h'`:hour(00-23) + minute
|
||||
- `timeFormat === '12h'`:hour(01-12) + minute + AM/PM
|
||||
- onChange 时拼接为 `HH:MM` 存入 reportData[field.key]
|
||||
|
||||
#### smart field 同步(useEffect)
|
||||
在拼接/取值后,增加格式转换:
|
||||
```ts
|
||||
if (fieldKey === 'startTime' || fieldKey === 'endTime') {
|
||||
// ... 拼接 HH:MM
|
||||
const fieldDef = formFields.find(f => f.key === fieldKey);
|
||||
newValue = formatTimeDisplay(newValue, fieldDef?.timeFormat);
|
||||
} else {
|
||||
const rawValue = (reportData as any)[fieldKey];
|
||||
// ... 处理 array/string
|
||||
const fieldDef = formFields.find(f => f.key === fieldKey);
|
||||
if (fieldDef?.type === 'date') {
|
||||
newValue = formatDateDisplay(newValue, fieldDef.timeFormat);
|
||||
} else if (fieldDef?.type === 'time') {
|
||||
newValue = formatTimeDisplay(newValue, fieldDef.timeFormat);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 初始化自动填充
|
||||
在 `useEffect` 初始化数据后,遍历 `formFields`:
|
||||
```ts
|
||||
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]) {
|
||||
setReportData(prev => ({ ...prev, [field.key]: current }));
|
||||
}
|
||||
} 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');
|
||||
const current = `${hh}:${mm}`;
|
||||
if (field.key === 'startTime') {
|
||||
if (!reportData.startHour) setReportData(prev => ({ ...prev, startHour: hh, startMinute: mm }));
|
||||
} else if (field.key === 'endTime') {
|
||||
if (!reportData.endHour) setReportData(prev => ({ ...prev, endHour: hh, endMinute: mm }));
|
||||
} else {
|
||||
if (!(reportData as any)[field.key]) {
|
||||
setReportData(prev => ({ ...prev, [field.key]: current }));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 风险点与应对措施
|
||||
|
||||
| 风险 | 应对措施 |
|
||||
|------|---------|
|
||||
| 现有用户已保存的 `formFieldsConfig` 缺少新字段,导致 `timeFormat` 为 undefined | 代码中统一使用 `field.timeFormat || 默认值` 做回退 |
|
||||
| 12h 表单与 24h 存储转换出错 | 增加边界单元测试(12AM→00, 12PM→12, 1PM→13 等) |
|
||||
| startTime/endTime 的 hour/minute 存储结构改动影响历史报告 | 保持存储结构不变,仅改动渲染和显示 |
|
||||
| 自动填充当前时间在编辑已有报告时覆盖用户值 | 仅当字段值为空时才填充 |
|
||||
|
||||
## 回滚策略
|
||||
|
||||
- `types.ts` 中新增的属性为 optional,回滚时删除即可,不影响已有数据结构。
|
||||
- `defaultContent.ts` 的修改可通过 Git revert 恢复。
|
||||
- TemplateManage/ReportEditor 的 UI 改动为增量添加,回滚时移除条件渲染块即可。
|
||||
96
工程分析/测试方案-2026-04-17-21-32-27.md
Normal file
96
工程分析/测试方案-2026-04-17-21-32-27.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 测试方案 — 2026-04-17-21-32-27
|
||||
|
||||
## 测试目标
|
||||
|
||||
验证时间/日期字段的格式配置、默认值策略、以及模板底部「撰写时间」动态字段的正确性。
|
||||
|
||||
## 测试环境
|
||||
|
||||
- 本地开发服务器:`npm run dev`(端口 3000)
|
||||
- 浏览器:Chrome/Edge
|
||||
- 测试账号:admin / 123456(超级管理员)
|
||||
|
||||
## 测试用例
|
||||
|
||||
### TC-1:TemplateManage 新增时间字段配置
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 登录 admin,进入「模板管理」 |
|
||||
| 2 | 点击「新增字段」,category 选「时间」,type 选「日期」 | 下方出现「默认值」select(手动选择/当前时间)和「显示格式」select(YYYY-MM-DD / YYYY年MM月DD日) |
|
||||
| 3 | 默认值选「当前时间」,格式选「YYYY年MM月DD日」,填写标签「出院日期」,点击「添加字段」 | 字段列表中出现「出院日期」,category 显示「时间 · date」 |
|
||||
| 4 | 新增字段 category 选「时间」,type 选「时分」 | 显示格式 select 出现「24小时制 / 12小时制」 |
|
||||
|
||||
### TC-2:TemplateManage 编辑已有时间字段配置
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 在字段列表中点击「手术日期」 | 进入编辑模式 |
|
||||
| 2 | 修改显示格式为「YYYY年MM月DD日」,保存 | 字段信息更新 |
|
||||
| 3 | 点击「手术开始时间」 | 编辑模式中出现 24h/12h 选项 |
|
||||
|
||||
### TC-3:ReportEditor 日期格式同步到富文本
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 新建报告,加载默认模板 | 基本信息中出现「撰写时间」字段 |
|
||||
| 2 | 在 TemplateManage 中将「手术日期」格式设为「YYYY年MM月DD日」 | — |
|
||||
| 3 | 回到 ReportEditor,手术日期选「2026-04-17」 | 编辑器中「手术日期」smart field 显示为「2026年04月17日」 |
|
||||
| 4 | 将格式改回「YYYY-MM-DD」 | 编辑器中显示为「2026-04-17」 |
|
||||
|
||||
### TC-4:ReportEditor 时间 12h/24h 格式
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 在 TemplateManage 中将「手术开始时间」格式设为「12小时制」 |
|
||||
| 2 | 在 ReportEditor 中选择 09:30 AM | 编辑器中显示「09:30 上午」 |
|
||||
| 3 | 切换为 02:30 PM | 编辑器中显示「02:30 下午」;reportData.startHour = "14" |
|
||||
| 4 | 将格式改回「24小时制」 | 表单变为 hour(00-23)+minute;编辑器显示「14:30」 |
|
||||
|
||||
### TC-5:自动填充当前时间
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 确保「撰写时间」的 timeDefault 为「当前时间」 | — |
|
||||
| 2 | 新建报告 | 「撰写时间」字段自动填充为当天日期(如 2026-04-17) |
|
||||
| 3 | 确保「手术开始时间」的 timeDefault 为「当前时间」 | — |
|
||||
| 4 | 新建报告 | 「手术开始时间」自动填充为当前时分 |
|
||||
| 5 | 编辑已有报告(已有值的报告) | 已有值不被覆盖 |
|
||||
|
||||
### TC-6:模板底部「撰写时间」
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 新建报告,加载默认模板 | 模板底部出现「撰写时间:2026年04月17日」(或当天日期) |
|
||||
| 2 | 在基本信息中修改「撰写时间」 | 编辑器底部同步更新 |
|
||||
| 3 | 预览/打印报告 | 底部显示正确的撰写时间 |
|
||||
|
||||
### TC-7:通用 time 字段(非 startTime/endTime)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 在 TemplateManage 新增一个 time 字段「麻醉开始时间」 |
|
||||
| 2 | 在 ReportEditor 中新建报告 | 基本信息中出现「麻醉开始时间」,可正常选择时分 |
|
||||
| 3 | 选择 08:15 | 编辑器中对应 smart field 显示「08:15」(24h)或「08:15 上午」(12h) |
|
||||
|
||||
### TC-8:向后兼容
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|---------|
|
||||
| 1 | 清除 localStorage 中 `formFieldsConfig`,重新登录 | 系统加载默认字段,所有时间字段正常工作 |
|
||||
| 2 | 不配置 timeFormat/timeDefault 的自定义字段 | 按默认行为工作(date 显示 YYYY-MM-DD,time 显示 24h) |
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `npm run lint` 无 TypeScript 编译错误
|
||||
- [ ] TemplateManage 中时间字段可正常新增、编辑、保存配置
|
||||
- [ ] ReportEditor 中 date 字段可根据格式正确显示在富文本中
|
||||
- [ ] ReportEditor 中 time 字段 12h/24h 切换正常,存储值正确
|
||||
- [ ] 自动填充当前时间仅在值为空时触发
|
||||
- [ ] 模板底部「撰写时间」动态显示且可编辑
|
||||
- [ ] 通用 time 字段有表单渲染并能正确同步到富文本
|
||||
- [ ] 现有报告和历史数据不受本次改动影响
|
||||
|
||||
## 测试方式
|
||||
|
||||
全部使用手工功能验证(项目无单元测试框架)。
|
||||
43
工程分析/需求分析-2026-04-17-21-32-27.md
Normal file
43
工程分析/需求分析-2026-04-17-21-32-27.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 需求分析 — 2026-04-17-21-32-27
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
1. TemplateManage 字段管理中,时间/日期字段增加配置选项:
|
||||
- date/time 均可选择默认值策略:「当前时间」或「手动选择(特定时间)」
|
||||
- date 可选择显示格式:`YYYY-MM-DD` 或 `YYYY年MM月DD日`
|
||||
- time 可选择显示格式:24小时制 或 12小时制(AM/PM)
|
||||
2. 默认模板底部写死的「年 月 日」改为动态「撰写时间」智能字段,自动取当前日期。
|
||||
|
||||
## 需求拆解
|
||||
|
||||
### 功能点
|
||||
|
||||
| # | 功能点 | 说明 |
|
||||
|---|--------|------|
|
||||
| 1 | 扩展 `FormField` 数据结构 | 新增 `timeFormat?: string` 和 `timeDefault?: 'current' \| 'specific'` |
|
||||
| 2 | TemplateManage 新增字段表单增强 | 当 category === '时间' 时,显示「默认值」和「显示格式」两个配置项 |
|
||||
| 3 | TemplateManage 字段编辑面板增强 | 已有时间字段点击编辑时,可修改默认值策略和显示格式 |
|
||||
| 4 | 默认字段配置更新 | `surgeryDate/startTime/endTime` 加上合理的默认配置;新增 `reportDate` 字段 |
|
||||
| 5 | 默认模板底部「撰写时间」 | `defaultContent.ts` 底部静态文本替换为 `${smartField('reportDate')}` |
|
||||
| 6 | ReportEditor date 字段格式同步 | smart field 同步时根据 `timeFormat` 转换日期显示格式 |
|
||||
| 7 | ReportEditor time 字段格式同步 | smart field 同步时根据 `timeFormat` 转换时间显示格式(12h/24h) |
|
||||
| 8 | ReportEditor time 字段 12h 表单渲染 | time 字段表单增加 AM/PM 选择,与 hour/minute 联动 |
|
||||
| 9 | ReportEditor 自动填充当前时间 | 组件初始化时,对 `timeDefault === 'current'` 且值为空的字段自动填充 |
|
||||
| 10 | 通用 time 字段表单渲染 | 非 `startTime/endTime` 的 time 字段新增 hour+minute select 渲染 |
|
||||
|
||||
### 非功能点
|
||||
- 向后兼容:未配置 `timeFormat/timeDefault` 的现有字段按原有行为工作
|
||||
- 最小侵入:不改动现有数据存储结构(date 仍存 `YYYY-MM-DD`,time 仍存 `HH:MM` 或 `startHour:startMinute`)
|
||||
|
||||
## 影响范围预估
|
||||
|
||||
| 模块 | 影响程度 | 说明 |
|
||||
|------|---------|------|
|
||||
| `src/types.ts` | 高 | `FormField` 接口扩展 + `DEFAULT_FORM_FIELDS` 新增/更新 |
|
||||
| `src/utils/defaultContent.ts` | 中 | 底部静态文本替换为 smartField |
|
||||
| `src/pages/TemplateManage.tsx` | 高 | 新增字段表单、字段编辑面板、保存逻辑均需改动 |
|
||||
| `src/pages/ReportEditor.tsx` | 高 | date/time 表单渲染、smart field 同步、初始化自动填充 |
|
||||
|
||||
## 待确认问题
|
||||
|
||||
无(用户已明确需求)。
|
||||
35
过往经验/经验记录.md
35
过往经验/经验记录.md
@@ -437,3 +437,38 @@ if ((settings.autoInsertDelay || 0) > 0) {
|
||||
- 任何 "实时解析输入" 的逻辑都必须警惕 `filter(Boolean)` 对空字符串的过滤效应——如果允许用户输入分隔符,应使用独立状态缓存原始输入,仅在确认时(blur/enter)执行解析。
|
||||
- `StrReplaceFile` 的批量替换若返回 "Applied N edit(s) with M total replacement(s)" 且 M < N,应立即检查未匹配的文件,避免遗漏。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 记录 15:时间/日期字段格式配置与撰写时间动态字段
|
||||
|
||||
**A. 具体问题**
|
||||
用户提出 2 个需求:
|
||||
1. TemplateManage 字段管理中,时间/日期字段增加配置:date 可选 `YYYY-MM-DD` / `YYYY年MM月DD日` 显示格式;time 可选 24h / 12h 显示格式;两者均可选「当前时间」或「手动选择」作为默认值策略。
|
||||
2. 默认模板底部写死的「年 月 日」改为动态「撰写时间」智能字段,自动取当前日期。
|
||||
|
||||
**B. 产生问题原因**
|
||||
1. `FormField` 数据结构缺少格式和默认值配置字段。
|
||||
2. `ReportEditor` 中 time 字段的表单渲染仅支持 `startTime/endTime` 且固定为 24 小时制;smart field 同步时直接显示原始值,不做任何格式转换。
|
||||
3. 模板底部「年 月 日」是纯静态 HTML 文本,没有数据绑定能力。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. **扩展数据结构**:`FormField` 增加 `timeFormat?: string` 和 `timeDefault?: 'current' | 'specific'`。现有字段补充默认值(`surgeryDate` → `YYYY-MM-DD`+`specific`,`startTime/endTime` → `24h`+`specific`);新增系统字段 `reportDate`(`YYYY年MM月DD日`+`current`)。
|
||||
2. **TemplateManage UI 增强**:
|
||||
- 新增字段表单:category 为「时间」时显示「默认值」select(手动选择/当前时间)和「显示格式」select(date 提供两种日期格式,time 提供 24h/12h)。
|
||||
- 字段编辑面板:点击已有时间字段进入编辑模式时,可修改上述两项配置。
|
||||
3. **ReportEditor 自动填充**:新增 `useEffect` 监听 `formFields`,对 `timeDefault === 'current'` 且值为空的字段,自动填充系统当前日期/时间。
|
||||
4. **ReportEditor 表单渲染重构**:
|
||||
- `startTime/endTime`:根据 `timeFormat` 选择 hour select 的选项范围(24h: 00-23,12h: 01-12),12h 时额外增加 AM/PM select。存储仍保持 24h(`startHour/startMinute`),转换函数 `to24h`/`from24h` 处理 12h↔24h。
|
||||
- 通用 time 字段(非 startTime/endTime):新增 hour+minute select 渲染,值统一存储为 `HH:MM` 字符串。
|
||||
5. **smart field 同步格式化**:同步 useEffect 中,根据字段定义调用 `formatDateDisplay`/`formatTimeDisplay`,将原始值转换为配置格式后写入编辑器。
|
||||
6. **编辑器反向编辑解析**:`handleEditorInput` 中,当用户直接在编辑器内修改 date/time smart field 时,通过正则解析格式化文本(如 `2026年04月17日` → `2026-04-17`、`02:30 下午` → `14:30`),转回原始值后存入 `reportData`。
|
||||
7. **默认模板更新**:`defaultContent.ts` 底部静态「年 月 日」替换为 `${smartField('reportDate')}`。
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 当为字段增加新的配置属性时,务必在 `DEFAULT_FORM_FIELDS` 中为所有已有字段提供合理的默认值,保证向后兼容。
|
||||
- 显示格式与存储格式分离时,必须同时实现「正向格式化」(存储→显示)和「反向解析」(显示→存储),否则用户在编辑器中直接编辑格式化后的值会导致数据格式混乱。
|
||||
- 12h/24h 转换要覆盖所有边界情况:12AM→00、12PM→12、1PM→13,建议用独立纯函数(`to24h`/`from24h`)集中处理,避免在 JSX 中内联复杂计算。
|
||||
- 自动填充当前时间必须增加「仅当值为空时触发」的保护,防止编辑已有报告时覆盖用户数据。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user