export function dateKey(date: Date) { const y = date.getFullYear() const m = `${date.getMonth() + 1}`.padStart(2, '0') const d = `${date.getDate()}`.padStart(2, '0') return `${y}-${m}-${d}` } export function habitWeek(now = new Date()) { const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate()) monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7)) return Array.from({ length: 7 }, (_, index) => { const date = new Date(monday) date.setDate(monday.getDate() + index) return date }) } export function mergePage(page: T[] | { items?: T[]; next_cursor?: string | null }) { if (Array.isArray(page)) return { items: page, nextCursor: null } return { items: page.items ?? [], nextCursor: page.next_cursor ?? null } } export function isTaskView(view: string) { return view === 'tasks' || view === 'today' || view === 'upcoming' } export function defaultView() { return 'today' as const } export function quickTaskFields(view: string, activeList: string, inboxList: string, now = new Date()) { if (view !== 'today') return { list_id: activeList } const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(now) const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value return { list_id: inboxList, due_at: `${value('year')}-${value('month')}-${value('day')}T12:00:00+08:00`, } } export function isHabitComplete(kind: string | undefined, value: number | boolean | undefined, target = 1) { if (kind === 'numeric') return Number(value ?? 0) >= target return Boolean(value) } type HabitSchedule = { kind?: string cells?: Array<{ day: string; scheduled?: boolean; paused?: boolean; value?: number | boolean }> } export function isHabitScheduledToday(habit: HabitSchedule, day: string) { const cell = (habit.cells ?? []).find((item) => item.day === day) return Boolean(cell?.scheduled && !cell.paused) } export function nextHabitSwipeValue(kind: string | undefined, current: number | boolean | undefined, target = 1) { const value = Number(current ?? 0) if (kind === 'numeric') return Math.min(value + 1, target) return value > 0 ? 0 : 1 } export function previousHabitSwipeValue(kind: string | undefined, current: number | boolean | undefined) { const value = Number(current ?? 0) if (kind === 'numeric') return Math.max(0, value - 1) return 0 } export function numericHabitInputValue(input: number | string | undefined) { if (input === undefined || input === '') return null const value = Number(input) return Number.isFinite(value) && value >= 0 ? value : null } export function countdownDayText(days: number) { if (days > 0) return `还有 ${days} 天` if (days === 0) return '就是今天' return `已经 ${Math.abs(days)} 天` } export function countdownKindLabel(kind: string) { return ({ countdown: '倒数日', anniversary: '纪念日', birthday: '生日' } as Record)[kind] ?? '倒数日' } export function calendarModeLabel(mode: string) { return mode === 'lunar' ? '农历' : '公历' } export function shouldToggleRowSwipe(deltaX: number, deltaY: number) { return deltaX >= 64 && deltaX > Math.abs(deltaY) * 1.5 } export function nextTotalAfterLocalTaskAdd(total: number) { return Math.max(0, Number(total) || 0) + 1 } export function clampFabPosition(x: number, y: number, viewportWidth: number, viewportHeight: number, size = 52, margin = 14, bottomReserved = 74) { return { x: Math.min(Math.max(x, margin), Math.max(margin, viewportWidth - size - margin)), y: Math.min(Math.max(y, margin), Math.max(margin, viewportHeight - size - bottomReserved)), } } export function isFabDrag(deltaX: number, deltaY: number, threshold = 8) { return Math.hypot(deltaX, deltaY) >= threshold } export function formatApiErrorDetail(detail: unknown): string { if (typeof detail === 'string') return detail if (Array.isArray(detail)) { const messages = detail.map((item) => { if (item && typeof item === 'object') { const record = item as Record const location = Array.isArray(record.loc) ? record.loc.filter((part) => part !== 'body').join('.') : '' const message = typeof record.msg === 'string' ? record.msg : JSON.stringify(record) return location ? `${location}:${message}` : message } return String(item) }) return messages.filter(Boolean).join(';') || '请求参数有误' } if (detail && typeof detail === 'object') { const record = detail as Record if ('detail' in record) return formatApiErrorDetail(record.detail) return JSON.stringify(record) } return '请求失败' }