import { useState, useEffect } from 'react' import * as Popover from '@radix-ui/react-popover' import * as Select from '@radix-ui/react-select' import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, XMarkIcon, ChevronDownIcon } from '@heroicons/react/24/solid' import dayjs from 'dayjs' // 日期选择器组件 export default function DateRangePicker({ startDate, endDate, onChange, onStartDateChange, onEndDateChange }) { const [currentMonth, setCurrentMonth] = useState(dayjs()) const [nextMonth, setNextMonth] = useState(dayjs().add(1, 'month')) const [selectedStartDate, setSelectedStartDate] = useState(startDate ? dayjs(startDate) : null) const [selectedEndDate, setSelectedEndDate] = useState(endDate ? dayjs(endDate) : null) // 确保初始值设置正确 useEffect(() => { if (startDate && !selectedStartDate) { setSelectedStartDate(dayjs(startDate)) } if (endDate && !selectedEndDate) { setSelectedEndDate(dayjs(endDate)) } }, [startDate, endDate]) const [isOpen, setIsOpen] = useState(false) const monthOptions = [ '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' ] const yearOptions = Array.from({ length: 10 }, (_, i) => dayjs().year() - 5 + i) // 快速选择选项 const quickSelectOptions = [ { label: '今天', value: 'today' }, { label: '昨天', value: 'yesterday' }, { label: '本周', value: 'this_week' }, { label: '上周', value: 'last_week' }, { label: '最近7天', value: 'last_7_days' }, { label: '本月', value: 'this_month' }, { label: '上月', value: 'last_month' }, { label: '本年', value: 'this_year' }, { label: '上年', value: 'last_year' } ] // 获取月份的天数数组 const getDaysInMonth = (year, month) => { const date = dayjs().year(year).month(month).startOf('month') const days = [] const firstDay = date.day() // 0 是周日,1 是周一 // 添加上个月的日期填充第一周 const prevMonth = date.subtract(1, 'month') const prevMonthDays = prevMonth.daysInMonth() for (let i = firstDay - 1; i >= 0; i--) { days.push({ date: prevMonth.date(prevMonthDays - i), isCurrentMonth: false }) } // 添加当前月的日期 const daysInMonth = date.daysInMonth() for (let i = 1; i <= daysInMonth; i++) { days.push({ date: date.date(i), isCurrentMonth: true }) } // 添加下个月的日期填充最后一周 const lastDayOfMonth = date.date(daysInMonth).day() // 0 是周日,6 是周六 const daysToAdd = 6 - lastDayOfMonth const nextMonth = date.add(1, 'month').date(1) for (let i = 1; i <= daysToAdd; i++) { days.push({ date: nextMonth.date(i), isCurrentMonth: false }) } return days } // 处理月份变更 const handlePrevMonth = () => { setCurrentMonth(currentMonth.subtract(1, 'month')) setNextMonth(nextMonth.subtract(1, 'month')) } const handleNextMonth = () => { setCurrentMonth(currentMonth.add(1, 'month')) setNextMonth(nextMonth.add(1, 'month')) } // 处理年份变更 const handleYearChange = (calendarIndex, year) => { if (calendarIndex === 0) { const newCurrentMonth = currentMonth.year(parseInt(year)) setCurrentMonth(newCurrentMonth) setNextMonth(newCurrentMonth.add(1, 'month')) } else { const newNextMonth = nextMonth.year(parseInt(year)) setNextMonth(newNextMonth) setCurrentMonth(newNextMonth.subtract(1, 'month')) } } // 处理月份变更 const handleMonthChange = (calendarIndex, month) => { if (calendarIndex === 0) { const newCurrentMonth = currentMonth.month(parseInt(month)) setCurrentMonth(newCurrentMonth) setNextMonth(newCurrentMonth.add(1, 'month')) } else { const newNextMonth = nextMonth.month(parseInt(month)) setNextMonth(newNextMonth) setCurrentMonth(newNextMonth.subtract(1, 'month')) } } // 处理日期选择 const handleDateSelect = (date) => { if (!selectedStartDate || (selectedStartDate && selectedEndDate)) { // 选择开始日期 setSelectedStartDate(date) setSelectedEndDate(null) // 确保调用父组件的回调函数,传递正确的日期字符串 const formattedDate = formatDate(date) onChange(formattedDate, '') } else { if (date.isBefore(selectedStartDate)) { // 如果选择的日期早于已选的开始日期,则调整范围 setSelectedStartDate(date) setSelectedEndDate(selectedStartDate) const formattedStartDate = formatDate(date) const formattedEndDate = formatDate(selectedStartDate) onChange(formattedStartDate, formattedEndDate) } else { // 选择结束日期,保持开始日期不变 setSelectedEndDate(date) const formattedStartDate = formatDate(selectedStartDate) const formattedEndDate = formatDate(date) // 重要:同时再次传递开始日期,确保不会丢失 onChange(formattedStartDate, formattedEndDate) } } } // 处理快速选择 const handleQuickSelect = (option) => { const today = dayjs().startOf('day') let start, end switch (option) { case 'today': start = end = today break case 'yesterday': start = end = today.subtract(1, 'day') break case 'this_week': start = today.startOf('week') // dayjs 默认周日为一周的开始 end = today.endOf('week') break case 'last_week': start = today.subtract(1, 'week').startOf('week') end = today.subtract(1, 'week').endOf('week') break case 'last_7_days': start = today.subtract(6, 'day') end = today break case 'this_month': start = today.startOf('month') end = today.endOf('month') break case 'last_month': start = today.subtract(1, 'month').startOf('month') end = today.subtract(1, 'month').endOf('month') break case 'this_year': start = today.startOf('year') end = today.endOf('year') break case 'last_year': start = today.subtract(1, 'year').startOf('year') end = today.subtract(1, 'year').endOf('year') break default: return } setSelectedStartDate(start) setSelectedEndDate(end) // 确保调用父组件的回调函数,传递正确的日期字符串 const formattedStartDate = formatDate(start) const formattedEndDate = formatDate(end) onStartDateChange(formattedStartDate) onEndDateChange(formattedEndDate) // 调试日志 setIsOpen(false) } // 格式化日期为 YYYY-MM-DD const formatDate = (date) => { if (!date) return '' // 确保返回正确格式的日期字符串 const formatted = date.format('YYYY-MM-DD') return formatted } // 格式化显示日期 const formatDisplayDate = (date) => { if (!date) return '' return date.format('YYYY-MM-DD') } // 判断日期是否在选中范围内 const isInRange = (date) => { if (!selectedStartDate || !selectedEndDate) return false return date.isAfter(selectedStartDate) && date.isBefore(selectedEndDate) } // 判断是否是今天 const isToday = (date) => { return date.format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD') } // 渲染日历 const renderCalendar = (year, month) => { const days = getDaysInMonth(year, month) const weekdays = ['日', '一', '二', '三', '四', '五', '六'] return ( <div className="calendar"> <div className="grid grid-cols-7 gap-1"> {weekdays.map((day, index) => ( <div key={index} className="text-center text-xs font-medium text-gray-500 py-2"> {day} </div> ))} {days.map((day, index) => { const isSelected = selectedStartDate && day.date.format('YYYY-MM-DD') === selectedStartDate.format('YYYY-MM-DD') || selectedEndDate && day.date.format('YYYY-MM-DD') === selectedEndDate.format('YYYY-MM-DD') const inRange = isInRange(day.date) return ( <button key={index} type="button" onClick={() => handleDateSelect(day.date)} className={` h-8 w-8 rounded-full flex items-center justify-center text-sm ${!day.isCurrentMonth ? 'text-gray-400' : ''} ${isToday(day.date) && !isSelected ? 'border border-indigo-500' : ''} ${isSelected ? 'bg-indigo-600 text-white' : ''} ${inRange ? 'bg-indigo-100' : ''} ${day.isCurrentMonth && !isSelected && !inRange ? 'hover:bg-gray-100' : ''} `} > {day.date.date()} </button> ) })} </div> </div> ) } return ( <div className="relative"> <div className="flex items-center"> <div className="relative w-full"> <Popover.Root open={isOpen} onOpenChange={setIsOpen}> <Popover.Trigger asChild> <button type="button" className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-white px-3 py-2 text-left border flex items-center justify-between" > <span> {selectedStartDate && selectedEndDate ? `${formatDisplayDate(selectedStartDate)} 至 ${formatDisplayDate(selectedEndDate)}` : selectedStartDate ? `${formatDisplayDate(selectedStartDate)} 至 未选择` : '选择日期范围'} </span> <CalendarIcon className="h-5 w-5 text-gray-400" /> </button> </Popover.Trigger> <Popover.Portal> <Popover.Content className="bg-white rounded-md shadow-lg p-4 w-[700px] border border-gray-200 z-50" sideOffset={5} > <div className="flex flex-col"> <div className="flex justify-between items-center mb-4"> <div className="text-sm font-medium text-gray-700">选择日期范围</div> <Popover.Close className="rounded-full p-1 hover:bg-gray-100"> <XMarkIcon className="h-4 w-4 text-gray-500" /> </Popover.Close> </div> <div className="flex space-x-4"> {/* 左侧快速选择 */} <div className="w-1/4 border-r pr-4"> <div className="text-sm font-medium text-gray-700 mb-2">快速选择</div> <div className="space-y-1"> {quickSelectOptions.map((option) => ( <button key={option.value} type="button" className="block w-full text-left px-2 py-1 text-sm rounded hover:bg-gray-100" onClick={() => handleQuickSelect(option.value)} > {option.label} </button> ))} </div> </div> {/* 右侧日历 */} <div className="w-3/4"> <div className="flex justify-between items-center mb-4"> <button type="button" onClick={handlePrevMonth} className="p-1 rounded-full hover:bg-gray-100" > <ChevronLeftIcon className="h-5 w-5 text-gray-500" /> </button> <div className="flex space-x-4"> <div className="flex items-center space-x-2"> <Select.Root value={currentMonth.year().toString()} onValueChange={(value) => handleYearChange(0, value)}> <Select.Trigger className="inline-flex items-center justify-center rounded px-2 py-1 text-sm leading-none h-8 gap-1 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none"> <Select.Value /> <Select.Icon> <ChevronDownIcon className="h-4 w-4" /> </Select.Icon> </Select.Trigger> <Select.Portal> <Select.Content className="overflow-hidden bg-white rounded-md shadow-lg z-[9999]"> <Select.Viewport className="p-1"> <Select.Group> {yearOptions.map(year => ( <Select.Item key={year} value={year.toString()} className="text-sm leading-none rounded-md flex items-center h-8 pr-9 pl-6 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-indigo-100 data-[highlighted]:text-indigo-900"> <Select.ItemText>{year}</Select.ItemText> </Select.Item> ))} </Select.Group> </Select.Viewport> </Select.Content> </Select.Portal> </Select.Root> <Select.Root value={currentMonth.month().toString()} onValueChange={(value) => handleMonthChange(0, value)}> <Select.Trigger className="inline-flex items-center justify-center rounded px-2 py-1 text-sm leading-none h-8 gap-1 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none"> <Select.Value placeholder={monthOptions[currentMonth.month()]} /> <Select.Icon> <ChevronDownIcon className="h-4 w-4" /> </Select.Icon> </Select.Trigger> <Select.Portal> <Select.Content className="overflow-hidden bg-white rounded-md shadow-lg z-[9999]"> <Select.Viewport className="p-1"> <Select.Group> {monthOptions.map((month, index) => ( <Select.Item key={index} value={index.toString()} className="text-sm leading-none rounded-md flex items-center h-8 pr-9 pl-6 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-indigo-100 data-[highlighted]:text-indigo-900"> <Select.ItemText>{month}</Select.ItemText> </Select.Item> ))} </Select.Group> </Select.Viewport> </Select.Content> </Select.Portal> </Select.Root> </div> <div className="flex items-center space-x-2"> <Select.Root value={nextMonth.year().toString()} onValueChange={(value) => handleYearChange(1, value)}> <Select.Trigger className="inline-flex items-center justify-center rounded px-2 py-1 text-sm leading-none h-8 gap-1 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none"> <Select.Value /> <Select.Icon> <ChevronDownIcon className="h-4 w-4" /> </Select.Icon> </Select.Trigger> <Select.Portal> <Select.Content className="overflow-hidden bg-white rounded-md shadow-lg z-[9999]"> <Select.Viewport className="p-1"> <Select.Group> {yearOptions.map(year => ( <Select.Item key={year} value={year.toString()} className="text-sm leading-none rounded-md flex items-center h-8 pr-9 pl-6 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-indigo-100 data-[highlighted]:text-indigo-900"> <Select.ItemText>{year}</Select.ItemText> </Select.Item> ))} </Select.Group> </Select.Viewport> </Select.Content> </Select.Portal> </Select.Root> <Select.Root value={nextMonth.month().toString()} onValueChange={(value) => handleMonthChange(1, value)}> <Select.Trigger className="inline-flex items-center justify-center rounded px-2 py-1 text-sm leading-none h-8 gap-1 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none"> <Select.Value placeholder={monthOptions[nextMonth.month()]} /> <Select.Icon> <ChevronDownIcon className="h-4 w-4" /> </Select.Icon> </Select.Trigger> <Select.Portal> <Select.Content className="overflow-hidden bg-white rounded-md shadow-lg z-[9999]"> <Select.Viewport className="p-1"> <Select.Group> {monthOptions.map((month, index) => ( <Select.Item key={index} value={index.toString()} className="text-sm leading-none rounded-md flex items-center h-8 pr-9 pl-6 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-indigo-100 data-[highlighted]:text-indigo-900"> <Select.ItemText>{month}</Select.ItemText> </Select.Item> ))} </Select.Group> </Select.Viewport> </Select.Content> </Select.Portal> </Select.Root> </div> </div> <button type="button" onClick={handleNextMonth} className="p-1 rounded-full hover:bg-gray-100" > <ChevronRightIcon className="h-5 w-5 text-gray-500" /> </button> </div> <div className="flex space-x-4"> <div className="w-1/2"> {renderCalendar(currentMonth.year(), currentMonth.month())} </div> <div className="w-1/2"> {renderCalendar(nextMonth.year(), nextMonth.month())} </div> </div> </div> </div> </div> </Popover.Content> </Popover.Portal> </Popover.Root> </div> </div> </div> ) }