mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
feat(settings): unify export date range defaults
This commit is contained in:
341
src/utils/exportDateRange.ts
Normal file
341
src/utils/exportDateRange.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
export type ExportDateRangePreset =
|
||||
| 'all'
|
||||
| 'today'
|
||||
| 'yesterday'
|
||||
| 'last3days'
|
||||
| 'last7days'
|
||||
| 'last30days'
|
||||
| 'last1year'
|
||||
| 'last2years'
|
||||
| 'custom'
|
||||
|
||||
export type CalendarCell = { date: Date; inCurrentMonth: boolean }
|
||||
|
||||
export interface ExportDateRange {
|
||||
start: Date
|
||||
end: Date
|
||||
}
|
||||
|
||||
export interface ExportDateRangeSelection {
|
||||
preset: ExportDateRangePreset
|
||||
useAllTime: boolean
|
||||
dateRange: ExportDateRange
|
||||
}
|
||||
|
||||
export interface ExportDefaultDateRangeConfig {
|
||||
version?: 1
|
||||
preset?: ExportDateRangePreset | string
|
||||
useAllTime?: boolean
|
||||
start?: string | number | Date | null
|
||||
end?: string | number | Date | null
|
||||
}
|
||||
|
||||
export const EXPORT_DATE_RANGE_PRESETS: Array<{
|
||||
value: Exclude<ExportDateRangePreset, 'custom'>
|
||||
label: string
|
||||
}> = [
|
||||
{ value: 'all', label: '全部时间' },
|
||||
{ value: 'today', label: '今天' },
|
||||
{ value: 'yesterday', label: '昨天' },
|
||||
{ value: 'last3days', label: '最近3天' },
|
||||
{ value: 'last7days', label: '最近一周' },
|
||||
{ value: 'last30days', label: '最近30天' },
|
||||
{ value: 'last1year', label: '最近一年' }
|
||||
]
|
||||
|
||||
const PRESET_LABELS: Record<Exclude<ExportDateRangePreset, 'custom'>, string> = {
|
||||
all: '全部时间',
|
||||
today: '今天',
|
||||
yesterday: '昨天',
|
||||
last3days: '最近3天',
|
||||
last7days: '最近一周',
|
||||
last30days: '最近30天',
|
||||
last1year: '最近一年',
|
||||
last2years: '最近两年'
|
||||
}
|
||||
|
||||
const LEGACY_PRESET_MAP: Record<string, Exclude<ExportDateRangePreset, 'custom'> | 'legacy90days'> = {
|
||||
all: 'all',
|
||||
today: 'today',
|
||||
yesterday: 'yesterday',
|
||||
last3days: 'last3days',
|
||||
last7days: 'last7days',
|
||||
last30days: 'last30days',
|
||||
last1year: 'last1year',
|
||||
last2years: 'last2years',
|
||||
'7d': 'last7days',
|
||||
'30d': 'last30days',
|
||||
'90d': 'legacy90days'
|
||||
}
|
||||
|
||||
export const WEEKDAY_SHORT_LABELS = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
export const startOfDay = (date: Date): Date => {
|
||||
const next = new Date(date)
|
||||
next.setHours(0, 0, 0, 0)
|
||||
return next
|
||||
}
|
||||
|
||||
export const endOfDay = (date: Date): Date => {
|
||||
const next = new Date(date)
|
||||
next.setHours(23, 59, 59, 999)
|
||||
return next
|
||||
}
|
||||
|
||||
export const createDefaultDateRange = (): ExportDateRange => {
|
||||
const now = new Date()
|
||||
return {
|
||||
start: startOfDay(now),
|
||||
end: now
|
||||
}
|
||||
}
|
||||
|
||||
export const createDateRangeByPreset = (
|
||||
preset: Exclude<ExportDateRangePreset, 'all' | 'custom'>,
|
||||
now = new Date()
|
||||
): ExportDateRange => {
|
||||
const end = new Date(now)
|
||||
const baseStart = startOfDay(now)
|
||||
|
||||
if (preset === 'today') {
|
||||
return { start: baseStart, end }
|
||||
}
|
||||
|
||||
if (preset === 'yesterday') {
|
||||
const yesterday = new Date(baseStart)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
return {
|
||||
start: yesterday,
|
||||
end: endOfDay(yesterday)
|
||||
}
|
||||
}
|
||||
|
||||
if (preset === 'last1year' || preset === 'last2years') {
|
||||
const yearsBack = preset === 'last1year' ? 1 : 2
|
||||
const start = new Date(baseStart)
|
||||
const expectedMonth = start.getMonth()
|
||||
start.setFullYear(start.getFullYear() - yearsBack)
|
||||
if (start.getMonth() !== expectedMonth) {
|
||||
start.setDate(0)
|
||||
}
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
const daysBack = preset === 'last3days' ? 2 : preset === 'last7days' ? 6 : 29
|
||||
const start = new Date(baseStart)
|
||||
start.setDate(start.getDate() - daysBack)
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
export const createDateRangeByLastNDays = (days: number, now = new Date()): ExportDateRange => {
|
||||
const end = new Date(now)
|
||||
const start = startOfDay(now)
|
||||
start.setDate(start.getDate() - Math.max(0, days - 1))
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
export const formatDateInputValue = (date: Date): string => {
|
||||
const y = date.getFullYear()
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const d = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export const toMonthStart = (date: Date): Date => new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
|
||||
export const addMonths = (date: Date, delta: number): Date => {
|
||||
const next = new Date(date)
|
||||
next.setMonth(next.getMonth() + delta)
|
||||
return toMonthStart(next)
|
||||
}
|
||||
|
||||
export const isSameDay = (left: Date, right: Date): boolean => (
|
||||
left.getFullYear() === right.getFullYear() &&
|
||||
left.getMonth() === right.getMonth() &&
|
||||
left.getDate() === right.getDate()
|
||||
)
|
||||
|
||||
export const buildCalendarCells = (monthStart: Date): CalendarCell[] => {
|
||||
const firstDay = new Date(monthStart.getFullYear(), monthStart.getMonth(), 1)
|
||||
const startOffset = firstDay.getDay()
|
||||
const gridStart = new Date(firstDay)
|
||||
gridStart.setDate(gridStart.getDate() - startOffset)
|
||||
const cells: CalendarCell[] = []
|
||||
for (let index = 0; index < 42; index += 1) {
|
||||
const current = new Date(gridStart)
|
||||
current.setDate(gridStart.getDate() + index)
|
||||
cells.push({
|
||||
date: current,
|
||||
inCurrentMonth: current.getMonth() === monthStart.getMonth()
|
||||
})
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
export const formatCalendarMonthTitle = (date: Date): string => `${date.getFullYear()}年${date.getMonth() + 1}月`
|
||||
|
||||
export const cloneExportDateRange = (range: ExportDateRange): ExportDateRange => ({
|
||||
start: new Date(range.start),
|
||||
end: new Date(range.end)
|
||||
})
|
||||
|
||||
export const cloneExportDateRangeSelection = (selection: ExportDateRangeSelection): ExportDateRangeSelection => ({
|
||||
preset: selection.preset,
|
||||
useAllTime: selection.useAllTime,
|
||||
dateRange: cloneExportDateRange(selection.dateRange)
|
||||
})
|
||||
|
||||
export const createExportDateRangeSelectionFromPreset = (
|
||||
preset: Exclude<ExportDateRangePreset, 'custom'>,
|
||||
now = new Date()
|
||||
): ExportDateRangeSelection => {
|
||||
if (preset === 'all') {
|
||||
return {
|
||||
preset,
|
||||
useAllTime: true,
|
||||
dateRange: createDefaultDateRange()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
preset,
|
||||
useAllTime: false,
|
||||
dateRange: createDateRangeByPreset(preset, now)
|
||||
}
|
||||
}
|
||||
|
||||
export const createDefaultExportDateRangeSelection = (): ExportDateRangeSelection => (
|
||||
createExportDateRangeSelectionFromPreset('today')
|
||||
)
|
||||
|
||||
const parseStoredDate = (value: unknown): Date | null => {
|
||||
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||
return new Date(value)
|
||||
}
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
const parsed = new Date(value)
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const normalized = parseDateInputValue(value)
|
||||
if (normalized) return normalized
|
||||
const parsed = new Date(value)
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizePreset = (raw: unknown): Exclude<ExportDateRangePreset, 'custom'> | 'legacy90days' | null => {
|
||||
if (typeof raw !== 'string') return null
|
||||
const normalized = LEGACY_PRESET_MAP[raw]
|
||||
return normalized ?? null
|
||||
}
|
||||
|
||||
export const resolveExportDateRangeConfig = (
|
||||
raw: ExportDefaultDateRangeConfig | string | null | undefined,
|
||||
now = new Date()
|
||||
): ExportDateRangeSelection => {
|
||||
if (!raw) {
|
||||
return createDefaultExportDateRangeSelection()
|
||||
}
|
||||
|
||||
if (typeof raw === 'string') {
|
||||
const preset = normalizePreset(raw)
|
||||
if (!preset) return createDefaultExportDateRangeSelection()
|
||||
if (preset === 'legacy90days') {
|
||||
return {
|
||||
preset: 'custom',
|
||||
useAllTime: false,
|
||||
dateRange: createDateRangeByLastNDays(90, now)
|
||||
}
|
||||
}
|
||||
return createExportDateRangeSelectionFromPreset(preset, now)
|
||||
}
|
||||
|
||||
const preset = normalizePreset(raw.preset)
|
||||
if (raw.useAllTime || preset === 'all') {
|
||||
return createExportDateRangeSelectionFromPreset('all', now)
|
||||
}
|
||||
if (preset && preset !== 'legacy90days') {
|
||||
return createExportDateRangeSelectionFromPreset(preset, now)
|
||||
}
|
||||
|
||||
if (preset === 'legacy90days') {
|
||||
return {
|
||||
preset: 'custom',
|
||||
useAllTime: false,
|
||||
dateRange: createDateRangeByLastNDays(90, now)
|
||||
}
|
||||
}
|
||||
|
||||
const parsedStart = parseStoredDate(raw.start)
|
||||
const parsedEnd = parseStoredDate(raw.end)
|
||||
if (parsedStart && parsedEnd) {
|
||||
const start = startOfDay(parsedStart)
|
||||
const end = endOfDay(parsedEnd)
|
||||
return {
|
||||
preset: 'custom',
|
||||
useAllTime: false,
|
||||
dateRange: {
|
||||
start,
|
||||
end: end < start ? endOfDay(start) : end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createDefaultExportDateRangeSelection()
|
||||
}
|
||||
|
||||
export const serializeExportDateRangeConfig = (
|
||||
selection: ExportDateRangeSelection
|
||||
): ExportDefaultDateRangeConfig => {
|
||||
if (selection.useAllTime) {
|
||||
return {
|
||||
version: 1,
|
||||
preset: 'all',
|
||||
useAllTime: true
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.preset === 'custom') {
|
||||
return {
|
||||
version: 1,
|
||||
preset: 'custom',
|
||||
useAllTime: false,
|
||||
start: formatDateInputValue(selection.dateRange.start),
|
||||
end: formatDateInputValue(selection.dateRange.end)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
preset: selection.preset,
|
||||
useAllTime: false
|
||||
}
|
||||
}
|
||||
|
||||
export const getExportDateRangeLabel = (selection: ExportDateRangeSelection): string => {
|
||||
if (selection.useAllTime) return PRESET_LABELS.all
|
||||
if (selection.preset !== 'custom') return PRESET_LABELS[selection.preset]
|
||||
return `${formatDateInputValue(selection.dateRange.start)} 至 ${formatDateInputValue(selection.dateRange.end)}`
|
||||
}
|
||||
Reference in New Issue
Block a user