'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { AlertTriangle, CalendarDays, ChevronLeft, ChevronRight, Clock3, X } from 'lucide-react'; import { getChinaWorkdayInfo, getDateKey } from '@/lib/china-workday-calendar'; interface WorkDateTimePickerProps { value: string; onChange: (value: string) => void; placeholder?: string; className?: string; disabled?: boolean; popoverAlign?: 'left' | 'right'; defaultHour?: number; } const WEEKDAYS = ['一', '二', '三', '四', '五', '六', '日']; const HOURS = Array.from({ length: 24 }, (_, index) => index); const MINUTES = [0, 15, 30, 45]; export function WorkDateTimePicker({ value, onChange, placeholder = '选择日期时间', className = '', disabled = false, popoverAlign = 'left', defaultHour = 9, }: WorkDateTimePickerProps) { const ref = useRef(null); const [open, setOpen] = useState(false); const parts = parseLocalDateTime(value, defaultHour); const selectedDate = parts.dateKey; const selectedInfo = selectedDate ? getChinaWorkdayInfo(selectedDate) : null; const [viewDate, setViewDate] = useState(() => getInitialViewDate(value)); useEffect(() => { if (!open) return; const handler = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false); }; const keyHandler = (event: KeyboardEvent) => { if (event.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', handler); document.addEventListener('keydown', keyHandler); return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('keydown', keyHandler); }; }, [open]); useEffect(() => { if (value) setViewDate(getInitialViewDate(value)); }, [value]); const days = useMemo(() => buildCalendarDays(viewDate), [viewDate]); const displayText = value ? value.replace('T', ' ') : ''; const hasRestDayWarning = Boolean(selectedInfo && !selectedInfo.isWorkday); const updateDate = (dateKey: string) => { onChange(makeLocalDateTime(dateKey, parts.hour, parts.minute)); }; const updateTime = (nextHour: number, nextMinute: number) => { const dateKey = selectedDate || getDateKey(new Date()); onChange(makeLocalDateTime(dateKey, nextHour, nextMinute)); }; const clearValue = (event: React.MouseEvent) => { event.stopPropagation(); onChange(''); }; return (
{open && (
{viewDate.getFullYear()}年{viewDate.getMonth() + 1}月
{WEEKDAYS.map((day) => {day})}
{days.map((day) => { const info = getChinaWorkdayInfo(day.dateKey); const isSelected = day.dateKey === selectedDate; const isToday = day.dateKey === getDateKey(new Date()); const tone = getDayTone(info.type, day.inMonth, isSelected); return ( ); })}
{selectedInfo && (
{selectedInfo.isWorkday ? selectedInfo.type === 'makeup_workday' ? `${selectedInfo.label},按工作日处理` : '所选日期为工作日' : `所选日期为${selectedInfo.label},仍可保存`}
)}
)}
); } function parseLocalDateTime(value: string, defaultHour: number): { dateKey: string; hour: number; minute: number } { const matched = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/); if (!matched) return { dateKey: '', hour: defaultHour, minute: 0 }; return { dateKey: matched[1], hour: Number(matched[2]), minute: normalizeMinute(Number(matched[3])), }; } function normalizeMinute(minute: number): number { if (MINUTES.includes(minute)) return minute; return MINUTES.reduce((best, current) => Math.abs(current - minute) < Math.abs(best - minute) ? current : best, 0); } function makeLocalDateTime(dateKey: string, hour: number, minute: number): string { return `${dateKey}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; } function getInitialViewDate(value: string): Date { const dateKey = getDateKey(value || new Date()); const [year, month] = dateKey.split('-').map(Number); return new Date(year, month - 1, 1); } function addMonths(date: Date, count: number): Date { return new Date(date.getFullYear(), date.getMonth() + count, 1); } function buildCalendarDays(viewDate: Date): Array<{ dateKey: string; day: number; inMonth: boolean }> { const year = viewDate.getFullYear(); const month = viewDate.getMonth(); const first = new Date(year, month, 1); const mondayOffset = (first.getDay() + 6) % 7; const cursor = new Date(year, month, 1 - mondayOffset); const days = []; for (let index = 0; index < 42; index += 1) { days.push({ dateKey: getDateKey(cursor), day: cursor.getDate(), inMonth: cursor.getMonth() === month, }); cursor.setDate(cursor.getDate() + 1); } return days; } function getDayTone(type: string, inMonth: boolean, selected: boolean): string { if (selected) return 'bg-[var(--accent)] text-white shadow-sm'; if (!inMonth) return 'text-[var(--ink-muted)] opacity-35 hover:bg-[var(--bg-subtle)]'; if (type === 'holiday') return 'bg-red-50 text-red-600 hover:bg-red-100'; if (type === 'makeup_workday') return 'bg-blue-50 text-blue-600 hover:bg-blue-100'; if (type === 'weekend') return 'text-amber-600 hover:bg-amber-50'; return 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]'; }