Files
frontend/src/navigation/MobileTabNavigatorWithDelay.tsx

556 lines
15 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.
/**
* 移动端 Tab Navigator完全独立版本
*
* 这个组件完全独立于 MainNavigator.tsx 中的其他导航组件,
* 用于解决从大屏切换到小屏时的白屏问题。
*
* 问题根源:
* - React Navigation 的 TabNavigator 在初始化时需要完整的 navigation state
* - 从大屏Sidebar切换到小屏BottomTabstate 可能还没有准备好
* - 导致 TabRouter 报错:"Cannot read properties of undefined (reading 'filter')"
*
* 解决方案:
* - 完全独立的组件,不依赖外部 navigation state
* - 使用延迟初始化,确保 React Navigation 内部状态就绪
* - 错误边界和重试机制
*/
import React, { useEffect, useState, useRef, useCallback } from 'react';
import {
View,
ActivityIndicator,
StyleSheet,
AppState,
Platform,
} from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, shadows } from '../theme';
import { useTotalUnreadCount } from '../stores';
import { messageManager } from '../stores';
// ==================== 导入屏幕组件 ====================
import { HomeScreen, PostDetailScreen, SearchScreen } from '../screens/home';
import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule';
import {
MessageListScreen,
ChatScreen,
NotificationsScreen,
CreateGroupScreen,
JoinGroupScreen,
GroupRequestDetailScreen,
GroupInviteDetailScreen,
GroupInfoScreen,
GroupMembersScreen,
PrivateChatInfoScreen
} from '../screens/message';
import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile';
import { CreatePostScreen } from '../screens/create';
import { UserScreen } from '../screens/profile';
import FollowListScreen from '../screens/profile/FollowListScreen';
// ==================== 类型定义 ====================
export type MainTabParamList = {
HomeTab: undefined;
MessageTab: undefined;
ScheduleTab: undefined;
ProfileTab: undefined;
};
export type HomeStackParamList = {
Home: undefined;
Search: undefined;
CreatePost: undefined;
};
export type MessageStackParamList = {
MessageList: undefined;
Notifications: undefined;
PrivateChatInfo: { conversationId: string; userId: string };
};
export type ScheduleStackParamList = {
Schedule: undefined;
CourseDetail: { courseId: string };
};
export type ProfileStackParamList = {
Profile: undefined;
Settings: undefined;
EditProfile: undefined;
AccountSecurity: undefined;
MyPosts: undefined;
Bookmarks: undefined;
NotificationSettings: undefined;
BlockedUsers: undefined;
};
// ==================== Stack Navigators ====================
const HomeStack = createNativeStackNavigator<HomeStackParamList>();
const MessageStack = createNativeStackNavigator<MessageStackParamList>();
const ScheduleStack = createNativeStackNavigator<ScheduleStackParamList>();
const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();
const Tab = createBottomTabNavigator<MainTabParamList>();
// ==================== 常量 ====================
const MOBILE_TAB_FLOATING_MARGIN = 12;
const INITIALIZATION_DELAY = 500; // ms增加延迟时间确保 React Navigation 完全初始化
const MAX_RETRY_ATTEMPTS = 5;
const RETRY_DELAY = 500; // ms
// ==================== Stack Navigator 组件 ====================
function HomeStackNavigatorComponent() {
return (
<HomeStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.background.paper,
},
headerTintColor: colors.text.primary,
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitle: '',
headerShadowVisible: false,
}}
>
<HomeStack.Screen
name="Home"
component={HomeScreen}
options={{
title: '首页',
headerShown: false,
}}
/>
<HomeStack.Screen
name="Search"
component={SearchScreen}
options={{
title: '搜索',
headerShown: false,
}}
/>
<HomeStack.Screen
name="CreatePost"
component={CreatePostScreen}
options={{
title: '发帖',
headerShown: false,
presentation: 'modal',
}}
/>
</HomeStack.Navigator>
);
}
function MessageStackNavigatorComponent() {
return (
<MessageStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.background.paper,
},
headerTintColor: colors.text.primary,
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitle: '',
headerShadowVisible: false,
}}
>
<MessageStack.Screen
name="MessageList"
component={MessageListScreen}
options={{
title: '消息',
headerShown: false,
}}
/>
<MessageStack.Screen
name="Notifications"
component={NotificationsScreen}
options={{
title: '通知',
}}
/>
<MessageStack.Screen
name="PrivateChatInfo"
component={PrivateChatInfoScreen}
options={{
title: '聊天信息',
}}
/>
</MessageStack.Navigator>
);
}
function ScheduleStackNavigatorComponent() {
return (
<ScheduleStack.Navigator
screenOptions={{
headerShown: false,
}}
>
<ScheduleStack.Screen
name="Schedule"
component={ScheduleScreen}
options={{
title: '课表',
}}
/>
<ScheduleStack.Screen
name="CourseDetail"
component={CourseDetailScreen}
options={{
headerShown: false,
presentation: 'transparentModal',
animation: 'fade',
}}
/>
</ScheduleStack.Navigator>
);
}
function ProfileStackNavigatorComponent() {
return (
<ProfileStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.background.paper,
},
headerTintColor: colors.text.primary,
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitle: '',
headerShadowVisible: false,
}}
>
<ProfileStack.Screen
name="Profile"
component={ProfileScreen}
options={{
headerShown: false,
}}
/>
<ProfileStack.Screen
name="Settings"
component={SettingsScreen}
options={{
title: '设置',
}}
/>
<ProfileStack.Screen
name="EditProfile"
component={EditProfileScreen}
options={{
title: '编辑资料',
}}
/>
<ProfileStack.Screen
name="AccountSecurity"
component={AccountSecurityScreen}
options={{
title: '账号安全',
}}
/>
<ProfileStack.Screen
name="MyPosts"
component={ProfileScreen}
options={{
title: '我的帖子',
}}
/>
<ProfileStack.Screen
name="Bookmarks"
component={ProfileScreen}
options={{
title: '收藏',
}}
/>
<ProfileStack.Screen
name="NotificationSettings"
component={NotificationSettingsScreen}
options={{
title: '通知设置',
}}
/>
<ProfileStack.Screen
name="BlockedUsers"
component={BlockedUsersScreen}
options={{
title: '黑名单',
}}
/>
</ProfileStack.Navigator>
);
}
// ==================== 主组件MobileTabNavigatorWithDelay ====================
export function MobileTabNavigatorWithDelay() {
const insets = useSafeAreaInsets();
const messageUnreadCount = useTotalUnreadCount();
const [isReady, setIsReady] = useState(false);
const [hasError, setHasError] = useState(false);
const initAttemptRef = useRef(0);
// 初始化 MessageManager
useEffect(() => {
messageManager.initialize();
}, []);
// 延迟初始化
useEffect(() => {
let timeoutId: NodeJS.Timeout;
let isMounted = true;
const initializeNavigator = async () => {
try {
initAttemptRef.current += 1;
console.log(
`[MobileTabNavigator] Initialization attempt ${initAttemptRef.current}/${MAX_RETRY_ATTEMPTS}`
);
// 延迟初始化
await new Promise((resolve) => {
timeoutId = setTimeout(resolve, INITIALIZATION_DELAY);
});
if (!isMounted) return;
// 后台检测
if (Platform.OS !== 'web' && AppState.currentState === 'background') {
console.log('[MobileTabNavigator] App in background, delaying...');
await new Promise((resolve) => {
timeoutId = setTimeout(resolve, 300);
});
}
if (!isMounted) return;
setIsReady(true);
setHasError(false);
console.log('[MobileTabNavigator] Initialization successful');
} catch (error) {
console.error('[MobileTabNavigator] Initialization error:', error);
if (!isMounted) return;
if (initAttemptRef.current < MAX_RETRY_ATTEMPTS) {
console.log('[MobileTabNavigator] Retrying...');
timeoutId = setTimeout(initializeNavigator, RETRY_DELAY);
} else {
setHasError(true);
}
}
};
initializeNavigator();
return () => {
isMounted = false;
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
// 渲染加载状态
if (!isReady && !hasError) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
);
}
// 渲染错误状态(带重试按钮)
if (hasError) {
return (
<View style={styles.errorContainer}>
<MaterialCommunityIcons
name="alert-circle-outline"
size={48}
color={colors.error.main}
/>
<ActivityIndicator
size="small"
color={colors.primary.main}
style={styles.retryIndicator}
/>
</View>
);
}
// 渲染 Tab Navigator
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: colors.primary.main,
tabBarInactiveTintColor: colors.text.secondary,
tabBarHideOnKeyboard: true,
tabBarStyle: {
backgroundColor: colors.background.paper,
borderTopWidth: 1,
borderTopColor: `${colors.divider}88`,
borderRadius: 24,
marginHorizontal: 14,
marginBottom: MOBILE_TAB_FLOATING_MARGIN + insets.bottom,
height: 64,
paddingBottom: 6,
paddingTop: 8,
paddingHorizontal: 8,
position: 'absolute',
...shadows.lg,
},
tabBarItemStyle: {
borderRadius: 18,
paddingVertical: 1,
marginHorizontal: 2,
},
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '600',
marginTop: -2,
letterSpacing: 0.2,
},
tabBarBadgeStyle: {
backgroundColor: colors.error.main,
color: colors.primary.contrast,
fontSize: 10,
fontWeight: '700',
top: 4,
},
headerShown: false,
}}
>
<Tab.Screen
name="HomeTab"
component={HomeStackNavigatorComponent}
options={{
tabBarLabel: '首页',
tabBarIcon: ({ color, size, focused }) => (
<View
style={[
styles.tabIconContainer,
focused && styles.tabIconActive,
]}
>
<MaterialCommunityIcons
name={focused ? 'home' : 'home-outline'}
size={focused ? 26 : 24}
color={color}
/>
</View>
),
}}
/>
<Tab.Screen
name="MessageTab"
component={MessageStackNavigatorComponent}
options={{
tabBarLabel: '消息',
tabBarBadge:
messageUnreadCount > 0
? messageUnreadCount > 99
? '99+'
: messageUnreadCount
: undefined,
tabBarIcon: ({ color, size, focused }) => (
<View
style={[
styles.tabIconContainer,
focused && styles.tabIconActive,
]}
>
<MaterialCommunityIcons
name={focused ? 'message-text' : 'message-text-outline'}
size={focused ? 26 : 24}
color={color}
/>
</View>
),
}}
/>
<Tab.Screen
name="ScheduleTab"
component={ScheduleStackNavigatorComponent}
options={{
tabBarLabel: '课表',
tabBarIcon: ({ color, size, focused }) => (
<View
style={[
styles.tabIconContainer,
focused && styles.tabIconActive,
]}
>
<MaterialCommunityIcons
name={focused ? 'calendar-today' : 'calendar-today'}
size={focused ? 26 : 24}
color={color}
/>
</View>
),
}}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileStackNavigatorComponent}
options={{
tabBarLabel: '我的',
tabBarIcon: ({ color, size, focused }) => (
<View
style={[
styles.tabIconContainer,
focused && styles.tabIconActive,
]}
>
<MaterialCommunityIcons
name={focused ? 'account' : 'account-outline'}
size={focused ? 26 : 24}
color={color}
/>
</View>
),
}}
/>
</Tab.Navigator>
);
}
// ==================== 样式 ====================
const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
padding: 20,
},
retryIndicator: {
marginTop: 16,
},
tabIconContainer: {
alignItems: 'center',
justifyContent: 'center',
width: 38,
height: 30,
borderRadius: 15,
},
tabIconActive: {
backgroundColor: `${colors.primary.main}20`,
transform: [{ translateY: -1 }],
},
});