Files
dodo/frontend/src/lib/mvp-utils.ts
T
2026-09-07 10:52:20 +08:00

135 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T>(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<string, string>)[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<string, unknown>
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<string, unknown>
if ('detail' in record) return formatApiErrorDetail(record.detail)
return JSON.stringify(record)
}
return '请求失败'
}