import { useState, useMemo } from 'react'; import { Box, Text, Group, ActionIcon, } from '@mantine/core'; import { IconChevronLeft, IconChevronRight, } from '@tabler/icons-react'; import { Lunar } from 'lunar-javascript'; interface FixedCalendarProps { value: Date | null; onChange: (date: Date | null) => void; minDate?: Date; maxDate?: Date; } // 获取某月的第一天是星期几 function getFirstDayOfMonth(date: Date): number { return new Date(date.getFullYear(), date.getMonth(), 1).getDay(); } // 获取某月有多少天 function getDaysInMonth(date: Date): number { return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); } // 检查两个日期是否是同一天 function isSameDay(d1: Date | null, d2: Date | null): boolean { if (!d1 || !d2) return false; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } // 检查日期是否在范围内 function isDateInRange(date: Date, min?: Date, max?: Date): boolean { if (min && date < min) return false; if (max && date > max) return false; return true; } const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六']; const MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']; // 获取农历日期简写(用于日历显示) function getLunarDayText(date: Date): string { try { const lunar = Lunar.fromYmd(date.getFullYear(), date.getMonth() + 1, date.getDate()); const day = lunar.getDay(); // 初一显示为"初一",其他日期只显示日期数字 if (day === 1) { return lunar.getMonthInChinese(); } // 初一、十五、廿五等特殊日子 const specialDays: Record = { 1: '初一', 15: '十五', 20: '廿', 25: '廿五', 30: '卅', }; if (specialDays[day]) { return specialDays[day]; } return lunar.getDayInChinese(); } catch { return ''; } } export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalendarProps) { const [currentMonth, setCurrentMonth] = useState(value || new Date()); // 生成日历数据(固定6行7列 = 42天) const calendarDays = useMemo(() => { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const firstDay = getFirstDayOfMonth(new Date(year, month, 1)); const daysInMonth = getDaysInMonth(new Date(year, month, 1)); const days: Array<{ date: Date; isCurrentMonth: boolean; isToday: boolean; isSelected: boolean; isDisabled: boolean; lunarText?: string; // 农历日期简写 }> = []; // 上月剩余的天数 const prevMonthDays = getDaysInMonth(new Date(year, month - 1, 1)); for (let i = firstDay - 1; i >= 0; i--) { const date = new Date(year, month - 1, prevMonthDays - i); const lunarText = getLunarDayText(date); days.push({ date, isCurrentMonth: false, isToday: isSameDay(date, new Date()), isSelected: isSameDay(date, value), isDisabled: !isDateInRange(date, minDate, maxDate), lunarText, }); } // 当月的天数 for (let i = 1; i <= daysInMonth; i++) { const date = new Date(year, month, i); const lunarText = getLunarDayText(date); days.push({ date, isCurrentMonth: true, isToday: isSameDay(date, new Date()), isSelected: isSameDay(date, value), isDisabled: !isDateInRange(date, minDate, maxDate), lunarText, }); } // 下月需要填充的天数 const remainingDays = 42 - days.length; for (let i = 1; i <= remainingDays; i++) { const date = new Date(year, month + 1, i); const lunarText = getLunarDayText(date); days.push({ date, isCurrentMonth: false, isToday: isSameDay(date, new Date()), isSelected: isSameDay(date, value), isDisabled: !isDateInRange(date, minDate, maxDate), lunarText, }); } return days; }, [currentMonth, value, minDate, maxDate]); const handlePrevMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)); }; const handleNextMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)); }; const handleDateClick = (date: Date, isDisabled: boolean) => { if (!isDisabled) { onChange(date); } }; return ( {/* 头部:月份选择 */} {MONTH_NAMES[currentMonth.getMonth()]} {currentMonth.getFullYear()} {/* 星期行 */} {WEEKDAYS.map((day) => ( {day} ))} {/* 日期网格 - 固定6行7列 */} {Array.from({ length: 6 }).map((_, weekIndex) => ( {Array.from({ length: 7 }).map((_, dayIndex) => { const dayIndex_ = weekIndex * 7 + dayIndex; const day = calendarDays[dayIndex_]; if (!day) return null; return ( handleDateClick(day.date, day.isDisabled)} style={{ flex: 1, aspectRatio: '1', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontSize: 13, cursor: day.isDisabled ? 'not-allowed' : 'pointer', borderRadius: 8, background: day.isSelected ? '#007AFF' : day.isToday ? 'rgba(0, 122, 255, 0.12)' : 'transparent', color: day.isCurrentMonth ? day.isDisabled ? '#d1d5db' : day.isSelected ? '#fff' : '#374151' : day.isDisabled ? '#e5e7eb' : '#9ca3af', fontWeight: day.isSelected ? 600 : 500, transition: 'all 0.15s ease', }} onMouseEnter={(e) => { if (!day.isDisabled && !day.isSelected) { e.currentTarget.style.background = 'rgba(0, 122, 255, 0.06)'; } }} onMouseLeave={(e) => { if (!day.isDisabled && !day.isSelected) { e.currentTarget.style.background = 'transparent'; } }} > {day.date.getDate()} {day.lunarText && day.isCurrentMonth && !day.isDisabled && ( {day.lunarText} )} ); })} ))} ); }