mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
232 lines
8.2 KiB
TypeScript
232 lines
8.2 KiB
TypeScript
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<HTMLDivElement>(null)
|
|
|
|
// 点击外部关闭
|
|
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)
|
|
onStartDateChange(start.toISOString().split('T')[0])
|
|
onEndDateChange(end.toISOString().split('T')[0])
|
|
}
|
|
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) {
|
|
onStartDateChange(dateStr)
|
|
if (endDate && dateStr > endDate) {
|
|
onEndDateChange('')
|
|
}
|
|
setSelectingStart(false)
|
|
} else {
|
|
if (dateStr < startDate) {
|
|
onStartDateChange(dateStr)
|
|
onEndDateChange(startDate)
|
|
} else {
|
|
onEndDateChange(dateStr)
|
|
}
|
|
setSelectingStart(true)
|
|
setIsOpen(false)
|
|
setTimeout(() => onRangeComplete?.(), 0)
|
|
}
|
|
}
|
|
|
|
const isInRange = (day: number) => {
|
|
if (!startDate || !endDate) return false
|
|
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
return dateStr >= startDate && dateStr <= endDate
|
|
}
|
|
|
|
const isStartDate = (day: number) => {
|
|
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
return dateStr === startDate
|
|
}
|
|
|
|
const isEndDate = (day: number) => {
|
|
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
return dateStr === endDate
|
|
}
|
|
|
|
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 (
|
|
<div className="calendar-grid">
|
|
{WEEKDAY_NAMES.map(name => (
|
|
<div key={name} className="weekday-header">{name}</div>
|
|
))}
|
|
{days.map((day, index) => (
|
|
<div
|
|
key={index}
|
|
className={`calendar-day ${day ? 'valid' : ''} ${day && isInRange(day) ? 'in-range' : ''} ${day && isStartDate(day) ? 'start' : ''} ${day && isEndDate(day) ? 'end' : ''} ${day && isToday(day) ? 'today' : ''}`}
|
|
onClick={() => day && handleDateClick(day)}
|
|
>
|
|
{day}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="date-range-picker" ref={containerRef}>
|
|
<button className="picker-trigger" onClick={() => setIsOpen(!isOpen)}>
|
|
<Calendar size={14} />
|
|
<span className="picker-text">{getDisplayText()}</span>
|
|
{(startDate || endDate) && (
|
|
<button className="clear-btn" onClick={handleClear}>
|
|
<X size={12} />
|
|
</button>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="picker-dropdown">
|
|
<div className="quick-options">
|
|
{QUICK_OPTIONS.map(opt => (
|
|
<button key={opt.label} className="quick-option" onClick={() => handleQuickOption(opt.days)}>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="calendar-section">
|
|
<div className="calendar-header">
|
|
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}>
|
|
<ChevronLeft size={16} />
|
|
</button>
|
|
<span className="month-year clickable" onClick={() => setShowYearMonthPicker(!showYearMonthPicker)}>
|
|
{currentMonth.getFullYear()}年 {MONTH_NAMES[currentMonth.getMonth()]}
|
|
</span>
|
|
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}>
|
|
<ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
{showYearMonthPicker ? (
|
|
<div className="year-month-picker">
|
|
<div className="year-selector">
|
|
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear() - 1, currentMonth.getMonth()))}>
|
|
<ChevronLeft size={14} />
|
|
</button>
|
|
<span className="year-label">{currentMonth.getFullYear()}年</span>
|
|
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear() + 1, currentMonth.getMonth()))}>
|
|
<ChevronRight size={14} />
|
|
</button>
|
|
</div>
|
|
<div className="month-grid">
|
|
{MONTH_NAMES.map((name, i) => (
|
|
<button
|
|
key={i}
|
|
className={`month-btn ${i === currentMonth.getMonth() ? 'active' : ''}`}
|
|
onClick={() => {
|
|
setCurrentMonth(new Date(currentMonth.getFullYear(), i))
|
|
setShowYearMonthPicker(false)
|
|
}}
|
|
>{name}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : renderCalendar()}
|
|
<div className="selection-hint">
|
|
{selectingStart ? '请选择开始日期' : '请选择结束日期'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DateRangePicker
|