import React, { useState, useEffect, useRef } from 'react'; import { useUIKit } from '@tencentcloud/uikit-base-component-react'; import classNames from 'classnames'; import styles from './DateRangePicker.module.scss'; interface DateRangePickerProps { value?: [Date | null, Date | null]; onChange?: (dates: [Date | null, Date | null]) => void; className?: string; } export function DateRangePicker({ value, onChange, className }: DateRangePickerProps) { const { t } = useUIKit(); const [leftDate, setLeftDate] = useState(new Date()); const [rightDate, setRightDate] = useState(() => { const date = new Date(); date.setMonth(date.getMonth() + 1); return date; }); const [internalDates, setInternalDates] = useState<[Date | null, Date | null]>([null, null]); const [isPickerOpenLeft, setIsPickerOpenLeft] = useState(false); const [isPickerOpenRight, setIsPickerOpenRight] = useState(false); const [pickerYearLeft, setPickerYearLeft] = useState(leftDate.getFullYear()); const [pickerYearRight, setPickerYearRight] = useState(rightDate.getFullYear()); const pickerRefLeft = useRef(null); const pickerRefRight = useRef(null); const isUpdatingRef = useRef(false); const MONTH_NAMES = [ t('DateRangePicker.January'), t('DateRangePicker.February'), t('DateRangePicker.March'), t('DateRangePicker.April'), t('DateRangePicker.May'), t('DateRangePicker.June'), t('DateRangePicker.July'), t('DateRangePicker.August'), t('DateRangePicker.September'), t('DateRangePicker.October'), t('DateRangePicker.November'), t('DateRangePicker.December'), ]; const WEEK_DAYS = [ t('DateRangePicker.Monday'), t('DateRangePicker.Tuesday'), t('DateRangePicker.Wednesday'), t('DateRangePicker.Thursday'), t('DateRangePicker.Friday'), t('DateRangePicker.Saturday'), t('DateRangePicker.Sunday'), ]; const setStartOfDay = (date: Date): Date => { const newDate = new Date(date); newDate.setHours(0, 0, 0, 0); return newDate; }; const setEndOfDay = (date: Date): Date => { const newDate = new Date(date); newDate.setHours(23, 59, 59, 999); return newDate; }; useEffect(() => { setInternalDates(value || [null, null]); }, [value]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (pickerRefLeft.current && !pickerRefLeft.current.contains(event.target as Node)) { setIsPickerOpenLeft(false); } if (pickerRefRight.current && !pickerRefRight.current.contains(event.target as Node)) { setIsPickerOpenRight(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); const getMonthData = (date: Date) => { const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const days = []; const firstDayWeek = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; // Adjust Sunday to be 6 for (let i = firstDayWeek; i > 0; i -= 1) { const prevDate = new Date(year, month, 1 - i); days.push({ date: prevDate, isCurrentMonth: false }); } for (let i = 1; i <= lastDay.getDate(); i += 1) { days.push({ date: new Date(year, month, i), isCurrentMonth: true }); } for (let i = 1; days.length < 42; i += 1) { // Ensure 6 rows const nextDate = new Date(year, month + 1, i); days.push({ date: nextDate, isCurrentMonth: false }); } return days; }; const handleDateClick = (date: Date) => { let newDates: [Date | null, Date | null]; if (!internalDates[0] || (internalDates[0] && internalDates[1])) { newDates = [setStartOfDay(date), null]; } else { const [start] = internalDates; if (start && date < start) { newDates = [setStartOfDay(date), setEndOfDay(start)]; } else { newDates = [setStartOfDay(start), setEndOfDay(date)]; } } isUpdatingRef.current = true; setInternalDates(newDates); onChange?.(newDates); isUpdatingRef.current = false; }; const updateDate = (newDate: Date, isLeft: boolean) => { const setDate = isLeft ? setLeftDate : setRightDate; const otherDate = isLeft ? rightDate : leftDate; // Normalize time to avoid issues with exact comparison const normalizedNewDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1); const normalizedOtherDate = new Date(otherDate.getFullYear(), otherDate.getMonth(), 1); // Ensure left < right month if (isLeft && normalizedNewDate >= normalizedOtherDate) { return; // Prevent left month from being >= right month } if (!isLeft && normalizedNewDate <= normalizedOtherDate) { return; // Prevent right month from being <= left month } setDate(newDate); }; const handleMonthChange = (isLeft: boolean, increment: boolean) => { const currentDate = isLeft ? leftDate : rightDate; const newDate = new Date(currentDate); newDate.setMonth(newDate.getMonth() + (increment ? 1 : -1)); updateDate(newDate, isLeft); }; const handleYearChange = (isLeft: boolean, increment: boolean) => { const currentDate = isLeft ? leftDate : rightDate; const newDate = new Date(currentDate); newDate.setFullYear(newDate.getFullYear() + (increment ? 1 : -1)); updateDate(newDate, isLeft); }; const handlePickerToggle = (isLeft: boolean) => { if (isLeft) { setPickerYearLeft(leftDate.getFullYear()); setIsPickerOpenLeft(!isPickerOpenLeft); setIsPickerOpenRight(false); // Close other picker } else { setPickerYearRight(rightDate.getFullYear()); setIsPickerOpenRight(!isPickerOpenRight); setIsPickerOpenLeft(false); // Close other picker } }; const handlePickerYearChange = (isLeft: boolean, year: number) => { if (isLeft) { setPickerYearLeft(year); } else { setPickerYearRight(year); } }; const handlePickerMonthSelect = (isLeft: boolean, month: number) => { const currentYear = isLeft ? pickerYearLeft : pickerYearRight; const newDate = new Date(currentYear, month, 1); updateDate(newDate, isLeft); if (isLeft) { setIsPickerOpenLeft(false); } else { setIsPickerOpenRight(false); } }; const renderPickerDropdown = (isLeft: boolean) => { const currentYear = isLeft ? pickerYearLeft : pickerYearRight; return (
{currentYear}
{MONTH_NAMES.map((monthName, index) => ( ))}
); }; const renderCalendar = (date: Date, isLeft: boolean) => { const days = getMonthData(date); const isPickerOpen = isLeft ? isPickerOpenLeft : isPickerOpenRight; return (
handlePickerToggle(isLeft)} > {date.getFullYear()} {' '} {MONTH_NAMES[date.getMonth()]} {isPickerOpen && renderPickerDropdown(isLeft)}
{WEEK_DAYS.map(day => (
{day}
))}
{days.map(({ date: day, isCurrentMonth }) => { const dayTime = day.setHours(0, 0, 0, 0); const startTime = internalDates[0] ? new Date(internalDates[0]).setHours(0, 0, 0, 0) : null; const endTime = internalDates[1] ? new Date(internalDates[1]).setHours(0, 0, 0, 0) : null; const isSelected = startTime && endTime && dayTime >= startTime && dayTime <= endTime; const isStart = startTime === dayTime; const isEnd = endTime === dayTime; return (
isCurrentMonth && handleDateClick(new Date(dayTime))} > {new Date(dayTime).getDate()}
); })}
); }; const handleH5Cancel = () => { onChange?.([null, null]); }; // H5 const renderH5 = () => (
e.stopPropagation()}>
{t('DateRangePicker.Time')}
{renderCalendar(leftDate, true)}
); // PC const renderPC = () => (
{renderCalendar(leftDate, true)} {renderCalendar(rightDate, false)}
); return renderPC(); } export default DateRangePicker;