PC端的部分适配
This commit is contained in:
@@ -35,6 +35,7 @@ import { colors, shadows } from '../theme';
|
||||
import { useAuthStore, useTotalUnreadCount } from '../stores';
|
||||
import { messageManager } from '../stores';
|
||||
import { useResponsive } from '../hooks';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import type {
|
||||
RootStackParamList,
|
||||
MainTabParamList,
|
||||
@@ -44,6 +45,7 @@ import type {
|
||||
ProfileStackParamList,
|
||||
AuthStackParamList,
|
||||
} from './types';
|
||||
import { SimpleMobileTabNavigator } from './SimpleMobileTabNavigator';
|
||||
|
||||
// ==================== 导入屏幕组件 ====================
|
||||
import { HomeScreen, PostDetailScreen, SearchScreen } from '../screens/home';
|
||||
@@ -81,6 +83,9 @@ const SIDEBAR_WIDTH_TABLET = 200;
|
||||
const SIDEBAR_COLLAPSED_WIDTH = 72;
|
||||
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
||||
|
||||
// ==================== 持久化存储键 ====================
|
||||
const LAST_ACTIVE_TAB_KEY = 'last_active_tab';
|
||||
|
||||
// ==================== 导航项类型 ====================
|
||||
type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab';
|
||||
|
||||
@@ -306,6 +311,18 @@ function Sidebar({
|
||||
}: SidebarProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// 调试日志
|
||||
useEffect(() => {
|
||||
console.log('[Debug Sidebar]', {
|
||||
width,
|
||||
isCollapsed,
|
||||
activeTab,
|
||||
unreadCount,
|
||||
insetsTop: insets.top,
|
||||
insetsBottom: insets.bottom,
|
||||
});
|
||||
}, [width, isCollapsed, activeTab, unreadCount, insets]);
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' },
|
||||
{ name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline', badge: unreadCount },
|
||||
@@ -461,12 +478,13 @@ interface ResponsiveTabNavigatorProps {
|
||||
function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Main'>>();
|
||||
const { isMobile, isTablet, isDesktop } = useResponsive();
|
||||
const { isMobile, isTablet, isDesktop, width, height } = useResponsive();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const messageUnreadCount = useTotalUnreadCount();
|
||||
|
||||
|
||||
// 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
@@ -483,54 +501,62 @@ function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) {
|
||||
|
||||
// 计算侧边栏宽度
|
||||
const sidebarWidth = useCallback(() => {
|
||||
if (isMobile) return 0; // 移动端不显示侧边栏
|
||||
if (isCollapsed) return SIDEBAR_COLLAPSED_WIDTH;
|
||||
if (isDesktop) return SIDEBAR_WIDTH_DESKTOP;
|
||||
if (isTablet) return SIDEBAR_WIDTH_TABLET;
|
||||
return 0;
|
||||
}, [isCollapsed, isDesktop, isTablet]);
|
||||
// 默认返回桌面宽度,避免返回 0 导致导航栏消失
|
||||
return SIDEBAR_WIDTH_DESKTOP;
|
||||
}, [isCollapsed, isDesktop, isTablet, isMobile]);
|
||||
|
||||
// 切换 Tab
|
||||
const handleTabChange = useCallback((tab: TabName) => {
|
||||
setActiveTab(tab);
|
||||
if (Platform.OS === 'web') {
|
||||
// 在大屏幕模式下,通过导航切换 Tab
|
||||
if (!isMobile) {
|
||||
let tabTarget: NavigatorScreenParams<MainTabParamList>;
|
||||
if (tab === 'HomeTab') {
|
||||
tabTarget = { screen: 'HomeTab', params: { screen: 'Home' } };
|
||||
} else if (tab === 'MessageTab') {
|
||||
tabTarget = { screen: 'MessageTab', params: { screen: 'MessageList' } };
|
||||
} else if (tab === 'ScheduleTab') {
|
||||
tabTarget = { screen: 'ScheduleTab', params: { screen: 'Schedule' } };
|
||||
} else {
|
||||
tabTarget = { screen: 'ProfileTab', params: { screen: 'Profile' } };
|
||||
}
|
||||
navigation.navigate('Main', tabTarget);
|
||||
}
|
||||
}, [navigation]);
|
||||
}, [navigation, isMobile]);
|
||||
|
||||
// 切换折叠状态
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
setIsCollapsed(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 渲染当前 Tab 的内容
|
||||
// 移动端:使用底部 Tab 导航
|
||||
if (isMobile) {
|
||||
// 使用简化的移动端导航,避免 React Navigation 状态恢复问题
|
||||
return <SimpleMobileTabNavigator />;
|
||||
}
|
||||
|
||||
// 渲染当前 Tab 的内容(仅在大屏模式下使用)
|
||||
// 注意:直接渲染屏幕组件,不使用 Stack Navigators
|
||||
// 这样可以避免 React Navigation 状态恢复问题
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'HomeTab':
|
||||
return <HomeStackNavigator />;
|
||||
return <HomeScreen />;
|
||||
case 'MessageTab':
|
||||
return <MessageStackNavigator />;
|
||||
return <MessageListScreen />;
|
||||
case 'ScheduleTab':
|
||||
return <ScheduleStackNavigator />;
|
||||
return <ScheduleScreen />;
|
||||
case 'ProfileTab':
|
||||
return <ProfileStackNavigator />;
|
||||
return <ProfileScreen />;
|
||||
default:
|
||||
return <HomeStackNavigator />;
|
||||
return <HomeScreen />;
|
||||
}
|
||||
};
|
||||
|
||||
// 移动端:使用底部 Tab 导航
|
||||
if (isMobile) {
|
||||
return <MainTabNavigator />;
|
||||
}
|
||||
|
||||
// 平板/桌面端:使用侧边导航
|
||||
const currentSidebarWidth = sidebarWidth();
|
||||
|
||||
|
||||
1126
src/navigation/MainNavigator.tsx.bak
Normal file
1126
src/navigation/MainNavigator.tsx.bak
Normal file
File diff suppressed because it is too large
Load Diff
545
src/navigation/MobileTabNavigatorWithDelay.tsx
Normal file
545
src/navigation/MobileTabNavigatorWithDelay.tsx
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 移动端 Tab Navigator(完全独立版本)
|
||||
*
|
||||
* 这个组件完全独立于 MainNavigator.tsx 中的其他导航组件,
|
||||
* 用于解决从大屏切换到小屏时的白屏问题。
|
||||
*
|
||||
* 问题根源:
|
||||
* - React Navigation 的 TabNavigator 在初始化时需要完整的 navigation state
|
||||
* - 从大屏(Sidebar)切换到小屏(BottomTab)时,state 可能还没有准备好
|
||||
* - 导致 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;
|
||||
};
|
||||
|
||||
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.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 }],
|
||||
},
|
||||
});
|
||||
282
src/navigation/SimpleMobileTabNavigator.tsx
Normal file
282
src/navigation/SimpleMobileTabNavigator.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 简单的移动端 Tab Navigator
|
||||
*
|
||||
* 不使用 React Navigation 的 Tab Navigator,而是使用一个简单的自定义实现
|
||||
* 这样可以完全避免 React Navigation 状态恢复的问题
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
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, SearchScreen } from '../screens/home';
|
||||
import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule';
|
||||
import {
|
||||
MessageListScreen,
|
||||
NotificationsScreen,
|
||||
PrivateChatInfoScreen,
|
||||
} from '../screens/message';
|
||||
import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab';
|
||||
|
||||
// ==================== 常量 ====================
|
||||
const TAB_BAR_HEIGHT = 64;
|
||||
const TAB_BAR_MARGIN = 14;
|
||||
const TAB_BAR_FLOATING_MARGIN = 12;
|
||||
|
||||
// ==================== 组件 ====================
|
||||
|
||||
/**
|
||||
* 简化的 Stack Navigator 包装
|
||||
*/
|
||||
function SimpleStackNavigator({
|
||||
children,
|
||||
screenName
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
screenName: string;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.stackContainer}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主组件:SimpleMobileTabNavigator
|
||||
*/
|
||||
export function SimpleMobileTabNavigator() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const messageUnreadCount = useTotalUnreadCount();
|
||||
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
||||
|
||||
// 初始化 MessageManager
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
}, []);
|
||||
|
||||
// 处理 Tab 切换
|
||||
const handleTabPress = useCallback((tabName: TabName) => {
|
||||
setActiveTab(tabName);
|
||||
}, []);
|
||||
|
||||
// 渲染当前 Tab 的内容
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'HomeTab':
|
||||
return <HomeScreen />;
|
||||
case 'MessageTab':
|
||||
return <MessageListScreen />;
|
||||
case 'ScheduleTab':
|
||||
return <ScheduleScreen />;
|
||||
case 'ProfileTab':
|
||||
return <ProfileScreen />;
|
||||
default:
|
||||
return <HomeScreen />;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染 Tab Bar 图标
|
||||
const renderTabIcon = (tabName: TabName, isActive: boolean) => {
|
||||
const iconColor = isActive ? colors.primary.main : colors.text.secondary;
|
||||
const iconSize = isActive ? 26 : 24;
|
||||
|
||||
switch (tabName) {
|
||||
case 'HomeTab':
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={isActive ? 'home' : 'home-outline'}
|
||||
size={iconSize}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
case 'MessageTab':
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={isActive ? 'message-text' : 'message-text-outline'}
|
||||
size={iconSize}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
case 'ScheduleTab':
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={isActive ? 'calendar-today' : 'calendar-today'}
|
||||
size={iconSize}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
case 'ProfileTab':
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={isActive ? 'account' : 'account-outline'}
|
||||
size={iconSize}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染 Tab Bar 标签
|
||||
const renderTabLabel = (tabName: TabName, isActive: boolean) => {
|
||||
const labels: Record<TabName, string> = {
|
||||
HomeTab: '首页',
|
||||
MessageTab: '消息',
|
||||
ScheduleTab: '课表',
|
||||
ProfileTab: '我的',
|
||||
};
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
isActive && styles.tabLabelActive,
|
||||
]}
|
||||
>
|
||||
{labels[tabName]}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 内容区域 */}
|
||||
<View style={styles.content}>
|
||||
{renderTabContent()}
|
||||
</View>
|
||||
|
||||
{/* Tab Bar */}
|
||||
<View
|
||||
style={[
|
||||
styles.tabBar,
|
||||
{
|
||||
bottom: TAB_BAR_FLOATING_MARGIN + insets.bottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(['HomeTab', 'MessageTab', 'ScheduleTab', 'ProfileTab'] as TabName[]).map(
|
||||
(tabName) => {
|
||||
const isActive = activeTab === tabName;
|
||||
const showBadge = tabName === 'MessageTab' && messageUnreadCount > 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tabName}
|
||||
style={[
|
||||
styles.tabItem,
|
||||
isActive && styles.tabItemActive,
|
||||
]}
|
||||
onPress={() => handleTabPress(tabName)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.tabIconContainer}>
|
||||
{renderTabIcon(tabName, isActive)}
|
||||
{showBadge && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>
|
||||
{messageUnreadCount > 99 ? '99+' : messageUnreadCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{renderTabLabel(tabName, isActive)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 样式 ====================
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
stackContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
marginBottom: TAB_BAR_HEIGHT + TAB_BAR_FLOATING_MARGIN * 2,
|
||||
},
|
||||
tabBar: {
|
||||
position: 'absolute',
|
||||
left: TAB_BAR_MARGIN,
|
||||
right: TAB_BAR_MARGIN,
|
||||
height: TAB_BAR_HEIGHT,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: 24,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: `${colors.divider}88`,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 8,
|
||||
...shadows.lg,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 6,
|
||||
borderRadius: 18,
|
||||
},
|
||||
tabItemActive: {
|
||||
backgroundColor: `${colors.primary.main}15`,
|
||||
},
|
||||
tabIconContainer: {
|
||||
position: 'relative',
|
||||
width: 38,
|
||||
height: 30,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 15,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: -2,
|
||||
letterSpacing: 0.2,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
tabLabelActive: {
|
||||
color: colors.primary.main,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
right: -2,
|
||||
backgroundColor: colors.error.main,
|
||||
borderRadius: 10,
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
badgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user