Files
digital-pilates/app/_layout.tsx
richarjiang 39671ed70f feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等
- 实现挑战邀请码系统,支持通过邀请码加入自定义挑战
- 完善挑战详情页面的多语言翻译支持
- 优化用户认证状态检查逻辑,使用token作为主要判断依据
- 添加阿里字体文件支持,提升UI显示效果
- 改进确认弹窗组件,支持Liquid Glass效果和自定义内容
- 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
2025-11-26 16:39:01 +08:00

557 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '@/i18n';
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack, useRouter } 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 { hrvMonitorService } from '@/services/hrvMonitor';
import { cleanupLegacyMedicationNotifications } from '@/services/medicationNotificationCleanup';
import { clearBadgeCount, notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { sleepMonitorService } from '@/services/sleepMonitor';
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, logout, setPrivacyAgreed } from '@/store/userSlice';
import { createWaterRecordAction } from '@/store/waterSlice';
import { loadActiveFastingSchedule } from '@/utils/fasting';
import { initializeHealthPermissions } from '@/utils/health';
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getMoodReminderEnabled, getNutritionReminderEnabled, getWaterReminderSettings } from '@/utils/userPreferences';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React, { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { DialogProvider } from '@/components/ui/DialogProvider';
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
import { ToastProvider } from '@/contexts/ToastContext';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { STORAGE_KEYS, setUnauthorizedHandler } from '@/services/api';
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
import { fetchChallenges } from '@/store/challengesSlice';
import { loadTabBarConfigs } from '@/store/tabBarConfigSlice';
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 router = useRouter();
const { profile, onboardingCompleted } = useAppSelector((state) => state.user);
const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule);
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const { isLoggedIn } = useAuthGuard()
const fastingHydrationRequestedRef = React.useRef(false);
const permissionInitializedRef = React.useRef(false);
// 初始化快捷动作处理
useQuickActions();
// 注册401未授权处理器应用启动时执行一次
React.useEffect(() => {
const handle401 = async () => {
try {
logger.info('[401处理] 开始处理登录过期');
// 清除Redux状态
await dispatch(logout());
// 跳转到登录页
router.push('/auth/login');
logger.info('[401处理] 登录过期处理完成');
} catch (error) {
logger.error('[401处理] 处理失败:', error);
}
};
setUnauthorizedHandler(handle401);
logger.info('[401处理器] 已注册到API服务');
}, [dispatch, router]);
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]);
// 初始化底部栏配置
useEffect(() => {
dispatch(loadTabBarConfigs());
}, [dispatch]);
// ==================== 基础服务初始化(不需要权限,总是执行)====================
React.useEffect(() => {
const initializeBasicServices = async () => {
try {
logger.info('🚀 开始初始化基础服务(不需要权限)...');
if (isLoggedIn) {
// 1. 加载用户数据(首屏展示需要)
await dispatch(fetchMyProfile());
logger.info('✅ 用户数据加载完成');
}
// 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化)
initializeHealthPermissions();
logger.info('✅ HealthKit 权限系统初始化完成');
// 3. 初始化快捷动作(用户可能立即使用)
await setupQuickActions();
logger.info('✅ 快捷动作初始化完成');
// 5. 初始化喝水记录 Bridge
initializeWaterRecordBridge();
logger.info('✅ 喝水记录 Bridge 初始化完成');
logger.info('🎉 基础服务初始化完成');
} catch (error) {
logger.error('❌ 基础服务初始化失败:', error);
}
};
initializeBasicServices();
}, [dispatch]);
// ==================== 应用状态监听 - 进入前台时清除角标 ====================
React.useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
// 应用进入前台时清除角标
clearBadgeCount();
}
});
return () => {
subscription.remove();
};
}, []);
// ==================== 权限相关服务初始化(应用启动时执行)====================
React.useEffect(() => {
// 如果已经初始化过,则跳过(确保只初始化一次)
if (permissionInitializedRef.current) {
return;
}
permissionInitializedRef.current = true;
// ==================== 辅助函数 ====================
// 异步同步 Widget 数据(不阻塞主流程)
const syncWidgetDataInBackground = async () => {
try {
const widgetSync = await syncPendingWidgetChanges();
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
logger.info(`🔄 检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
// 异步处理每条记录
for (const record of widgetSync.pendingRecords) {
try {
await store.dispatch(createWaterRecordAction({
amount: record.amount,
recordedAt: record.recordedAt,
source: WaterRecordSource.Auto,
})).unwrap();
logger.info(`✅ 成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
} catch (error) {
logger.error('❌ 同步水记录失败:', error);
}
}
// 清除已同步的记录
await clearPendingWaterRecords();
logger.info('✅ 所有待同步的水记录已处理完成');
}
} catch (error) {
logger.error('❌ Widget 数据同步失败:', error);
}
};
// 批量注册所有通知提醒
const registerAllNotifications = async () => {
try {
logger.info('📢 开始批量注册通知提醒...');
// 获取用户偏好设置
const [nutritionReminderEnabled, moodReminderEnabled, waterSettings] = await Promise.all([
getNutritionReminderEnabled(),
getMoodReminderEnabled(),
getWaterReminderSettings(),
]);
// 准备所有通知注册任务
const notificationTasks = [];
// 营养提醒 - 根据用户设置决定是否注册
if (nutritionReminderEnabled) {
notificationTasks.push(
NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() =>
logger.info('✅ 午餐提醒已注册')
),
NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() =>
logger.info('✅ 晚餐提醒已注册')
)
);
} else {
logger.info(' 用户未开启营养提醒,跳过注册');
}
// 心情提醒 - 根据用户设置决定是否注册
if (moodReminderEnabled) {
notificationTasks.push(
MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() =>
logger.info('✅ 心情提醒已注册')
)
);
} else {
logger.info(' 用户未开启心情提醒,跳过注册');
}
// 喝水提醒 - 根据用户设置决定是否注册
if (waterSettings.enabled) {
notificationTasks.push(
WaterNotificationHelpers.scheduleCustomWaterReminders(profile.name || '用户', waterSettings).then(() =>
logger.info('✅ 自定义喝水提醒已注册')
)
);
} else {
logger.info(' 用户未开启喝水提醒,跳过注册');
}
// 并行执行所有通知注册任务
if (notificationTasks.length > 0) {
await Promise.all(notificationTasks);
}
// 检查断食通知(如果有活跃计划)
const fastingSchedule = store.getState().fasting.activeSchedule;
if (fastingSchedule) {
logger.info('✅ 检测到活跃的断食计划,将通过页面 hook 自动安排通知');
}
logger.info('🎉 所有通知提醒注册完成');
} catch (error) {
logger.error('❌ 通知提醒注册失败:', error);
}
};
// 初始化后台任务管理器
const initializeBackgroundTaskManager = async () => {
try {
logger.info('⚙️ 初始化后台任务管理器...');
await BackgroundTaskManager.getInstance().initialize();
logger.info('✅ 后台任务管理器初始化成功');
// 简单的任务调度检查
const taskManager = BackgroundTaskManager.getInstance();
const status = await taskManager.getStatus();
if (status === 'available') {
const pendingRequests = await taskManager.getPendingRequests();
if (pendingRequests.length === 0) {
await taskManager.scheduleNextTask();
logger.info('✅ 已调度新的后台任务');
}
}
} catch (error) {
logger.error('❌ 后台任务管理器初始化失败:', error);
}
};
// 初始化健康监听服务(锻炼 + 睡眠)
const initializeHealthMonitoring = async () => {
try {
logger.info('💪 初始化健康监听服务...');
const [workoutResult, sleepResult, hrvResult] = await Promise.allSettled([
workoutMonitorService.initialize(),
sleepMonitorService.initialize(),
hrvMonitorService.initialize(),
]);
const workoutReady = workoutResult.status === 'fulfilled';
if (workoutReady) {
logger.info('✅ 锻炼监听服务初始化成功');
} else {
logger.error('❌ 锻炼监听服务初始化失败:', workoutResult.reason);
}
const sleepReady = sleepResult.status === 'fulfilled';
if (sleepReady) {
logger.info('✅ 睡眠监听服务初始化成功');
} else {
logger.error('❌ 睡眠监听服务初始化失败:', sleepResult.reason);
}
const hrvReady = hrvResult.status === 'fulfilled';
if (hrvReady) {
logger.info('✅ HRV 监听服务初始化成功');
} else {
logger.error('❌ HRV 监听服务初始化失败:', hrvResult.reason);
}
if (workoutReady && sleepReady && hrvReady) {
logger.info('🎉 健康监听服务初始化完成');
} else {
logger.warn('⚠️ 健康监听服务部分未能初始化成功,请检查上述错误日志');
}
return workoutReady && sleepReady && hrvReady;
} catch (error) {
logger.error('❌ 健康监听服务初始化失败:', error);
return false;
}
};
// 后台任务详细状态检查(空闲时执行)
const checkBackgroundTaskStatus = async () => {
try {
logger.info('🔍 检查后台任务详细状态...');
const taskManager = BackgroundTaskManager.getInstance();
const status = await taskManager.getStatus();
const statusText = await taskManager.checkStatus();
logger.info(`📊 后台任务状态: ${status} (${statusText})`);
// 检查上次执行时间
const lastCheckTime = await taskManager.getLastBackgroundCheckTime();
if (lastCheckTime) {
const timeSinceLastCheck = Date.now() - lastCheckTime;
const hoursSinceLastCheck = timeSinceLastCheck / (1000 * 60 * 60);
logger.info(`⏱️ 上次执行: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`);
if (hoursSinceLastCheck > 24) {
logger.warn('⚠️ 超过24小时未执行后台任务请检查系统设置');
}
}
logger.info('✅ 后台任务状态检查完成');
} catch (error) {
logger.error('❌ 后台任务状态检查失败:', error);
}
};
// 权限服务初始化
const initializePermissionServices = async () => {
try {
logger.info('🔐 开始初始化需要权限的服务...');
// 1. 初始化通知服务(包含权限请求)
await notificationService.initialize();
logger.info('✅ 通知服务初始化完成');
// 2. 清理旧的药品本地通知(迁移到服务端推送)
cleanupLegacyMedicationNotifications().catch(error => {
logger.error('❌ 清理旧药品通知失败:', error);
});
// 3. 异步同步 Widget 数据(不阻塞主流程)
syncWidgetDataInBackground();
logger.info('🎉 权限相关服务初始化完成');
logger.info('💡 HealthKit 权限将在用户首次访问健康数据时请求');
} catch (error) {
logger.error('❌ 权限相关服务初始化失败:', error);
throw error;
}
};
// ==================== 后台服务初始化(延迟执行)====================
const initializeBackgroundServices = () => {
const { InteractionManager } = require('react-native');
InteractionManager.runAfterInteractions(() => {
setTimeout(async () => {
try {
logger.info('📅 开始初始化后台服务...');
// 1. 批量注册所有通知提醒
await registerAllNotifications();
// 2. 初始化后台任务管理器
await initializeBackgroundTaskManager();
// 3. 初始化健康监听服务
await initializeHealthMonitoring();
logger.info('🎉 后台服务初始化完成');
} catch (error) {
logger.error('❌ 后台服务初始化失败:', error);
}
}, 3000);
});
};
// ==================== 空闲服务初始化====================
const initializeIdleServices = () => {
setTimeout(async () => {
try {
logger.info('🔄 开始初始化空闲服务...');
// 1. 后台任务详细状态检查
await checkBackgroundTaskStatus();
// 2. 开发环境调试工具
if (__DEV__ && BackgroundTaskDebugger) {
logger.info('✅ 后台任务调试工具未初始化(开发环境)');
return
}
logger.info('🎉 空闲服务初始化完成');
} catch (error) {
logger.error('❌ 空闲服务初始化失败:', error);
}
}, 8000);
};
const runInitializationSequence = async () => {
try {
await initializePermissionServices();
} catch {
logger.warn('⚠️ 权限相关服务初始化失败,将继续启动后台和空闲服务以便后续重试');
}
// 交互完成后执行后台服务
initializeBackgroundServices();
// 空闲时执行非关键服务
initializeIdleServices();
};
runInitializationSequence();
}, []); // 每次应用启动都执行,不依赖其他状态
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'),
AliRegular: require('../assets/fonts/ali-regular.ttf'),
AliBold: require('../assets/fonts/ali-bold.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="onboarding" />
<Stack.Screen name="(tabs)" />
<Stack.Screen name="profile/edit" />
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
<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="workout/notification-settings" options={{ headerShown: false }} />
<Stack.Screen
name="health-data-permissions"
options={{ headerShown: false }}
/>
<Stack.Screen name="medications/ai-camera" options={{ headerShown: false }} />
<Stack.Screen name="medications/ai-progress" options={{ headerShown: false }} />
<Stack.Screen name="badges/index" options={{ headerShown: false }} />
<Stack.Screen name="settings/tab-bar-config" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="dark" />
</ThemeProvider>
</ToastProvider>
</Bootstrapper>
</Provider>
</GestureHandlerRootView>
);
}