import { useState, useRef, useEffect } from 'react' import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-react' import './DateRangePicker.scss' interface DateRangePickerProps { startDate: string endDate: string onStartDateChange: (date: string) => void onEndDateChange: (date: string) => void onRangeComplete?: () => void } const MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] const WEEKDAY_NAMES = ['日', '一', '二', '三', '四', '五', '六'] // 快捷选项 const QUICK_OPTIONS = [ { label: '最近7天', days: 7 }, { label: '最近30天', days: 30 }, { label: '最近90天', days: 90 }, { label: '最近一年', days: 365 }, { label: '全部时间', days: 0 }, ] function DateRangePicker({ startDate, endDate, onStartDateChange, onEndDateChange, onRangeComplete }: DateRangePickerProps) { const [isOpen, setIsOpen] = useState(false) const [currentMonth, setCurrentMonth] = useState(new Date()) const [selectingStart, setSelectingStart] = useState(true) const [showYearMonthPicker, setShowYearMonthPicker] = useState(false) const containerRef = useRef(null) const [internalStart, setInternalStart] = useState(startDate) const [internalEnd, setInternalEnd] = useState(endDate) useEffect(() => { setInternalStart(startDate) setInternalEnd(endDate) }, [startDate, endDate]) useEffect(() => { if (isOpen) { setSelectingStart(true) } }, [isOpen]) // 点击外部关闭 useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setIsOpen(false) } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside) } return () => document.removeEventListener('mousedown', handleClickOutside) }, [isOpen]) const formatDisplayDate = (dateStr: string) => { if (!dateStr) return '' const date = new Date(dateStr) return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` } const getDisplayText = () => { if (!startDate && !endDate) return '选择时间范围' if (startDate && endDate) return `${formatDisplayDate(startDate)} - ${formatDisplayDate(endDate)}` if (startDate) return `${formatDisplayDate(startDate)} - ?` return `? - ${formatDisplayDate(endDate)}` } const handleQuickOption = (days: number) => { if (days === 0) { onStartDateChange('') onEndDateChange('') } else { const end = new Date() const start = new Date() start.setDate(start.getDate() - days) const startStr = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}-${String(start.getDate()).padStart(2, '0')}` const endStr = `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, '0')}-${String(end.getDate()).padStart(2, '0')}` onStartDateChange(startStr) onEndDateChange(endStr) } setIsOpen(false) setTimeout(() => onRangeComplete?.(), 0) } const handleClear = (e: React.MouseEvent) => { e.stopPropagation() onStartDateChange('') onEndDateChange('') } const getDaysInMonth = (date: Date) => { return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() } const getFirstDayOfMonth = (date: Date) => { return new Date(date.getFullYear(), date.getMonth(), 1).getDay() } const handleDateClick = (day: number) => { const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` if (selectingStart) { setInternalStart(dateStr) if (internalEnd && dateStr > internalEnd) { setInternalEnd('') } setSelectingStart(false) } else { let finalStart = internalStart let finalEnd = dateStr if (dateStr < internalStart) { finalStart = dateStr finalEnd = internalStart } setInternalStart(finalStart) setInternalEnd(finalEnd) setSelectingStart(true) setIsOpen(false) onStartDateChange(finalStart) onEndDateChange(finalEnd) setTimeout(() => onRangeComplete?.(), 0) } } const isInRange = (day: number) => { if (!internalStart || !internalEnd) return false const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` return dateStr >= internalStart && dateStr <= internalEnd } const isStartDate = (day: number) => { const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` return dateStr === internalStart } const isEndDate = (day: number) => { const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` return dateStr === internalEnd } const isToday = (day: number) => { const today = new Date() return currentMonth.getFullYear() === today.getFullYear() && currentMonth.getMonth() === today.getMonth() && day === today.getDate() } const renderCalendar = () => { const daysInMonth = getDaysInMonth(currentMonth) const firstDay = getFirstDayOfMonth(currentMonth) const days: (number | null)[] = [] for (let i = 0; i < firstDay; i++) { days.push(null) } for (let i = 1; i <= daysInMonth; i++) { days.push(i) } return (
{WEEKDAY_NAMES.map(name => (
{name}
))} {days.map((day, index) => (
day && handleDateClick(day)} > {day}
))}
) } return (
)} {isOpen && (
{QUICK_OPTIONS.map(opt => ( ))}
setShowYearMonthPicker(!showYearMonthPicker)}> {currentMonth.getFullYear()}年 {MONTH_NAMES[currentMonth.getMonth()]}
{showYearMonthPicker ? (
{currentMonth.getFullYear()}年
{MONTH_NAMES.map((name, i) => ( ))}
) : renderCalendar()}
{selectingStart ? '请选择开始日期' : '请选择结束日期'}
)}
) } export default DateRangePicker