import { useCallback, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { Check, X } from 'lucide-react' import { EXPORT_DATE_RANGE_PRESETS, WEEKDAY_SHORT_LABELS, addMonths, buildCalendarCells, cloneExportDateRangeSelection, createDateRangeByPreset, createDefaultDateRange, formatCalendarMonthTitle, formatDateInputValue, isSameDay, parseDateInputValue, startOfDay, endOfDay, toMonthStart, type ExportDateRangePreset, type ExportDateRangeSelection } from '../../utils/exportDateRange' import './ExportDateRangeDialog.scss' interface ExportDateRangeDialogProps { open: boolean value: ExportDateRangeSelection title?: string onClose: () => void onConfirm: (value: ExportDateRangeSelection) => void } interface ExportDateRangeDialogDraft extends ExportDateRangeSelection { startPanelMonth: Date endPanelMonth: Date } const buildDialogDraft = (value: ExportDateRangeSelection): ExportDateRangeDialogDraft => ({ ...cloneExportDateRangeSelection(value), startPanelMonth: toMonthStart(value.dateRange.start), endPanelMonth: toMonthStart(value.dateRange.end) }) export function ExportDateRangeDialog({ open, value, title = '时间范围设置', onClose, onConfirm }: ExportDateRangeDialogProps) { const [draft, setDraft] = useState(() => buildDialogDraft(value)) const [dateInput, setDateInput] = useState({ start: formatDateInputValue(value.dateRange.start), end: formatDateInputValue(value.dateRange.end) }) const [dateInputError, setDateInputError] = useState({ start: false, end: false }) useEffect(() => { if (!open) return const nextDraft = buildDialogDraft(value) setDraft(nextDraft) setDateInput({ start: formatDateInputValue(nextDraft.dateRange.start), end: formatDateInputValue(nextDraft.dateRange.end) }) setDateInputError({ start: false, end: false }) }, [open, value]) useEffect(() => { if (!open) return setDateInput({ start: formatDateInputValue(draft.dateRange.start), end: formatDateInputValue(draft.dateRange.end) }) setDateInputError({ start: false, end: false }) }, [draft.dateRange.end.getTime(), draft.dateRange.start.getTime(), open]) const applyPreset = useCallback((preset: Exclude) => { if (preset === 'all') { const previewRange = createDefaultDateRange() setDraft(prev => ({ ...prev, preset, useAllTime: true, dateRange: previewRange, startPanelMonth: toMonthStart(previewRange.start), endPanelMonth: toMonthStart(previewRange.end) })) return } const range = createDateRangeByPreset(preset) setDraft(prev => ({ ...prev, preset, useAllTime: false, dateRange: range, startPanelMonth: toMonthStart(range.start), endPanelMonth: toMonthStart(range.end) })) }, []) const updateDraftStart = useCallback((targetDate: Date) => { const start = startOfDay(targetDate) setDraft(prev => { const nextEnd = prev.dateRange.end < start ? endOfDay(start) : prev.dateRange.end return { ...prev, preset: 'custom', useAllTime: false, dateRange: { start, end: nextEnd }, startPanelMonth: toMonthStart(start), endPanelMonth: toMonthStart(nextEnd) } }) }, []) const updateDraftEnd = useCallback((targetDate: Date) => { const end = endOfDay(targetDate) setDraft(prev => { const nextStart = prev.useAllTime ? startOfDay(targetDate) : prev.dateRange.start const nextEnd = end < nextStart ? endOfDay(nextStart) : end return { ...prev, preset: 'custom', useAllTime: false, dateRange: { start: nextStart, end: nextEnd }, startPanelMonth: toMonthStart(nextStart), endPanelMonth: toMonthStart(nextEnd) } }) }, []) const commitStartFromInput = useCallback(() => { const parsed = parseDateInputValue(dateInput.start) if (!parsed) { setDateInputError(prev => ({ ...prev, start: true })) return } setDateInputError(prev => ({ ...prev, start: false })) updateDraftStart(parsed) }, [dateInput.start, updateDraftStart]) const commitEndFromInput = useCallback(() => { const parsed = parseDateInputValue(dateInput.end) if (!parsed) { setDateInputError(prev => ({ ...prev, end: true })) return } setDateInputError(prev => ({ ...prev, end: false })) updateDraftEnd(parsed) }, [dateInput.end, updateDraftEnd]) const shiftPanelMonth = useCallback((panel: 'start' | 'end', delta: number) => { setDraft(prev => ( panel === 'start' ? { ...prev, startPanelMonth: addMonths(prev.startPanelMonth, delta) } : { ...prev, endPanelMonth: addMonths(prev.endPanelMonth, delta) } )) }, []) const isRangeModeActive = !draft.useAllTime const modeText = isRangeModeActive ? '当前导出模式:按时间范围导出' : '当前导出模式:全部时间导出(选择下方日期将切换为按时间范围导出)' const isPresetActive = useCallback((preset: ExportDateRangePreset): boolean => { if (preset === 'all') return draft.useAllTime return !draft.useAllTime && draft.preset === preset }, [draft]) const startPanelCells = useMemo(() => buildCalendarCells(draft.startPanelMonth), [draft.startPanelMonth]) const endPanelCells = useMemo(() => buildCalendarCells(draft.endPanelMonth), [draft.endPanelMonth]) if (!open) return null return createPortal(
event.stopPropagation()}>

{title}

{EXPORT_DATE_RANGE_PRESETS.map((preset) => { const active = isPresetActive(preset.value) return ( ) })}
{modeText}
起始日期 { const nextValue = event.target.value setDateInput(prev => ({ ...prev, start: nextValue })) if (dateInputError.start) { setDateInputError(prev => ({ ...prev, start: false })) } }} onKeyDown={(event) => { if (event.key !== 'Enter') return event.preventDefault() commitStartFromInput() }} onBlur={commitStartFromInput} />
{formatCalendarMonthTitle(draft.startPanelMonth)}
{WEEKDAY_SHORT_LABELS.map(label => ( {label} ))}
{startPanelCells.map((cell) => { const selected = !draft.useAllTime && isSameDay(cell.date, draft.dateRange.start) return ( ) })}
截止日期 { const nextValue = event.target.value setDateInput(prev => ({ ...prev, end: nextValue })) if (dateInputError.end) { setDateInputError(prev => ({ ...prev, end: false })) } }} onKeyDown={(event) => { if (event.key !== 'Enter') return event.preventDefault() commitEndFromInput() }} onBlur={commitEndFromInput} />
{formatCalendarMonthTitle(draft.endPanelMonth)}
{WEEKDAY_SHORT_LABELS.map(label => ( {label} ))}
{endPanelCells.map((cell) => { const selected = !draft.useAllTime && isSameDay(cell.date, draft.dateRange.end) return ( ) })}
, document.body ) }