2026-03-16 17:47:10 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 简单的移动端 Tab Navigator
|
2026-03-20 19:28:42 +08:00
|
|
|
|
*
|
2026-03-16 17:47:10 +08:00
|
|
|
|
* 不使用 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';
|
2026-03-20 19:28:42 +08:00
|
|
|
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
2026-03-16 17:47:10 +08:00
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
2026-03-20 19:28:42 +08:00
|
|
|
|
// Schedule Stack 类型定义
|
|
|
|
|
|
export type ScheduleStackParamList = {
|
|
|
|
|
|
Schedule: undefined;
|
|
|
|
|
|
CourseDetail: { course: any; relatedCourses?: any[] };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-21 01:36:40 +08:00
|
|
|
|
// Profile Stack 类型定义
|
|
|
|
|
|
export type ProfileStackParamList = {
|
|
|
|
|
|
Profile: undefined;
|
|
|
|
|
|
Settings: undefined;
|
|
|
|
|
|
EditProfile: undefined;
|
|
|
|
|
|
AccountSecurity: undefined;
|
|
|
|
|
|
MyPosts: undefined;
|
|
|
|
|
|
Bookmarks: undefined;
|
|
|
|
|
|
NotificationSettings: undefined;
|
|
|
|
|
|
BlockedUsers: undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-20 19:28:42 +08:00
|
|
|
|
// ==================== Stack Navigators ====================
|
|
|
|
|
|
const ScheduleStack = createNativeStackNavigator<ScheduleStackParamList>();
|
2026-03-21 01:36:40 +08:00
|
|
|
|
const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();
|
2026-03-20 19:28:42 +08:00
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
// ==================== 常量 ====================
|
|
|
|
|
|
const TAB_BAR_HEIGHT = 64;
|
|
|
|
|
|
const TAB_BAR_MARGIN = 14;
|
|
|
|
|
|
const TAB_BAR_FLOATING_MARGIN = 12;
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 组件 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-20 19:28:42 +08:00
|
|
|
|
* Schedule Stack Navigator 组件
|
2026-03-16 17:47:10 +08:00
|
|
|
|
*/
|
2026-03-20 19:28:42 +08:00
|
|
|
|
function ScheduleStackNavigatorComponent() {
|
2026-03-16 17:47:10 +08:00
|
|
|
|
return (
|
2026-03-20 19:28:42 +08:00
|
|
|
|
<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>
|
2026-03-16 17:47:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:36:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Profile Stack 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 主组件: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':
|
2026-03-20 19:28:42 +08:00
|
|
|
|
return <ScheduleStackNavigatorComponent />;
|
2026-03-16 17:47:10 +08:00
|
|
|
|
case 'ProfileTab':
|
2026-03-21 01:36:40 +08:00
|
|
|
|
return <ProfileStackNavigatorComponent />;
|
2026-03-16 17:47:10 +08:00
|
|
|
|
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',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|