Files
digital-pilates/app/_layout.tsx
richarjiang 7f2afdf671 feat: 增强通知功能及用户体验
- 在 Bootstrapper 组件中新增通知服务初始化逻辑,注册每日午餐提醒
- 在 CoachScreen 中优化欢迎消息生成逻辑,整合用户配置文件数据
- 更新 GoalsScreen 组件,优化目标创建时的通知设置逻辑
- 在 NotificationTest 组件中添加调试通知状态功能,提升开发便利性
- 新增 NutritionNotificationHelpers 中的午餐提醒功能,支持每日提醒设置
- 更新相关文档,详细描述新功能和使用方法
2025-08-26 09:56:23 +08:00

151 lines
5.4 KiB
TypeScript

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 { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
import { notificationService } from '@/services/notifications';
import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import { NutritionNotificationHelpers } from '@/utils/notificationHelpers';
import React from 'react';
import RNExitApp from 'react-native-exit-app';
import { DialogProvider } from '@/components/ui/DialogProvider';
import { ToastProvider } from '@/contexts/ToastContext';
import { Provider } from 'react-redux';
function Bootstrapper({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch();
const { privacyAgreed, profile } = useAppSelector((state) => state.user);
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const [userDataLoaded, setUserDataLoaded] = React.useState(false);
React.useEffect(() => {
const loadUserData = async () => {
await dispatch(rehydrateUser());
setUserDataLoaded(true);
};
const initializeBackgroundTasks = async () => {
try {
await backgroundTaskManager.initialize();
console.log('后台任务管理器初始化成功');
} catch (error) {
console.error('后台任务管理器初始化失败:', error);
}
};
const initializeNotifications = async () => {
try {
// 初始化通知服务
await notificationService.initialize();
// 只有在用户数据加载完成后且用户名存在时才注册午餐提醒
if (userDataLoaded && profile?.name) {
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name);
console.log('通知服务初始化成功,午餐提醒已注册');
}
} catch (error) {
console.error('通知服务初始化失败:', error);
}
};
loadUserData();
initializeBackgroundTasks();
initializeNotifications();
// 冷启动时清空 AI 教练会话缓存
clearAiCoachSessionCache();
}, [dispatch]);
React.useEffect(() => {
// 当用户数据加载完成后,检查是否需要显示隐私同意弹窗
if (userDataLoaded && !privacyAgreed) {
setShowPrivacyModal(true);
}
}, [userDataLoaded, privacyAgreed]);
// 当用户数据加载完成且用户名存在时,注册午餐提醒
React.useEffect(() => {
const registerLunchReminder = async () => {
if (userDataLoaded && profile?.name) {
try {
await notificationService.initialize();
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name);
console.log('午餐提醒已注册');
} catch (error) {
console.error('注册午餐提醒失败:', error);
}
}
};
registerLunchReminder();
}, [userDataLoaded, profile?.name]);
const handlePrivacyAgree = () => {
dispatch(setPrivacyAgreed());
setShowPrivacyModal(false);
};
const handlePrivacyDisagree = () => {
RNExitApp.exitApp();
};
return (
<DialogProvider>
{children}
<PrivacyConsentModal
visible={showPrivacyModal}
onAgree={handlePrivacyAgree}
onDisagree={handlePrivacyDisagree}
/>
</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="profile/goals" options={{ headerShown: false }} />
<Stack.Screen name="goals-list" 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="+not-found" />
</Stack>
<StatusBar style="dark" />
</ThemeProvider>
</ToastProvider>
</Bootstrapper>
</Provider>
</GestureHandlerRootView>
);
}