135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
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 '请求失败'
|
||
}
|