feat(fasting): 添加周期性断食计划功能
实现完整的周期性断食计划系统,支持每日自动续订和通知管理: - 新增周期性断食状态管理(activeCycle、currentCycleSession、cycleHistory) - 实现周期性断食会话的自动完成和续订逻辑 - 添加独立的周期性断食通知系统,避免与单次断食通知冲突 - 支持暂停/恢复周期性断食计划 - 添加周期性断食数据持久化和水合功能 - 优化断食界面,优先显示周期性断食信息 - 新增空状态引导界面,提升用户体验 - 保持单次断食功能向后兼容
This commit is contained in:
150
utils/fasting.ts
150
utils/fasting.ts
@@ -255,3 +255,153 @@ export const clearFastingNotificationIds = async () => {
|
||||
console.warn('清除断食通知ID失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 周期性断食相关的存储函数
|
||||
export const loadActiveFastingCycle = async (): Promise<any | null> => {
|
||||
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<void> => {
|
||||
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<any | null> => {
|
||||
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<void> => {
|
||||
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<any[]> => {
|
||||
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<void> => {
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user