feat(fasting): 新增轻断食功能模块

新增完整的轻断食功能,包括:
- 断食计划列表和详情页面,支持12-12、14-10、16-8、18-6四种计划
- 断食状态实时追踪和倒计时显示
- 自定义开始时间选择器
- 断食通知提醒功能
- Redux状态管理和数据持久化
- 新增tab导航入口和路由配置
This commit is contained in:
richarjiang
2025-10-13 19:21:29 +08:00
parent 971aebd560
commit e03b2b3032
17 changed files with 2390 additions and 7 deletions

View File

@@ -0,0 +1,162 @@
import dayjs from 'dayjs';
import { FastingPlan } from '@/constants/Fasting';
import { FastingSchedule } from '@/store/fastingSlice';
import {
clearFastingNotificationIds,
getFastingNotificationsRegistered,
loadStoredFastingNotificationIds,
saveFastingNotificationIds,
setFastingNotificationsRegistered,
FastingNotificationIds,
} from '@/utils/fasting';
import { getNotificationEnabled } from '@/utils/userPreferences';
import { notificationService, NotificationTypes } from './notifications';
const REMINDER_OFFSET_MINUTES = 10;
const cancelNotificationIds = async (ids?: FastingNotificationIds) => {
if (!ids) return;
const { startId, endId } = ids;
try {
if (startId) {
await notificationService.cancelNotification(startId);
}
} catch (error) {
console.warn('取消断食开始提醒失败', error);
}
try {
if (endId) {
await notificationService.cancelNotification(endId);
}
} catch (error) {
console.warn('取消断食结束提醒失败', error);
}
};
export const ensureFastingNotificationsReady = async (): Promise<boolean> => {
try {
const notificationsEnabled = await getNotificationEnabled();
if (!notificationsEnabled) {
return false;
}
const registered = await getFastingNotificationsRegistered();
if (registered) {
return true;
}
const status = await notificationService.getPermissionStatus();
if (status !== 'granted') {
const requestStatus = await notificationService.requestPermission();
if (requestStatus !== 'granted') {
return false;
}
}
await setFastingNotificationsRegistered(true);
return true;
} catch (error) {
console.warn('初始化断食通知失败', error);
return false;
}
};
type ResyncOptions = {
schedule: FastingSchedule | null;
plan?: FastingPlan;
previousIds?: FastingNotificationIds;
enabled: boolean;
};
export const resyncFastingNotifications = async ({
schedule,
plan,
previousIds,
enabled,
}: ResyncOptions): Promise<FastingNotificationIds> => {
const storedIds = previousIds ?? (await loadStoredFastingNotificationIds());
await cancelNotificationIds(storedIds);
if (!enabled) {
await setFastingNotificationsRegistered(false);
await clearFastingNotificationIds();
return {};
}
const preferenceEnabled = await getNotificationEnabled();
if (!preferenceEnabled) {
await setFastingNotificationsRegistered(false);
await clearFastingNotificationIds();
return {};
}
if (!schedule || !plan) {
await clearFastingNotificationIds();
return {};
}
const now = dayjs();
const start = dayjs(schedule.startISO);
const end = dayjs(schedule.endISO);
if (end.isBefore(now)) {
await clearFastingNotificationIds();
return {};
}
const notificationIds: FastingNotificationIds = {};
if (start.isAfter(now)) {
const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute');
const triggerMoment = preStart.isAfter(now) ? preStart : start;
try {
const startId = await notificationService.scheduleNotificationAtDate(
{
title: `${plan.title} 即将开始`,
body: preStart.isAfter(now)
? `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要进入断食窗口,喝一杯温水,准备迎接更轻盈的自己!`
: `现在开始 ${plan.title},放下零食,给身体一次真正的休息时间。`,
data: {
type: NotificationTypes.FASTING_START,
planId: plan.id,
},
sound: true,
priority: 'high',
},
triggerMoment.toDate()
);
notificationIds.startId = startId;
} catch (error) {
console.error('安排断食开始通知失败', error);
}
}
if (end.isAfter(now)) {
try {
const endId = await notificationService.scheduleNotificationAtDate(
{
title: '补能时刻到啦',
body: `${plan.title} 已完成!用一顿高蛋白 + 低GI 的餐食奖励自己,让代谢持续高效运转。`,
data: {
type: NotificationTypes.FASTING_END,
planId: plan.id,
},
sound: true,
priority: 'high',
},
end.toDate()
);
notificationIds.endId = endId;
} catch (error) {
console.error('安排断食结束通知失败', error);
}
}
await saveFastingNotificationIds(notificationIds);
return notificationIds;
};
export type { FastingNotificationIds };