Files
digital-pilates/app/_layout.tsx
richarjiang 635d835a50 feat(fasting): 完善断食通知系统并优化错误提示
在应用启动时添加断食通知初始化逻辑,改进错误消息提示,并新增后台任务支持断食通知同步。同时优化挑战加入后的数据刷新流程和会员卡片显示样式。

主要更改:
- 添加断食通知启动检测和初始化
- 改进断食通知错误消息,提供更详细的用户指导
- 新增断食通知后台任务处理
- 优化挑战加入后自动刷新详情和排名数据
- 调整会员价格字体大小以提升视觉效果
2025-11-03 14:13:49 +08:00

303 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import 'react-native-reanimated';
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useQuickActions } from '@/hooks/useQuickActions';
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
import { WaterRecordSource } from '@/services/waterRecords';
import { workoutMonitorService } from '@/services/workoutMonitor';
import { store } from '@/store';
import { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
import { createWaterRecordAction } from '@/store/waterSlice';
import { loadActiveFastingSchedule } from '@/utils/fasting';
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React, { useEffect } from 'react';
import { DialogProvider } from '@/components/ui/DialogProvider';
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
import { ToastProvider } from '@/contexts/ToastContext';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { STORAGE_KEYS } from '@/services/api';
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
import { fetchChallenges } from '@/store/challengesSlice';
import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
import { Provider } from 'react-redux';
// 在开发环境中导入调试工具
let BackgroundTaskDebugger: any = null;
if (__DEV__) {
try {
const debuggerModule = require('@/services/backgroundTaskDebugger');
BackgroundTaskDebugger = debuggerModule.BackgroundTaskDebugger;
} catch (error) {
logger.warn('无法导入后台任务调试工具:', error);
}
}
function Bootstrapper({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch();
const { profile } = useAppSelector((state) => state.user);
const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule);
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const { isLoggedIn } = useAuthGuard()
const fastingHydrationRequestedRef = React.useRef(false);
// 初始化快捷动作处理
useQuickActions();
React.useEffect(() => {
if (fastingHydrationRequestedRef.current) return;
if (activeFastingSchedule) {
fastingHydrationRequestedRef.current = true;
return;
}
fastingHydrationRequestedRef.current = true;
let cancelled = false;
const hydrate = async () => {
try {
const stored = await loadActiveFastingSchedule();
if (cancelled || !stored) return;
if (store.getState().fasting.activeSchedule) return;
dispatch(hydrateActiveSchedule(stored));
} catch (error) {
logger.warn('恢复断食计划失败:', error);
}
};
hydrate();
return () => {
cancelled = true;
};
}, [dispatch, activeFastingSchedule]);
useEffect(() => {
if (isLoggedIn) {
dispatch(fetchChallenges());
}
}, [isLoggedIn]);
React.useEffect(() => {
const loadUserData = async () => {
// 数据已经在启动界面预加载,这里只需要快速同步到 Redux 状态
await dispatch(fetchMyProfile());
};
const initHealthPermissions = async () => {
// 初始化 HealthKit 权限管理系统
try {
logger.info('初始化 HealthKit 权限管理系统...');
initializeHealthPermissions();
// 延迟请求权限,避免应用启动时弹窗
setTimeout(async () => {
try {
await ensureHealthPermissions();
logger.info('HealthKit 权限请求完成');
} catch (error) {
logger.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error);
}
}, 2000);
logger.info('HealthKit 权限管理初始化完成');
} catch (error) {
logger.warn('HealthKit 权限管理初始化失败:', error);
}
}
const initializeNotifications = async () => {
try {
try {
await BackgroundTaskManager.getInstance().initialize();
// 在开发环境中初始化调试工具
if (__DEV__) {
BackgroundTaskDebugger.getInstance().initialize();
logger.info('后台任务调试工具已初始化(开发环境)');
}
} catch (backgroundError) {
logger.error('后台任务管理器初始化失败,将跳过后台任务:', backgroundError);
}
// 初始化通知服务
await notificationService.initialize();
logger.info('通知服务初始化成功');
// 注册午餐提醒12:00
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
logger.info('午餐提醒已注册');
// 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
logger.info('晚餐提醒已注册');
// 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
logger.info('心情提醒已注册');
// 注册默认喝水提醒9:00-21:00每2小时一次
await WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户');
logger.info('默认喝水提醒已注册');
// 安排断食通知(如果存在活跃的断食计划)
try {
const fastingSchedule = store.getState().fasting.activeSchedule;
if (fastingSchedule) {
const fastingPlan = store.getState().fasting.activeSchedule ? null : null;
// 断食通知将通过 useFastingNotifications hook 在页面加载时自动安排
logger.info('检测到活跃的断食计划,将通过页面 hook 自动安排通知');
}
} catch (error) {
logger.warn('安排断食通知失败:', error);
}
// 初始化快捷动作
await setupQuickActions();
logger.info('快捷动作初始化成功');
// 初始化喝水记录 bridge
initializeWaterRecordBridge();
logger.info('喝水记录 Bridge 初始化成功');
// 初始化锻炼监听服务
const initializeWorkoutMonitoring = async () => {
try {
await workoutMonitorService.initialize();
logger.info('锻炼监听服务初始化成功');
} catch (error) {
logger.warn('锻炼监听服务初始化失败:', error);
}
};
initializeWorkoutMonitoring();
// 检查并同步Widget数据更改
const widgetSync = await syncPendingWidgetChanges();
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
logger.info(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
// 将待同步的记录添加到 Redux store
for (const record of widgetSync.pendingRecords) {
try {
await store.dispatch(createWaterRecordAction({
amount: record.amount,
recordedAt: record.recordedAt,
source: WaterRecordSource.Auto, // 标记为自动添加来自Widget
})).unwrap();
logger.info(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
} catch (error) {
logger.error('同步水记录失败:', error);
}
}
// 清除已同步的记录
await clearPendingWaterRecords();
logger.info('所有待同步的水记录已处理完成');
}
} catch (error) {
logger.error('通知服务、后台任务管理器或快捷动作初始化失败:', error);
}
};
loadUserData();
initHealthPermissions();
initializeNotifications();
// 冷启动时清空 AI 教练会话缓存
clearAiCoachSessionCache();
}, [dispatch]);
React.useEffect(() => {
const getPrivacyAgreed = async () => {
const str = await AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed)
setShowPrivacyModal(str !== 'true');
}
getPrivacyAgreed();
}, []);
const handlePrivacyAgree = () => {
dispatch(setPrivacyAgreed());
setShowPrivacyModal(false);
};
const handlePrivacyDisagree = () => {
// RNExitApp.exitApp();
};
return (
<DialogProvider>
<MembershipModalProvider>
{children}
<PrivacyConsentModal
visible={showPrivacyModal}
onAgree={handlePrivacyAgree}
onDisagree={handlePrivacyDisagree}
/>
</MembershipModalProvider>
</DialogProvider>
);
}
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
if (!loaded) {
// Async font loading only occurs in development.
return null;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Provider store={store}>
<Bootstrapper>
<ToastProvider>
<ThemeProvider value={DefaultTheme}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="challenge" options={{ headerShown: false }} />
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
<Stack.Screen name="workout" options={{ headerShown: false }} />
<Stack.Screen name="profile/edit" />
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
<Stack.Screen name="ai-posture-assessment" />
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="dark" />
</ThemeProvider>
</ToastProvider>
</Bootstrapper>
</Provider>
</GestureHandlerRootView>
);
}