refactor(App, navigation): migrate to Expo Router and clean up navigation structure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m27s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h16m45s

- 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:
lafay
2026-03-24 14:21:31 +08:00
parent b91e78c921
commit 2ddb9cadd8
111 changed files with 1829 additions and 3214 deletions

View File

@@ -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>
);
}

View File

@@ -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,
},
});

View File

@@ -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>
);
}

View File

@@ -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 };

View File

@@ -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>
);
}

View File

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

View File

@@ -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>
);
}

View File

@@ -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,
},
});

View File

@@ -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>
);
}

View File

@@ -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',
},
});

View File

@@ -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
View 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';
}

View File

@@ -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';

View 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;
}

View File

@@ -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;
}