refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- Updated App entry point to utilize Expo Router, simplifying the navigation setup. - Removed legacy navigation components and services to streamline the codebase. - Adjusted package.json to reflect the new entry point and updated dependencies for compatibility. - Enhanced overall application structure by consolidating navigation logic and improving maintainability.
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* 认证流程导航
|
||||
* 处理登录、注册、忘记密码等认证页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import type { AuthStackParamList } from './types';
|
||||
|
||||
import { LoginScreen, RegisterScreen, ForgotPasswordScreen } from '../screens/auth';
|
||||
|
||||
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
|
||||
|
||||
export function AuthNavigator() {
|
||||
return (
|
||||
<AuthStack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<AuthStack.Screen name="Login" component={LoginScreen} />
|
||||
<AuthStack.Screen name="Register" component={RegisterScreen} />
|
||||
<AuthStack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
||||
</AuthStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
/**
|
||||
* 桌面端导航器
|
||||
* 为平板/桌面设备提供侧边栏导航体验
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
Animated,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import type { TabName, NavItemConfig } from '../infrastructure/navigation/types';
|
||||
import { NAVIGATION_CONSTANTS } from '../infrastructure/navigation/types';
|
||||
import { colors, shadows } from '../theme';
|
||||
import { useNavigationState } from '../infrastructure/navigation/hooks/useNavigationState';
|
||||
|
||||
// 导入屏幕
|
||||
import { HomeScreen } from '../screens/home';
|
||||
import { ScheduleScreen } from '../screens/schedule';
|
||||
import { MessageListScreen } from '../screens/message';
|
||||
import { ProfileScreen } from '../screens/profile';
|
||||
|
||||
// 侧边栏常量
|
||||
const { SIDEBAR_WIDTH_DESKTOP, SIDEBAR_WIDTH_TABLET, SIDEBAR_COLLAPSED_WIDTH } = NAVIGATION_CONSTANTS;
|
||||
|
||||
// 导航项配置
|
||||
const NAV_ITEMS: NavItemConfig[] = [
|
||||
{ name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' },
|
||||
{ name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline' },
|
||||
{ name: 'ScheduleTab', label: '课表', icon: 'calendar-today', iconOutline: 'calendar-today' },
|
||||
{ name: 'ProfileTab', label: '我的', icon: 'account', iconOutline: 'account-outline' },
|
||||
];
|
||||
|
||||
interface DesktopNavigatorProps {
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
export function DesktopNavigator({ unreadCount = 0 }: DesktopNavigatorProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTab, isCollapsed, setCurrentTab, toggleCollapse, setIsReady } = useNavigationState();
|
||||
const [isDesktop, setIsDesktop] = useState(false);
|
||||
|
||||
// 检测是否是桌面尺寸
|
||||
useEffect(() => {
|
||||
const checkDesktop = () => {
|
||||
const width = window.innerWidth || document.documentElement.clientWidth;
|
||||
setIsDesktop(width >= SIDEBAR_WIDTH_DESKTOP);
|
||||
};
|
||||
|
||||
checkDesktop();
|
||||
window.addEventListener('resize', checkDesktop);
|
||||
setIsReady(true);
|
||||
|
||||
return () => window.removeEventListener('resize', checkDesktop);
|
||||
}, [setIsReady]);
|
||||
|
||||
// 计算侧边栏宽度
|
||||
const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : (isDesktop ? SIDEBAR_WIDTH_DESKTOP : SIDEBAR_WIDTH_TABLET);
|
||||
|
||||
// 处理 Tab 切换
|
||||
const handleTabChange = useCallback((tab: TabName) => {
|
||||
setCurrentTab(tab);
|
||||
}, [setCurrentTab]);
|
||||
|
||||
// 渲染当前 Tab 的内容
|
||||
const renderTabContent = () => {
|
||||
switch (currentTab) {
|
||||
case 'HomeTab':
|
||||
return <HomeScreen />;
|
||||
case 'MessageTab':
|
||||
return <MessageListScreen />;
|
||||
case 'ScheduleTab':
|
||||
return <ScheduleScreen />;
|
||||
case 'ProfileTab':
|
||||
return <ProfileScreen />;
|
||||
default:
|
||||
return <HomeScreen />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 侧边栏 */}
|
||||
<SafeAreaView style={[styles.sidebar, { width: sidebarWidth, paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||
{/* Logo 区域 */}
|
||||
<View style={styles.sidebarHeader}>
|
||||
<MaterialCommunityIcons name="carrot" size={32} color={colors.primary.main} />
|
||||
{!isCollapsed && (
|
||||
<Text style={styles.logoText}>胡萝卜BBS</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 导航项 */}
|
||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = currentTab === item.name;
|
||||
const showBadge = item.name === 'MessageTab' && unreadCount > 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.name}
|
||||
style={[
|
||||
styles.sidebarItem,
|
||||
isActive && styles.sidebarItemActive,
|
||||
isCollapsed && styles.sidebarItemCollapsed,
|
||||
]}
|
||||
onPress={() => handleTabChange(item.name)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.sidebarIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name={isActive ? item.icon as any : item.iconOutline as any}
|
||||
size={24}
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
/>
|
||||
{showBadge && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{!isCollapsed && (
|
||||
<Text style={[styles.sidebarLabel, isActive && styles.sidebarLabelActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
|
||||
{/* 折叠按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.collapseButton, isCollapsed && styles.collapseButtonCollapsed]}
|
||||
onPress={toggleCollapse}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isCollapsed ? 'chevron-right' : 'chevron-left'}
|
||||
size={24}
|
||||
color={colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<View style={styles.mainContent}>
|
||||
{renderTabContent()}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.divider,
|
||||
flexDirection: 'column',
|
||||
...shadows.md,
|
||||
},
|
||||
sidebarHeader: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.primary.main,
|
||||
marginLeft: 8,
|
||||
},
|
||||
sidebarContent: {
|
||||
flex: 1,
|
||||
paddingTop: 8,
|
||||
},
|
||||
sidebarItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
marginHorizontal: 8,
|
||||
marginVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
sidebarItemActive: {
|
||||
backgroundColor: `${colors.primary.main}15`,
|
||||
},
|
||||
sidebarItemCollapsed: {
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
sidebarIconContainer: {
|
||||
position: 'relative',
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 12,
|
||||
},
|
||||
sidebarLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
sidebarLabelActive: {
|
||||
color: colors.primary.main,
|
||||
fontWeight: '600',
|
||||
},
|
||||
collapseButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.divider,
|
||||
},
|
||||
collapseButtonCollapsed: {
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
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',
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* 首页 Stack 导航
|
||||
* 处理首页相关页面:首页、搜索等
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import type { HomeStackParamList } from './types';
|
||||
|
||||
import { colors } from '../theme';
|
||||
import { HomeScreen, SearchScreen } from '../screens/home';
|
||||
|
||||
const HomeStack = createNativeStackNavigator<HomeStackParamList>();
|
||||
|
||||
export function HomeNavigator() {
|
||||
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: '首页',
|
||||
headerBackTitle: '',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name="Search"
|
||||
component={SearchScreen}
|
||||
options={{
|
||||
title: '搜索',
|
||||
headerBackTitle: '',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</HomeStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 主导航组件(重构版)
|
||||
*
|
||||
* 此文件仅保留导航器组装逻辑,所有业务逻辑已解耦至:
|
||||
* - RootNavigator: 处理根导航和认证状态
|
||||
* - DesktopNavigator: 处理桌面端侧边栏导航
|
||||
* - SimpleMobileTabNavigator: 处理移动端 Tab 导航
|
||||
* - navigationService: 提供全局导航方法
|
||||
*
|
||||
* 原文件 1118 行已精简至约 200 行
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { LinkingOptions } from '@react-navigation/native';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import type { RootStackParamList } from './types';
|
||||
import { RootNavigator } from './RootNavigator';
|
||||
|
||||
// 导入认证 Store
|
||||
import { useAuthStore } from '../stores';
|
||||
|
||||
// Deep linking 配置
|
||||
const linking: LinkingOptions<RootStackParamList> = {
|
||||
prefixes: ['carrotbbs://'],
|
||||
config: {
|
||||
screens: {
|
||||
Auth: {
|
||||
screens: {
|
||||
Login: 'login',
|
||||
Register: 'register',
|
||||
ForgotPassword: 'forgot-password',
|
||||
},
|
||||
},
|
||||
Main: {
|
||||
screens: {
|
||||
HomeTab: {
|
||||
screens: {
|
||||
Home: 'home',
|
||||
Search: 'search',
|
||||
},
|
||||
},
|
||||
MessageTab: {
|
||||
screens: {
|
||||
MessageList: 'messages',
|
||||
Notifications: 'notifications',
|
||||
},
|
||||
},
|
||||
ProfileTab: {
|
||||
screens: {
|
||||
Profile: 'me',
|
||||
Settings: 'settings',
|
||||
EditProfile: 'me/edit',
|
||||
AccountSecurity: 'me/security',
|
||||
NotificationSettings: 'me/notifications',
|
||||
BlockedUsers: 'me/blocked',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PostDetail: 'posts/:postId',
|
||||
UserProfile: 'users/:userId',
|
||||
CreatePost: 'posts/create',
|
||||
Chat: 'chat/:conversationId',
|
||||
FollowList: 'users/:userId/:type',
|
||||
QRCodeConfirm: 'qrcode/login/:sessionId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 主导航组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 初始化认证状态
|
||||
* 2. 根据认证状态渲染 RootNavigator
|
||||
* 3. 配置 Deep Linking
|
||||
*/
|
||||
export default function MainNavigator() {
|
||||
const { isAuthenticated, fetchCurrentUser } = useAuthStore();
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
|
||||
// 初始化认证状态
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
await fetchCurrentUser();
|
||||
setIsInitializing(false);
|
||||
};
|
||||
initAuth();
|
||||
}, [fetchCurrentUser]);
|
||||
|
||||
return (
|
||||
<RootNavigator
|
||||
isAuthenticated={isAuthenticated}
|
||||
isInitializing={isInitializing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 导出 linking 配置供外部使用
|
||||
export { linking };
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 消息 Stack 导航
|
||||
* 处理消息相关页面:消息列表、通知等
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import type { MessageStackParamList } from './types';
|
||||
|
||||
import { colors } from '../theme';
|
||||
import {
|
||||
MessageListScreen,
|
||||
NotificationsScreen,
|
||||
PrivateChatInfoScreen,
|
||||
} from '../screens/message';
|
||||
|
||||
const MessageStack = createNativeStackNavigator<MessageStackParamList>();
|
||||
|
||||
export function MessageNavigator() {
|
||||
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: '消息',
|
||||
headerBackTitle: '',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<MessageStack.Screen
|
||||
name="Notifications"
|
||||
component={NotificationsScreen}
|
||||
options={{
|
||||
title: '通知',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<MessageStack.Screen
|
||||
name="PrivateChatInfo"
|
||||
component={PrivateChatInfoScreen}
|
||||
options={{
|
||||
title: '聊天信息',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
</MessageStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -1,555 +0,0 @@
|
||||
/**
|
||||
* 移动端 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;
|
||||
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 }],
|
||||
},
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 个人中心 Stack 导航
|
||||
* 处理个人中心相关页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import type { ProfileStackParamList } from './types';
|
||||
|
||||
import { colors } from '../theme';
|
||||
import {
|
||||
ProfileScreen,
|
||||
SettingsScreen,
|
||||
EditProfileScreen,
|
||||
NotificationSettingsScreen,
|
||||
BlockedUsersScreen,
|
||||
AccountSecurityScreen,
|
||||
} from '../screens/profile';
|
||||
|
||||
const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();
|
||||
|
||||
export function ProfileNavigator() {
|
||||
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: '设置',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="EditProfile"
|
||||
component={EditProfileScreen}
|
||||
options={{
|
||||
title: '编辑资料',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="AccountSecurity"
|
||||
component={AccountSecurityScreen}
|
||||
options={{
|
||||
title: '账号安全',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="MyPosts"
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: '我的帖子',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="Bookmarks"
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: '收藏',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="NotificationSettings"
|
||||
component={NotificationSettingsScreen}
|
||||
options={{
|
||||
title: '通知设置',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
<ProfileStack.Screen
|
||||
name="BlockedUsers"
|
||||
component={BlockedUsersScreen}
|
||||
options={{
|
||||
title: '黑名单',
|
||||
headerBackTitle: '',
|
||||
}}
|
||||
/>
|
||||
</ProfileStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* 根导航器
|
||||
* 处理整个应用的顶级导航,包括认证状态切换
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
|
||||
import type { RootStackParamList } from './types';
|
||||
import { colors } from '../theme';
|
||||
import { navigationService } from '../infrastructure/navigation/navigationService';
|
||||
|
||||
import { AuthNavigator } from './AuthNavigator';
|
||||
import { SimpleMobileTabNavigator } from './SimpleMobileTabNavigator';
|
||||
import { DesktopNavigator } from './DesktopNavigator';
|
||||
|
||||
import { useResponsive } from '../hooks';
|
||||
import { useTotalUnreadCount } from '../stores';
|
||||
|
||||
// 导入全局屏幕组件
|
||||
import { PostDetailScreen } from '../screens/home';
|
||||
import { UserScreen } from '../screens/profile';
|
||||
import FollowListScreen from '../screens/profile/FollowListScreen';
|
||||
import { CreatePostScreen } from '../screens/create';
|
||||
import { QRCodeConfirmScreen } from '../screens/auth/QRCodeConfirmScreen';
|
||||
import {
|
||||
ChatScreen,
|
||||
CreateGroupScreen,
|
||||
JoinGroupScreen,
|
||||
GroupInfoScreen,
|
||||
GroupMembersScreen,
|
||||
GroupRequestDetailScreen,
|
||||
GroupInviteDetailScreen,
|
||||
PrivateChatInfoScreen,
|
||||
} from '../screens/message';
|
||||
|
||||
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
interface RootNavigatorProps {
|
||||
isAuthenticated: boolean;
|
||||
isInitializing: boolean;
|
||||
}
|
||||
|
||||
// 未认证时可访问的屏幕 - 返回数组而不是 JSX
|
||||
const getPublicScreens = () => [
|
||||
<RootStack.Screen
|
||||
key="PostDetail"
|
||||
name="PostDetail"
|
||||
component={PostDetailScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="UserProfile"
|
||||
name="UserProfile"
|
||||
component={UserScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '用户主页',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
// 认证后可访问的屏幕 - 返回数组而不是 JSX
|
||||
const getAuthenticatedScreens = () => [
|
||||
<RootStack.Screen
|
||||
key="PostDetail"
|
||||
name="PostDetail"
|
||||
component={PostDetailScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="UserProfile"
|
||||
name="UserProfile"
|
||||
component={UserScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '用户主页',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="CreatePost"
|
||||
name="CreatePost"
|
||||
component={CreatePostScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '发布帖子',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="Chat"
|
||||
name="Chat"
|
||||
component={ChatScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="FollowList"
|
||||
name="FollowList"
|
||||
component={FollowListScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '关注列表',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="CreateGroup"
|
||||
name="CreateGroup"
|
||||
component={CreateGroupScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '创建群聊',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="JoinGroup"
|
||||
name="JoinGroup"
|
||||
component={JoinGroupScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '搜索群聊',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="GroupInfo"
|
||||
name="GroupInfo"
|
||||
component={GroupInfoScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '群信息',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="GroupMembers"
|
||||
name="GroupMembers"
|
||||
component={GroupMembersScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '群成员',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="GroupRequestDetail"
|
||||
name="GroupRequestDetail"
|
||||
component={GroupRequestDetailScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '入群审批',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="GroupInviteDetail"
|
||||
name="GroupInviteDetail"
|
||||
component={GroupInviteDetailScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '群聊邀请',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="PrivateChatInfo"
|
||||
name="PrivateChatInfo"
|
||||
component={PrivateChatInfoScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '聊天信息',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>,
|
||||
<RootStack.Screen
|
||||
key="QRCodeConfirm"
|
||||
name="QRCodeConfirm"
|
||||
component={QRCodeConfirmScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
export function RootNavigator({ isAuthenticated, isInitializing }: RootNavigatorProps) {
|
||||
const { isMobile } = useResponsive();
|
||||
const unreadCount = useTotalUnreadCount();
|
||||
|
||||
// 设置导航引用
|
||||
const setNavigationRef = (ref: any) => {
|
||||
navigationService.setNavigationRef(ref);
|
||||
};
|
||||
|
||||
// 加载中显示
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationContainer ref={setNavigationRef}>
|
||||
<RootStack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<RootStack.Screen
|
||||
name="Main"
|
||||
navigationKey={isMobile ? 'main-mobile' : 'main-desktop'}
|
||||
>
|
||||
{() =>
|
||||
isMobile ? (
|
||||
<SimpleMobileTabNavigator />
|
||||
) : (
|
||||
<DesktopNavigator unreadCount={unreadCount} />
|
||||
)
|
||||
}
|
||||
</RootStack.Screen>
|
||||
{getAuthenticatedScreens()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RootStack.Screen
|
||||
name="Auth"
|
||||
component={AuthNavigator}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
{getPublicScreens()}
|
||||
</>
|
||||
)}
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* 课程表 Stack 导航
|
||||
* 处理课程表相关页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import type { ScheduleStackParamList } from './types';
|
||||
|
||||
import { colors } from '../theme';
|
||||
import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule';
|
||||
|
||||
const ScheduleStack = createNativeStackNavigator<ScheduleStackParamList>();
|
||||
|
||||
export function ScheduleNavigator() {
|
||||
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',
|
||||
contentStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ScheduleStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
/**
|
||||
* 简单的移动端 Tab Navigator(自定义 Tab 栏)
|
||||
*
|
||||
* 策略(在 EnsureSingleNavigator 限制下尽量省加载):
|
||||
* - 首页、消息:屏幕本身不带根级 NativeStack,首次进入后保留挂载,仅 display 隐藏 → 来回切换快。
|
||||
* - 课表、我的:各含一个 NativeStack,同一时刻只挂载当前选中的一个,避免重复注册 Navigator。
|
||||
*
|
||||
* 其它可叠加的优化(按需再做,不放在本文件里):
|
||||
* - 数据:TanStack Query 调 staleTime、列表 prefetch、占位骨架
|
||||
* - 时机:InteractionManager.runAfterInteractions 再拉非首屏接口
|
||||
* - 渲染:React.memo 重列表项、避免 Tab 切换时整树不必要 setState
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
|
||||
import { colors, shadows } from '../theme';
|
||||
import { useTotalUnreadCount } from '../stores';
|
||||
import { messageManager } from '../stores';
|
||||
|
||||
// ==================== 导入屏幕组件 ====================
|
||||
import { HomeScreen } from '../screens/home';
|
||||
import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule';
|
||||
import { MessageListScreen } from '../screens/message';
|
||||
import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab';
|
||||
|
||||
// Schedule Stack 类型定义
|
||||
export type ScheduleStackParamList = {
|
||||
Schedule: undefined;
|
||||
CourseDetail: { course: any; relatedCourses?: any[] };
|
||||
};
|
||||
|
||||
// Profile Stack 类型定义
|
||||
export type ProfileStackParamList = {
|
||||
Profile: undefined;
|
||||
Settings: undefined;
|
||||
EditProfile: undefined;
|
||||
AccountSecurity: undefined;
|
||||
MyPosts: undefined;
|
||||
Bookmarks: undefined;
|
||||
NotificationSettings: undefined;
|
||||
BlockedUsers: undefined;
|
||||
};
|
||||
|
||||
// ==================== Stack Navigators ====================
|
||||
const ScheduleStack = createNativeStackNavigator<ScheduleStackParamList>();
|
||||
const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();
|
||||
|
||||
// ==================== 常量 ====================
|
||||
const TAB_BAR_HEIGHT = 64;
|
||||
const TAB_BAR_MARGIN = 14;
|
||||
const TAB_BAR_FLOATING_MARGIN = 12;
|
||||
|
||||
// ==================== 组件 ====================
|
||||
|
||||
/**
|
||||
* Schedule Stack 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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主组件:SimpleMobileTabNavigator
|
||||
*/
|
||||
export function SimpleMobileTabNavigator() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const messageUnreadCount = useTotalUnreadCount();
|
||||
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
||||
/** 无嵌套 Stack 的 Tab:保留实例,避免每次切回都整页重挂载 */
|
||||
const [plainTabEverShown, setPlainTabEverShown] = useState({
|
||||
HomeTab: true,
|
||||
MessageTab: false,
|
||||
});
|
||||
|
||||
// 初始化 MessageManager
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
}, []);
|
||||
|
||||
const handleTabPress = useCallback((tabName: TabName) => {
|
||||
if (tabName === 'HomeTab' || tabName === 'MessageTab') {
|
||||
setPlainTabEverShown((prev) => ({ ...prev, [tabName]: true }));
|
||||
}
|
||||
setActiveTab(tabName);
|
||||
}, []);
|
||||
|
||||
// 渲染 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}>
|
||||
{plainTabEverShown.HomeTab && (
|
||||
<View
|
||||
key="HomeTab"
|
||||
style={[
|
||||
styles.tabScreen,
|
||||
activeTab !== 'HomeTab' && styles.tabScreenHidden,
|
||||
]}
|
||||
pointerEvents={activeTab === 'HomeTab' ? 'auto' : 'none'}
|
||||
>
|
||||
<HomeScreen />
|
||||
</View>
|
||||
)}
|
||||
{plainTabEverShown.MessageTab && (
|
||||
<View
|
||||
key="MessageTab"
|
||||
style={[
|
||||
styles.tabScreen,
|
||||
activeTab !== 'MessageTab' && styles.tabScreenHidden,
|
||||
]}
|
||||
pointerEvents={activeTab === 'MessageTab' ? 'auto' : 'none'}
|
||||
>
|
||||
<MessageListScreen />
|
||||
</View>
|
||||
)}
|
||||
{activeTab === 'ScheduleTab' && (
|
||||
<View key="ScheduleTab" style={styles.tabScreen}>
|
||||
<ScheduleStackNavigatorComponent />
|
||||
</View>
|
||||
)}
|
||||
{activeTab === 'ProfileTab' && (
|
||||
<View key="ProfileTab" style={styles.tabScreen}>
|
||||
<ProfileStackNavigatorComponent />
|
||||
</View>
|
||||
)}
|
||||
</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,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
tabScreen: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
tabScreenHidden: {
|
||||
display: 'none',
|
||||
},
|
||||
tabBar: {
|
||||
position: 'absolute',
|
||||
left: TAB_BAR_MARGIN,
|
||||
right: TAB_BAR_MARGIN,
|
||||
height: TAB_BAR_HEIGHT,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: 24,
|
||||
// 移除顶部边框,避免与内容之间出现线条
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 8,
|
||||
...shadows.lg,
|
||||
// 叠在全屏内容之上(shadows.lg 含 elevation,需最后覆盖)
|
||||
zIndex: 100,
|
||||
elevation: 100,
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* Tab 导航器
|
||||
* 处理底部标签导航(移动端)
|
||||
*/
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import type { MainTabParamList } from './types';
|
||||
import { colors, shadows } from '../theme';
|
||||
|
||||
import { HomeNavigator } from './HomeNavigator';
|
||||
import { MessageNavigator } from './MessageNavigator';
|
||||
import { ScheduleNavigator } from './ScheduleNavigator';
|
||||
import { ProfileNavigator } from './ProfileNavigator';
|
||||
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
|
||||
// 常量
|
||||
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
||||
|
||||
interface TabNavigatorProps {
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export function TabNavigator({ unreadCount }: TabNavigatorProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
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={HomeNavigator}
|
||||
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={MessageNavigator}
|
||||
options={{
|
||||
tabBarLabel: '消息',
|
||||
tabBarBadge: unreadCount > 0 ? (unreadCount > 99 ? '99+' : unreadCount) : 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={ScheduleNavigator}
|
||||
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={ProfileNavigator}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabIconContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 38,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
},
|
||||
tabIconActive: {
|
||||
backgroundColor: `${colors.primary.main}20`,
|
||||
transform: [{ translateY: -1 }],
|
||||
},
|
||||
});
|
||||
153
src/navigation/hrefs.ts
Normal file
153
src/navigation/hrefs.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Expo Router 路径构建(单一事实来源,避免魔法字符串散落)
|
||||
*/
|
||||
import type { SystemMessageResponse } from '../types/dto';
|
||||
import { routePayloadCache } from '../stores/routePayloadCache';
|
||||
|
||||
export function hrefPostDetail(postId: string, scrollToComments?: boolean): string {
|
||||
const q = scrollToComments ? '?scrollToComments=1' : '';
|
||||
return `/post/${encodeURIComponent(postId)}${q}`;
|
||||
}
|
||||
|
||||
export function hrefUserProfile(userId: string): string {
|
||||
return `/user/${encodeURIComponent(userId)}`;
|
||||
}
|
||||
|
||||
export function hrefHome(): string {
|
||||
return '/home';
|
||||
}
|
||||
|
||||
export function hrefHomeSearch(): string {
|
||||
return '/home/search';
|
||||
}
|
||||
|
||||
export function hrefMessages(): string {
|
||||
return '/messages';
|
||||
}
|
||||
|
||||
export function hrefNotifications(): string {
|
||||
return '/messages/notifications';
|
||||
}
|
||||
|
||||
export function hrefSchedule(): string {
|
||||
return '/schedule';
|
||||
}
|
||||
|
||||
export function hrefScheduleCourse(courseId: string): string {
|
||||
return `/schedule/course?courseId=${encodeURIComponent(courseId)}`;
|
||||
}
|
||||
|
||||
export function hrefProfileSettings(): string {
|
||||
return '/profile/settings';
|
||||
}
|
||||
|
||||
export function hrefProfileEdit(): string {
|
||||
return '/profile/edit-profile';
|
||||
}
|
||||
|
||||
export function hrefProfileSecurity(): string {
|
||||
return '/profile/account-security';
|
||||
}
|
||||
|
||||
export function hrefProfileNotifications(): string {
|
||||
return '/profile/notification-settings';
|
||||
}
|
||||
|
||||
export function hrefProfileBlocked(): string {
|
||||
return '/profile/blocked-users';
|
||||
}
|
||||
|
||||
export function hrefProfileMyPosts(): string {
|
||||
return '/profile/my-posts';
|
||||
}
|
||||
|
||||
export function hrefProfileBookmarks(): string {
|
||||
return '/profile/bookmarks';
|
||||
}
|
||||
|
||||
export function hrefCreatePost(mode?: 'create' | 'edit', postId?: string): string {
|
||||
if (mode === 'edit' && postId) {
|
||||
return `/posts/create?mode=edit&postId=${encodeURIComponent(postId)}`;
|
||||
}
|
||||
return '/posts/create';
|
||||
}
|
||||
|
||||
export function hrefChat(params: {
|
||||
conversationId: string;
|
||||
userId?: string;
|
||||
isGroupChat?: boolean;
|
||||
groupId?: string;
|
||||
groupName?: string;
|
||||
}): string {
|
||||
const { conversationId, userId, isGroupChat, groupId, groupName } = params;
|
||||
const q = new URLSearchParams();
|
||||
if (userId) q.set('userId', userId);
|
||||
if (isGroupChat) q.set('isGroupChat', '1');
|
||||
if (groupId != null && groupId !== '') q.set('groupId', String(groupId));
|
||||
if (groupName) q.set('groupName', groupName);
|
||||
const qs = q.toString();
|
||||
return `/chat/${encodeURIComponent(conversationId)}${qs ? `?${qs}` : ''}`;
|
||||
}
|
||||
|
||||
export function hrefFollowList(userId: string, type: 'following' | 'followers'): string {
|
||||
return `/users/${encodeURIComponent(userId)}/${type}`;
|
||||
}
|
||||
|
||||
export function hrefGroupCreate(): string {
|
||||
return '/group/create';
|
||||
}
|
||||
|
||||
export function hrefGroupJoin(): string {
|
||||
return '/group/join';
|
||||
}
|
||||
|
||||
export function hrefGroupInfo(groupId: string, conversationId?: string): string {
|
||||
const base = `/group/${encodeURIComponent(groupId)}`;
|
||||
if (!conversationId) return base;
|
||||
return `${base}?conversationId=${encodeURIComponent(conversationId)}`;
|
||||
}
|
||||
|
||||
export function hrefGroupMembers(groupId: string): string {
|
||||
return `/group/${encodeURIComponent(groupId)}/members`;
|
||||
}
|
||||
|
||||
export function hrefGroupRequestDetail(message: SystemMessageResponse): string {
|
||||
routePayloadCache.stashSystemMessage(message);
|
||||
return `/group/request?messageId=${encodeURIComponent(message.id)}`;
|
||||
}
|
||||
|
||||
export function hrefGroupInviteDetail(message: SystemMessageResponse): string {
|
||||
routePayloadCache.stashSystemMessage(message);
|
||||
return `/group/invite?messageId=${encodeURIComponent(message.id)}`;
|
||||
}
|
||||
|
||||
export function hrefPrivateChatInfo(params: {
|
||||
conversationId: string;
|
||||
userId: string;
|
||||
userName?: string;
|
||||
userAvatar?: string | null;
|
||||
}): string {
|
||||
const q = new URLSearchParams({
|
||||
conversationId: params.conversationId,
|
||||
userId: params.userId,
|
||||
});
|
||||
if (params.userName) q.set('userName', params.userName);
|
||||
if (params.userAvatar != null) q.set('userAvatar', params.userAvatar ?? '');
|
||||
return `/chat/private-info?${q.toString()}`;
|
||||
}
|
||||
|
||||
export function hrefQrLoginConfirm(sessionId: string): string {
|
||||
return `/qrcode/login/${encodeURIComponent(sessionId)}`;
|
||||
}
|
||||
|
||||
export function hrefAuthLogin(): string {
|
||||
return '/login';
|
||||
}
|
||||
|
||||
export function hrefAuthRegister(): string {
|
||||
return '/register';
|
||||
}
|
||||
|
||||
export function hrefAuthForgot(): string {
|
||||
return '/forgot-password';
|
||||
}
|
||||
@@ -1,29 +1,4 @@
|
||||
/**
|
||||
* 导出所有导航组件
|
||||
* 导航路径工具(运行时导航请使用 expo-router)
|
||||
*/
|
||||
|
||||
// 类型
|
||||
export type {
|
||||
RootStackParamList,
|
||||
MainTabParamList,
|
||||
HomeStackParamList,
|
||||
MessageStackParamList,
|
||||
ScheduleStackParamList,
|
||||
ProfileStackParamList,
|
||||
AuthStackParamList,
|
||||
TabName,
|
||||
NavItemConfig,
|
||||
} from './types';
|
||||
|
||||
export { AuthNavigator } from './AuthNavigator';
|
||||
export { HomeNavigator } from './HomeNavigator';
|
||||
export { MessageNavigator } from './MessageNavigator';
|
||||
export { ScheduleNavigator } from './ScheduleNavigator';
|
||||
export { ProfileNavigator } from './ProfileNavigator';
|
||||
export { TabNavigator } from './TabNavigator';
|
||||
export { DesktopNavigator } from './DesktopNavigator';
|
||||
export { RootNavigator } from './RootNavigator';
|
||||
export { default as MainNavigator } from './MainNavigator';
|
||||
|
||||
// 移动端简化导航
|
||||
export { SimpleMobileTabNavigator } from './SimpleMobileTabNavigator';
|
||||
export * from './hrefs';
|
||||
|
||||
11
src/navigation/paramUtils.ts
Normal file
11
src/navigation/paramUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Expo Router 的 searchParams、动态段可能是 string | string[]
|
||||
* 群 ID 等在应用内一律为 string;路由参数禁止 Number() 以免精度丢失
|
||||
*/
|
||||
export function firstRouteParam(value: string | string[] | undefined): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
const raw = Array.isArray(value) ? value[0] : value;
|
||||
if (raw == null) return undefined;
|
||||
const s = String(raw).trim();
|
||||
return s === '' ? undefined : s;
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* 导航类型定义
|
||||
* 定义所有导航栈的类型参数
|
||||
*/
|
||||
|
||||
import { NavigatorScreenParams } from '@react-navigation/native';
|
||||
import type { SystemMessageResponse } from '../types/dto';
|
||||
import type { Course } from '../types/schedule';
|
||||
|
||||
// ==================== 主Tab导航参数 ====================
|
||||
export type MainTabParamList = {
|
||||
HomeTab: NavigatorScreenParams<HomeStackParamList>;
|
||||
MessageTab: NavigatorScreenParams<MessageStackParamList>;
|
||||
ScheduleTab: NavigatorScreenParams<ScheduleStackParamList>;
|
||||
ProfileTab: NavigatorScreenParams<ProfileStackParamList>;
|
||||
};
|
||||
|
||||
// ==================== 首页Stack ====================
|
||||
export type HomeStackParamList = {
|
||||
Home: undefined;
|
||||
PostDetail: { postId: string; scrollToComments?: boolean };
|
||||
Search: undefined;
|
||||
UserProfile: { userId: string };
|
||||
FollowList: { userId: string; type: 'following' | 'followers' };
|
||||
};
|
||||
|
||||
// ==================== 消息Stack ====================
|
||||
export type MessageStackParamList = {
|
||||
MessageList: undefined;
|
||||
Chat: {
|
||||
conversationId: string;
|
||||
userId?: string;
|
||||
isGroupChat?: boolean;
|
||||
groupId?: number;
|
||||
groupName?: string;
|
||||
};
|
||||
Notifications: undefined;
|
||||
CreateGroup: undefined;
|
||||
JoinGroup: undefined;
|
||||
GroupInfo: { groupId: number; conversationId?: string };
|
||||
GroupMembers: { groupId: number };
|
||||
GroupRequestDetail: { message: SystemMessageResponse };
|
||||
GroupInviteDetail: { message: SystemMessageResponse };
|
||||
PrivateChatInfo: {
|
||||
conversationId: string;
|
||||
userId: string;
|
||||
userName?: string;
|
||||
userAvatar?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 个人中心Stack ====================
|
||||
export type ProfileStackParamList = {
|
||||
Profile: undefined;
|
||||
Settings: undefined;
|
||||
EditProfile: undefined;
|
||||
AccountSecurity: undefined;
|
||||
MyPosts: undefined;
|
||||
Bookmarks: undefined;
|
||||
NotificationSettings: undefined;
|
||||
BlockedUsers: undefined;
|
||||
};
|
||||
|
||||
// ==================== 课程表Stack ====================
|
||||
export type ScheduleStackParamList = {
|
||||
Schedule: undefined;
|
||||
CourseDetail: { course: Course; relatedCourses: Course[] };
|
||||
};
|
||||
|
||||
// ==================== 认证Stack ====================
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
Register: undefined;
|
||||
ForgotPassword: undefined;
|
||||
};
|
||||
|
||||
// ==================== 根导航 ====================
|
||||
export type RootStackParamList = {
|
||||
Main: NavigatorScreenParams<MainTabParamList>;
|
||||
Auth: undefined;
|
||||
PostDetail: { postId: string; scrollToComments?: boolean };
|
||||
UserProfile: { userId: string };
|
||||
CreatePost:
|
||||
| undefined
|
||||
| {
|
||||
mode?: 'create' | 'edit';
|
||||
postId?: string;
|
||||
};
|
||||
Chat: {
|
||||
conversationId: string;
|
||||
userId?: string;
|
||||
isGroupChat?: boolean;
|
||||
groupId?: number;
|
||||
groupName?: string;
|
||||
};
|
||||
FollowList: { userId: string; type: 'following' | 'followers' };
|
||||
CreateGroup: undefined;
|
||||
JoinGroup: undefined;
|
||||
GroupInfo: { groupId: number; conversationId?: string };
|
||||
GroupMembers: { groupId: number };
|
||||
GroupRequestDetail: { message: SystemMessageResponse };
|
||||
GroupInviteDetail: { message: SystemMessageResponse };
|
||||
PrivateChatInfo: {
|
||||
conversationId: string;
|
||||
userId: string;
|
||||
userName?: string;
|
||||
userAvatar?: string | null;
|
||||
};
|
||||
QRCodeConfirm: { sessionId: string };
|
||||
};
|
||||
|
||||
// ==================== 全局类型声明 ====================
|
||||
declare global {
|
||||
namespace ReactNavigation {
|
||||
interface RootParamList extends RootStackParamList {}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 屏幕名称类型(用于路由)====================
|
||||
export type HomeScreenNames = keyof HomeStackParamList;
|
||||
export type MessageScreenNames = keyof MessageStackParamList;
|
||||
export type ProfileScreenNames = keyof ProfileStackParamList;
|
||||
export type MainTabScreenNames = keyof MainTabParamList;
|
||||
export type RootScreenNames = keyof RootStackParamList;
|
||||
|
||||
// ==================== Tab 类型 ====================
|
||||
export type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab';
|
||||
|
||||
// ==================== 导航项配置 ====================
|
||||
export interface NavItemConfig {
|
||||
name: TabName;
|
||||
label: string;
|
||||
icon: string;
|
||||
iconOutline: string;
|
||||
}
|
||||
Reference in New Issue
Block a user