feat(fasting): 新增轻断食功能模块
新增完整的轻断食功能,包括: - 断食计划列表和详情页面,支持12-12、14-10、16-8、18-6四种计划 - 断食状态实时追踪和倒计时显示 - 自定义开始时间选择器 - 断食通知提醒功能 - Redux状态管理和数据持久化 - 新增tab导航入口和路由配置
This commit is contained in:
162
services/fastingNotifications.ts
Normal file
162
services/fastingNotifications.ts
Normal 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 };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { getNotificationEnabled } from '@/utils/userPreferences';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { router } from 'expo-router';
|
||||
@@ -5,7 +6,6 @@ import { router } from 'expo-router';
|
||||
// 配置通知处理方式
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: true,
|
||||
@@ -179,6 +179,8 @@ export class NotificationService {
|
||||
if (data?.url) {
|
||||
router.push(data.url as any);
|
||||
}
|
||||
} else if (data?.type === NotificationTypes.FASTING_START || data?.type === NotificationTypes.FASTING_END) {
|
||||
router.push(ROUTES.TAB_FASTING as any);
|
||||
} else if (data?.type === NotificationTypes.WORKOUT_COMPLETION) {
|
||||
// 处理锻炼完成通知
|
||||
console.log('用户点击了锻炼完成通知', data);
|
||||
@@ -517,6 +519,8 @@ export const NotificationTypes = {
|
||||
REGULAR_WATER_REMINDER: 'regular_water_reminder',
|
||||
CHALLENGE_ENCOURAGEMENT: 'challenge_encouragement',
|
||||
WORKOUT_COMPLETION: 'workout_completion',
|
||||
FASTING_START: 'fasting_start',
|
||||
FASTING_END: 'fasting_end',
|
||||
} as const;
|
||||
|
||||
// 便捷方法
|
||||
|
||||
Reference in New Issue
Block a user