import { FASTING_STORAGE_KEYS } from '@/constants/Fasting'; import type { FastingSchedule } from '@/store/fastingSlice'; import AsyncStorage from '@/utils/kvStore'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; dayjs.extend(duration); dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrBefore); export type FastingPhase = 'upcoming' | 'fasting' | 'completed'; export const calculateFastingWindow = (start: Date, fastingHours: number) => { const startDate = dayjs(start).second(0).millisecond(0); const endDate = startDate.add(fastingHours, 'hour'); return { start: startDate.toDate(), end: endDate.toDate(), }; }; export const getFastingPhase = (start?: Date | null, end?: Date | null, now: Date = new Date()): FastingPhase => { if (!start || !end) { return 'completed'; } const nowJs = dayjs(now); const startJs = dayjs(start); const endJs = dayjs(end); if (nowJs.isBefore(startJs)) { return 'upcoming'; } if (nowJs.isSameOrAfter(startJs) && nowJs.isBefore(endJs)) { return 'fasting'; } return 'completed'; }; export const getPhaseLabel = (phase: FastingPhase) => { switch (phase) { case 'fasting': return '断食中'; case 'upcoming': return '可饮食'; case 'completed': return '可饮食'; default: return '可饮食'; } }; export const formatDayDescriptor = (date: Date | null | undefined, now: Date = new Date()) => { if (!date) return '--'; const target = dayjs(date); const today = dayjs(now).startOf('day'); if (target.isSame(today, 'day')) { return '今天'; } if (target.isSame(today.subtract(1, 'day'), 'day')) { return '昨天'; } if (target.isSame(today.add(1, 'day'), 'day')) { return '明天'; } return target.format('MM-DD'); }; export const formatTime = (date: Date | null | undefined) => { if (!date) return '--:--'; return dayjs(date).format('HH:mm'); }; export const formatCountdown = (target: Date | null | undefined, now: Date = new Date()) => { if (!target) return '--:--:--'; const diff = dayjs(target).diff(now); if (diff <= 0) { return '00:00:00'; } const dur = dayjs.duration(diff); const hours = String(Math.floor(dur.asHours())).padStart(2, '0'); const minutes = String(dur.minutes()).padStart(2, '0'); const seconds = String(dur.seconds()).padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; }; export const buildDisplayWindow = (start?: Date | null, end?: Date | null) => { return { startDayLabel: formatDayDescriptor(start ?? null), startTimeLabel: formatTime(start ?? null), endDayLabel: formatDayDescriptor(end ?? null), endTimeLabel: formatTime(end ?? null), }; }; export const loadActiveFastingSchedule = async (): Promise => { try { const stored = await AsyncStorage.getItem(FASTING_STORAGE_KEYS.activeSchedule); if (!stored) return null; const parsed = JSON.parse(stored) as Partial; if ( !parsed || typeof parsed.planId !== 'string' || typeof parsed.startISO !== 'string' || typeof parsed.endISO !== 'string' ) { return null; } return { planId: parsed.planId, startISO: parsed.startISO, endISO: parsed.endISO, createdAtISO: parsed.createdAtISO ?? parsed.startISO, updatedAtISO: parsed.updatedAtISO ?? parsed.endISO, origin: parsed.origin ?? 'manual', }; } catch (error) { console.warn('读取断食计划失败', error); return null; } }; export const persistActiveFastingSchedule = async (schedule: FastingSchedule | null) => { try { if (schedule) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.activeSchedule, JSON.stringify(schedule)); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.activeSchedule); } } catch (error) { console.warn('保存断食计划失败', error); } }; export const loadPreferredPlanId = async (): Promise => { try { return await AsyncStorage.getItem(FASTING_STORAGE_KEYS.preferredPlanId); } catch (error) { console.warn('加载断食首选计划ID失败', error); return null; } }; export const savePreferredPlanId = async (planId: string) => { try { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.preferredPlanId, planId); } catch (error) { console.warn('保存断食首选计划ID失败', error); } }; export type FastingNotificationIds = { preStartId?: string | null; // 开始前30分钟 startId?: string | null; // 开始时 preEndId?: string | null; // 结束前30分钟 endId?: string | null; // 结束时 }; export const getFastingNotificationsRegistered = async (): Promise => { try { const value = await AsyncStorage.getItem(FASTING_STORAGE_KEYS.notificationsRegistered); return value === 'true'; } catch (error) { console.warn('读取断食通知注册状态失败', error); return false; } }; export const setFastingNotificationsRegistered = async (registered: boolean) => { try { if (registered) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.notificationsRegistered, 'true'); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.notificationsRegistered); } } catch (error) { console.warn('更新断食通知注册状态失败', error); } }; export const loadStoredFastingNotificationIds = async (): Promise => { try { const [preStartId, startId, preEndId, endId] = await Promise.all([ AsyncStorage.getItem(FASTING_STORAGE_KEYS.preStartNotificationId), AsyncStorage.getItem(FASTING_STORAGE_KEYS.startNotificationId), AsyncStorage.getItem(FASTING_STORAGE_KEYS.preEndNotificationId), AsyncStorage.getItem(FASTING_STORAGE_KEYS.endNotificationId), ]); return { preStartId: preStartId ?? undefined, startId: startId ?? undefined, preEndId: preEndId ?? undefined, endId: endId ?? undefined, }; } catch (error) { console.warn('读取断食通知ID失败', error); return {}; } }; export const saveFastingNotificationIds = async (ids: FastingNotificationIds) => { try { // 保存开始前30分钟通知ID if (ids.preStartId) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.preStartNotificationId, ids.preStartId); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.preStartNotificationId); } // 保存开始时通知ID if (ids.startId) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.startNotificationId, ids.startId); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.startNotificationId); } // 保存结束前30分钟通知ID if (ids.preEndId) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.preEndNotificationId, ids.preEndId); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.preEndNotificationId); } // 保存结束时通知ID if (ids.endId) { await AsyncStorage.setItem(FASTING_STORAGE_KEYS.endNotificationId, ids.endId); } else { await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.endNotificationId); } } catch (error) { console.warn('保存断食通知ID失败', error); } }; export const clearFastingNotificationIds = async () => { try { await Promise.all([ AsyncStorage.removeItem(FASTING_STORAGE_KEYS.preStartNotificationId), AsyncStorage.removeItem(FASTING_STORAGE_KEYS.startNotificationId), AsyncStorage.removeItem(FASTING_STORAGE_KEYS.preEndNotificationId), AsyncStorage.removeItem(FASTING_STORAGE_KEYS.endNotificationId), ]); } catch (error) { console.warn('清除断食通知ID失败', error); } }; // 周期性断食相关的存储函数 export const loadActiveFastingCycle = async (): Promise => { try { const stored = await AsyncStorage.getItem('@fasting_active_cycle'); if (!stored) return null; const parsed = JSON.parse(stored); return parsed; } catch (error) { console.warn('读取周期性断食计划失败', error); return null; } }; export const saveActiveFastingCycle = async (cycle: any | null): Promise => { try { if (cycle) { await AsyncStorage.setItem('@fasting_active_cycle', JSON.stringify(cycle)); } else { await AsyncStorage.removeItem('@fasting_active_cycle'); } } catch (error) { console.error('保存周期性断食计划失败', error); throw new Error('保存周期性断食计划失败,请稍后重试'); } }; export const loadCurrentCycleSession = async (): Promise => { try { const stored = await AsyncStorage.getItem('@fasting_current_cycle_session'); if (!stored) return null; const parsed = JSON.parse(stored); return parsed; } catch (error) { console.warn('读取当前断食会话失败', error); return null; } }; export const saveCurrentCycleSession = async (session: any | null): Promise => { try { if (session) { await AsyncStorage.setItem('@fasting_current_cycle_session', JSON.stringify(session)); } else { await AsyncStorage.removeItem('@fasting_current_cycle_session'); } } catch (error) { console.error('保存当前断食会话失败', error); throw new Error('保存断食会话失败,请稍后重试'); } }; export const loadCycleHistory = async (): Promise => { try { const stored = await AsyncStorage.getItem('@fasting_cycle_history'); if (!stored) return []; const parsed = JSON.parse(stored); return Array.isArray(parsed) ? parsed : []; } catch (error) { console.warn('读取断食周期历史失败', error); return []; } }; export const saveCycleHistory = async (history: any[]): Promise => { try { await AsyncStorage.setItem('@fasting_cycle_history', JSON.stringify(history)); } catch (error) { console.error('保存断食周期历史失败', error); throw new Error('保存断食历史失败,请稍后重试'); } }; // 计算下一个断食周期的开始时间 export const calculateNextCycleStart = ( cycle: { startHour: number; startMinute: number }, baseDate: Date = new Date() ): Date => { const now = dayjs(baseDate); const today = now.startOf('day').hour(cycle.startHour).minute(cycle.startMinute).second(0).millisecond(0); // 如果今天的开始时间已过,则从明天开始 if (today.isBefore(now)) { return today.add(1, 'day').toDate(); } return today.toDate(); }; // 获取周期性断食的统计信息 export const getCycleStats = (history: any[]) => { const completedCycles = history.filter(session => session.completed); const totalCycles = history.length; const currentStreak = calculateCurrentStreak(history); const longestStreak = calculateLongestStreak(history); return { totalCycles, completedCycles: completedCycles.length, completionRate: totalCycles > 0 ? (completedCycles.length / totalCycles) * 100 : 0, currentStreak, longestStreak, }; }; // 计算当前连续完成天数 const calculateCurrentStreak = (history: any[]): number => { if (history.length === 0) return 0; let streak = 0; const today = dayjs().startOf('day'); for (let i = 0; i < history.length; i++) { const session = history[i]; if (!session.completed) break; const sessionDate = dayjs(session.cycleDate); const expectedDate = today.subtract(i, 'day'); if (sessionDate.isSame(expectedDate, 'day')) { streak++; } else { break; } } return streak; }; // 计算最长连续完成天数 const calculateLongestStreak = (history: any[]): number => { if (history.length === 0) return 0; let longestStreak = 0; let currentStreak = 0; for (const session of history) { if (session.completed) { currentStreak++; longestStreak = Math.max(longestStreak, currentStreak); } else { currentStreak = 0; } } return longestStreak; };