diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 5428b95..0fefaea 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -2349,7 +2349,7 @@ } .time-range-dialog { - width: min(920px, calc(100vw - 32px)); + width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 64px); overflow-y: auto; border-radius: 12px; @@ -2374,24 +2374,32 @@ } .time-range-preset-list { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 8px; + display: flex; + flex-wrap: nowrap; + gap: 4px; + overflow-x: auto; + padding-bottom: 2px; + + &::-webkit-scrollbar { + height: 4px; + } } .time-range-preset-item { + flex: 0 0 auto; border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 8px; background: var(--bg-secondary); color: var(--text-primary); - min-height: 38px; - padding: 0 10px; + min-height: 30px; + padding: 0 8px; display: flex; align-items: center; justify-content: space-between; - gap: 8px; - font-size: 12px; + gap: 4px; + font-size: 11px; cursor: pointer; + white-space: nowrap; &.active { border-color: var(--primary); @@ -2403,14 +2411,30 @@ .time-range-calendar-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 12px; + gap: 8px; +} + +.time-range-mode-banner { + border-radius: 8px; + padding: 6px 8px; + font-size: 11px; + line-height: 1.4; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + + &.range { + border-color: rgba(var(--primary-rgb), 0.4); + background: rgba(var(--primary-rgb), 0.1); + color: var(--primary); + } } .time-range-calendar-panel { border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 8px; background: var(--bg-secondary); - padding: 10px; + padding: 7px; } .time-range-calendar-panel-header { @@ -2430,23 +2454,40 @@ color: var(--text-secondary); } - strong { - font-size: 13px; - color: var(--text-primary); + small { + font-size: 10px; + color: var(--text-tertiary); + } +} + +.time-range-date-input { + width: 100%; + min-width: 0; + border-radius: 6px; + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-primary); + height: 24px; + padding: 0 7px; + font-size: 11px; + + &.invalid { + border-color: #e84d4d; + box-shadow: 0 0 0 1px rgba(232, 77, 77, 0.2); } } .time-range-calendar-nav { display: inline-flex; align-items: center; - gap: 6px; - font-size: 12px; + gap: 4px; + font-size: 11px; color: var(--text-primary); button { - width: 22px; - height: 22px; - border-radius: 6px; + width: 20px; + height: 20px; + border-radius: 5px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary); @@ -2457,32 +2498,32 @@ } .time-range-calendar-weekdays { - margin-top: 8px; + margin-top: 6px; display: grid; grid-template-columns: repeat(7, 1fr); - gap: 4px; + gap: 2px; span { text-align: center; - font-size: 11px; + font-size: 10px; color: var(--text-tertiary); } } .time-range-calendar-days { - margin-top: 6px; + margin-top: 4px; display: grid; grid-template-columns: repeat(7, 1fr); - gap: 4px; + gap: 2px; } .time-range-calendar-day { border: 1px solid transparent; - border-radius: 8px; - min-height: 28px; + border-radius: 6px; + min-height: 20px; background: var(--bg-primary); color: var(--text-primary); - font-size: 12px; + font-size: 10px; cursor: pointer; padding: 0; @@ -2499,16 +2540,6 @@ } } -.time-range-full-hint { - font-size: 12px; - color: var(--text-secondary); - line-height: 1.45; - border-radius: 8px; - border: 1px dashed var(--border-color); - padding: 8px 10px; - background: var(--bg-secondary); -} - .time-range-custom-row { display: none; @@ -2672,7 +2703,7 @@ } .time-range-preset-list { - grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; } .time-range-calendar-grid { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 1b85b96..f01f256 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -499,6 +499,26 @@ const formatDateInputValue = (date: Date): string => { return `${y}-${m}-${d}` } +const parseDateInputValue = (raw: string): Date | null => { + const text = String(raw || '').trim() + const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text) + if (!matched) return null + const year = Number(matched[1]) + const month = Number(matched[2]) + const day = Number(matched[3]) + if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null + if (month < 1 || month > 12 || day < 1 || day > 31) return null + const parsed = new Date(year, month - 1, day) + if ( + parsed.getFullYear() !== year || + parsed.getMonth() !== month - 1 || + parsed.getDate() !== day + ) { + return null + } + return parsed +} + const toMonthStart = (date: Date): Date => new Date(date.getFullYear(), date.getMonth(), 1) const addMonths = (date: Date, delta: number): Date => { @@ -1212,6 +1232,8 @@ function ExportPage() { const [isTimeRangeDialogOpen, setIsTimeRangeDialogOpen] = useState(false) const [timeRangePreset, setTimeRangePreset] = useState('all') const [timeRangeDialogDraft, setTimeRangeDialogDraft] = useState(null) + const [timeRangeDateInput, setTimeRangeDateInput] = useState<{ start: string; end: string }>({ start: '', end: '' }) + const [timeRangeDateInputError, setTimeRangeDateInputError] = useState<{ start: boolean; end: boolean }>({ start: false, end: false }) const [options, setOptions] = useState({ format: 'json', @@ -2321,6 +2343,8 @@ function ExportPage() { setExportDialog(prev => ({ ...prev, open: false })) setIsTimeRangeDialogOpen(false) setTimeRangeDialogDraft(null) + setTimeRangeDateInput({ start: '', end: '' }) + setTimeRangeDateInputError({ start: false, end: false }) }, []) const buildTimeRangeDialogDraft = useCallback((): TimeRangeDialogDraft => { @@ -2338,23 +2362,33 @@ function ExportPage() { }, [options.dateRange, options.useAllTime, timeRangePreset]) const openTimeRangeDialog = useCallback(() => { - setTimeRangeDialogDraft(buildTimeRangeDialogDraft()) + const draft = buildTimeRangeDialogDraft() + setTimeRangeDialogDraft(draft) setIsTimeRangeDialogOpen(true) }, [buildTimeRangeDialogDraft]) const closeTimeRangeDialog = useCallback(() => { setIsTimeRangeDialogOpen(false) setTimeRangeDialogDraft(null) + setTimeRangeDateInput({ start: '', end: '' }) + setTimeRangeDateInputError({ start: false, end: false }) }, []) const applyTimeRangePresetToDraft = useCallback((preset: Exclude) => { setTimeRangeDialogDraft(prev => { const base = prev ?? buildTimeRangeDialogDraft() if (preset === 'all') { + const previewRange = createDefaultDateRange() return { ...base, preset, - useAllTime: true + useAllTime: true, + dateRange: { + start: previewRange.start, + end: previewRange.end + }, + startPanelMonth: toMonthStart(previewRange.start), + endPanelMonth: toMonthStart(previewRange.end) } } const range = createDateRangeByPreset(preset) @@ -2372,6 +2406,10 @@ function ExportPage() { }) }, [buildTimeRangeDialogDraft]) + const handleTimeRangePresetClick = useCallback((preset: Exclude) => { + applyTimeRangePresetToDraft(preset) + }, [applyTimeRangePresetToDraft]) + const updateTimeRangeDraftStart = useCallback((targetDate: Date) => { const start = startOfDay(targetDate) setTimeRangeDialogDraft(prev => { @@ -2395,20 +2433,45 @@ function ExportPage() { const end = endOfDay(targetDate) setTimeRangeDialogDraft(prev => { const base = prev ?? buildTimeRangeDialogDraft() - const nextEnd = end < base.dateRange.start ? endOfDay(base.dateRange.start) : end + const isAllTimeMode = base.useAllTime + const nextStart = isAllTimeMode + ? startOfDay(targetDate) + : base.dateRange.start + const nextEnd = end < nextStart ? endOfDay(nextStart) : end return { ...base, preset: 'custom', useAllTime: false, dateRange: { - start: base.dateRange.start, + start: nextStart, end: nextEnd }, + startPanelMonth: toMonthStart(nextStart), endPanelMonth: toMonthStart(nextEnd) } }) }, [buildTimeRangeDialogDraft]) + const commitTimeRangeStartFromInput = useCallback(() => { + const parsed = parseDateInputValue(timeRangeDateInput.start) + if (!parsed) { + setTimeRangeDateInputError(prev => ({ ...prev, start: true })) + return + } + setTimeRangeDateInputError(prev => ({ ...prev, start: false })) + updateTimeRangeDraftStart(parsed) + }, [timeRangeDateInput.start, updateTimeRangeDraftStart]) + + const commitTimeRangeEndFromInput = useCallback(() => { + const parsed = parseDateInputValue(timeRangeDateInput.end) + if (!parsed) { + setTimeRangeDateInputError(prev => ({ ...prev, end: true })) + return + } + setTimeRangeDateInputError(prev => ({ ...prev, end: false })) + updateTimeRangeDraftEnd(parsed) + }, [timeRangeDateInput.end, updateTimeRangeDraftEnd]) + const shiftTimeRangePanelMonth = useCallback((panel: 'start' | 'end', delta: number) => { setTimeRangeDialogDraft(prev => { const base = prev ?? buildTimeRangeDialogDraft() @@ -2445,7 +2508,7 @@ function ExportPage() { if (timeRangePreset === 'yesterday') return '昨天' if (timeRangePreset === 'last3days') return '最近3天' if (timeRangePreset === 'last7days') return '最近一周' - if (timeRangePreset === 'last30days') return '最近一个月' + if (timeRangePreset === 'last30days') return '最近30 天' if (timeRangePreset === 'last1year') return '最近一年' if (timeRangePreset === 'last2years') return '最近两年' if (options.dateRange) { @@ -2455,6 +2518,23 @@ function ExportPage() { }, [options.useAllTime, options.dateRange, timeRangePreset]) const activeTimeRangeDialogDraft = timeRangeDialogDraft ?? buildTimeRangeDialogDraft() + const isRangeModeActive = !activeTimeRangeDialogDraft.useAllTime + const timeRangeModeText = isRangeModeActive + ? '当前导出模式:按时间范围导出' + : '当前导出模式:全部时间导出(选择下方日期将切换为按时间范围导出)' + + useEffect(() => { + if (!isTimeRangeDialogOpen) return + setTimeRangeDateInput({ + start: formatDateInputValue(activeTimeRangeDialogDraft.dateRange.start), + end: formatDateInputValue(activeTimeRangeDialogDraft.dateRange.end) + }) + setTimeRangeDateInputError({ start: false, end: false }) + }, [ + isTimeRangeDialogOpen, + activeTimeRangeDialogDraft.dateRange.start.getTime(), + activeTimeRangeDialogDraft.dateRange.end.getTime() + ]) const isTimeRangePresetActive = useCallback((preset: DateRangePreset): boolean => { if (preset === 'all') return activeTimeRangeDialogDraft.useAllTime @@ -4696,9 +4776,8 @@ function ExportPage() { { value: 'yesterday', label: '昨天' }, { value: 'last3days', label: '最近3天' }, { value: 'last7days', label: '最近一周' }, - { value: 'last30days', label: '最近一个月' }, - { value: 'last1year', label: '最近一年' }, - { value: 'last2years', label: '最近两年' } + { value: 'last30days', label: '最近30 天' }, + { value: 'last1year', label: '最近一年' } ] as Array<{ value: Exclude; label: string }>).map((preset) => { const isActive = isTimeRangePresetActive(preset.value) return ( @@ -4706,7 +4785,7 @@ function ExportPage() { key={preset.value} type="button" className={`time-range-preset-item ${isActive ? 'active' : ''}`} - onClick={() => applyTimeRangePresetToDraft(preset.value)} + onClick={() => handleTimeRangePresetClick(preset.value)} > {preset.label} {isActive && } @@ -4715,12 +4794,36 @@ function ExportPage() { })} +
+ {timeRangeModeText} +
+
起始日期 - {formatDateInputValue(activeTimeRangeDialogDraft.dateRange.start)} + { + const nextValue = event.target.value + setTimeRangeDateInput(prev => ({ ...prev, start: nextValue })) + if (timeRangeDateInputError.start) { + setTimeRangeDateInputError(prev => ({ ...prev, start: false })) + } + }} + onKeyDown={(event) => { + if (event.key !== 'Enter') return + event.preventDefault() + commitTimeRangeStartFromInput() + }} + onBlur={() => { + commitTimeRangeStartFromInput() + }} + />
@@ -4735,7 +4838,8 @@ function ExportPage() {
{startPanelCells.map((cell) => { - const isSelected = isSameDay(cell.date, activeTimeRangeDialogDraft.dateRange.start) + const isSelected = !activeTimeRangeDialogDraft.useAllTime && + isSameDay(cell.date, activeTimeRangeDialogDraft.dateRange.start) return ( @@ -4769,7 +4893,8 @@ function ExportPage() {
{endPanelCells.map((cell) => { - const isSelected = isSameDay(cell.date, activeTimeRangeDialogDraft.dateRange.end) + const isSelected = !activeTimeRangeDialogDraft.useAllTime && + isSameDay(cell.date, activeTimeRangeDialogDraft.dateRange.end) return (
- {activeTimeRangeDialogDraft.useAllTime && ( -
- 已选择“全部时间”,确认后会按默认导出全部时间;下方日期仅用于切换为自定义范围时预览。 -
- )} -