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 };
|
||||
Reference in New Issue
Block a user