1040 lines
29 KiB
TypeScript
1040 lines
29 KiB
TypeScript
|
|
/**
|
|||
|
|
* 主导航组件
|
|||
|
|
* 配置底部Tab导航和Stack导航
|
|||
|
|
* 根据登录状态自动切换AuthStack和MainStack
|
|||
|
|
* 支持响应式布局:移动端底部导航,平板/桌面端侧边导航
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|||
|
|
import {
|
|||
|
|
StyleSheet,
|
|||
|
|
View,
|
|||
|
|
ActivityIndicator,
|
|||
|
|
TouchableOpacity,
|
|||
|
|
Animated,
|
|||
|
|
ScrollView,
|
|||
|
|
Text as RNText,
|
|||
|
|
Platform,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import {
|
|||
|
|
NavigationContainer,
|
|||
|
|
LinkingOptions,
|
|||
|
|
NavigatorScreenParams,
|
|||
|
|
RouteProp,
|
|||
|
|
useNavigation,
|
|||
|
|
useRoute,
|
|||
|
|
} from '@react-navigation/native';
|
|||
|
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|||
|
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|||
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|||
|
|
import { useTheme } from 'react-native-paper';
|
|||
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||
|
|
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
|
|||
|
|
|
|||
|
|
import { colors, shadows } from '../theme';
|
|||
|
|
import { useAuthStore, useTotalUnreadCount } from '../stores';
|
|||
|
|
import { messageManager } from '../stores';
|
|||
|
|
import { useResponsive } from '../hooks';
|
|||
|
|
import type {
|
|||
|
|
RootStackParamList,
|
|||
|
|
MainTabParamList,
|
|||
|
|
HomeStackParamList,
|
|||
|
|
MessageStackParamList,
|
|||
|
|
ProfileStackParamList,
|
|||
|
|
AuthStackParamList,
|
|||
|
|
} from './types';
|
|||
|
|
|
|||
|
|
// ==================== 导入屏幕组件 ====================
|
|||
|
|
import { HomeScreen, PostDetailScreen, SearchScreen } from '../screens/home';
|
|||
|
|
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';
|
|||
|
|
import { LoginScreen, RegisterScreen, ForgotPasswordScreen } from '../screens/auth';
|
|||
|
|
|
|||
|
|
// ==================== Stack Navigators ====================
|
|||
|
|
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
|||
|
|
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
|
|||
|
|
const HomeStack = createNativeStackNavigator<HomeStackParamList>();
|
|||
|
|
const MessageStack = createNativeStackNavigator<MessageStackParamList>();
|
|||
|
|
const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();
|
|||
|
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
|||
|
|
|
|||
|
|
// ==================== 导航栏宽度常量 ====================
|
|||
|
|
const SIDEBAR_WIDTH_DESKTOP = 240;
|
|||
|
|
const SIDEBAR_WIDTH_TABLET = 200;
|
|||
|
|
const SIDEBAR_COLLAPSED_WIDTH = 72;
|
|||
|
|
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
|||
|
|
|
|||
|
|
// ==================== 导航项类型 ====================
|
|||
|
|
type TabName = 'HomeTab' | 'MessageTab' | 'ProfileTab';
|
|||
|
|
|
|||
|
|
type IconName = React.ComponentProps<typeof MaterialCommunityIcons>['name'];
|
|||
|
|
|
|||
|
|
interface NavItem {
|
|||
|
|
name: TabName;
|
|||
|
|
label: string;
|
|||
|
|
icon: IconName;
|
|||
|
|
iconOutline: IconName;
|
|||
|
|
badge?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== AuthStack导航 ====================
|
|||
|
|
function AuthStackNavigator() {
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 首页Stack导航 ====================
|
|||
|
|
function HomeStackNavigator() {
|
|||
|
|
const paperTheme = useTheme();
|
|||
|
|
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 消息Stack导航 ====================
|
|||
|
|
function MessageStackNavigator() {
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 个人中心Stack导航 ====================
|
|||
|
|
function ProfileStackNavigator() {
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 侧边导航栏组件 ====================
|
|||
|
|
interface SidebarProps {
|
|||
|
|
activeTab: TabName;
|
|||
|
|
onTabChange: (tab: TabName) => void;
|
|||
|
|
unreadCount: number;
|
|||
|
|
isCollapsed: boolean;
|
|||
|
|
onToggleCollapse: () => void;
|
|||
|
|
width: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function Sidebar({
|
|||
|
|
activeTab,
|
|||
|
|
onTabChange,
|
|||
|
|
unreadCount,
|
|||
|
|
isCollapsed,
|
|||
|
|
onToggleCollapse,
|
|||
|
|
width,
|
|||
|
|
}: SidebarProps) {
|
|||
|
|
const insets = useSafeAreaInsets();
|
|||
|
|
|
|||
|
|
const navItems: NavItem[] = [
|
|||
|
|
{ name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' },
|
|||
|
|
{ name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline', badge: unreadCount },
|
|||
|
|
{ name: 'ProfileTab', label: '我的', icon: 'account', iconOutline: 'account-outline' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView
|
|||
|
|
style={[
|
|||
|
|
styles.sidebar,
|
|||
|
|
{
|
|||
|
|
width,
|
|||
|
|
paddingTop: insets.top,
|
|||
|
|
paddingBottom: insets.bottom,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{/* Logo/品牌区域 */}
|
|||
|
|
<View style={styles.sidebarHeader}>
|
|||
|
|
{!isCollapsed && (
|
|||
|
|
<View style={styles.logoContainer}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="carrot"
|
|||
|
|
size={32}
|
|||
|
|
color={colors.primary.main}
|
|||
|
|
/>
|
|||
|
|
<Animated.Text style={styles.logoText}>
|
|||
|
|
胡萝卜BBS
|
|||
|
|
</Animated.Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
{isCollapsed && (
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="carrot"
|
|||
|
|
size={32}
|
|||
|
|
color={colors.primary.main}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 导航项 */}
|
|||
|
|
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
|||
|
|
{navItems.map((item) => {
|
|||
|
|
const isActive = activeTab === item.name;
|
|||
|
|
const showBadge = !!(item.badge && item.badge > 0);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={item.name}
|
|||
|
|
style={[
|
|||
|
|
styles.sidebarItem,
|
|||
|
|
isActive && styles.sidebarItemActive,
|
|||
|
|
isCollapsed && styles.sidebarItemCollapsed,
|
|||
|
|
]}
|
|||
|
|
onPress={() => onTabChange(item.name)}
|
|||
|
|
activeOpacity={0.7}
|
|||
|
|
>
|
|||
|
|
<View style={styles.sidebarIconContainer}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name={isActive ? item.icon : item.iconOutline}
|
|||
|
|
size={24}
|
|||
|
|
color={isActive ? colors.primary.main : colors.text.secondary}
|
|||
|
|
/>
|
|||
|
|
{showBadge && (
|
|||
|
|
<View style={styles.badge}>
|
|||
|
|
<Text style={styles.badgeText}>
|
|||
|
|
{item.badge! > 99 ? '99+' : item.badge}
|
|||
|
|
</Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
{!isCollapsed && (
|
|||
|
|
<Text
|
|||
|
|
style={[
|
|||
|
|
styles.sidebarLabel,
|
|||
|
|
isActive && styles.sidebarLabelActive,
|
|||
|
|
]}
|
|||
|
|
numberOfLines={1}
|
|||
|
|
>
|
|||
|
|
{item.label}
|
|||
|
|
</Text>
|
|||
|
|
)}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</ScrollView>
|
|||
|
|
|
|||
|
|
{/* 底部折叠按钮 */}
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[
|
|||
|
|
styles.collapseButton,
|
|||
|
|
isCollapsed && styles.collapseButtonCollapsed,
|
|||
|
|
]}
|
|||
|
|
onPress={onToggleCollapse}
|
|||
|
|
activeOpacity={0.7}
|
|||
|
|
>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name={isCollapsed ? 'chevron-right' : 'chevron-left'}
|
|||
|
|
size={24}
|
|||
|
|
color={colors.text.secondary}
|
|||
|
|
/>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Text 组件 ====================
|
|||
|
|
function Text({ style, children, numberOfLines }: { style?: any; children: React.ReactNode; numberOfLines?: number }) {
|
|||
|
|
return (
|
|||
|
|
<RNText style={style} numberOfLines={numberOfLines}>
|
|||
|
|
{children}
|
|||
|
|
</RNText>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 响应式 Tab Navigator ====================
|
|||
|
|
interface ResponsiveTabNavigatorProps {
|
|||
|
|
children?: React.ReactNode;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) {
|
|||
|
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
|||
|
|
const route = useRoute<RouteProp<RootStackParamList, 'Main'>>();
|
|||
|
|
const { isMobile, isTablet, isDesktop } = useResponsive();
|
|||
|
|
const insets = useSafeAreaInsets();
|
|||
|
|
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
|||
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|||
|
|
const messageUnreadCount = useTotalUnreadCount();
|
|||
|
|
|
|||
|
|
// 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新
|
|||
|
|
useEffect(() => {
|
|||
|
|
messageManager.initialize();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 同步 URL 中的 Main 子路由到桌面侧边栏激活态
|
|||
|
|
useEffect(() => {
|
|||
|
|
const targetTab = route.params?.screen;
|
|||
|
|
if (!targetTab) return;
|
|||
|
|
if ((targetTab === 'HomeTab' || targetTab === 'MessageTab' || targetTab === 'ProfileTab') && targetTab !== activeTab) {
|
|||
|
|
setActiveTab(targetTab);
|
|||
|
|
}
|
|||
|
|
}, [route.params?.screen, activeTab]);
|
|||
|
|
|
|||
|
|
// 计算侧边栏宽度
|
|||
|
|
const sidebarWidth = useCallback(() => {
|
|||
|
|
if (isCollapsed) return SIDEBAR_COLLAPSED_WIDTH;
|
|||
|
|
if (isDesktop) return SIDEBAR_WIDTH_DESKTOP;
|
|||
|
|
if (isTablet) return SIDEBAR_WIDTH_TABLET;
|
|||
|
|
return 0;
|
|||
|
|
}, [isCollapsed, isDesktop, isTablet]);
|
|||
|
|
|
|||
|
|
// 切换 Tab
|
|||
|
|
const handleTabChange = useCallback((tab: TabName) => {
|
|||
|
|
setActiveTab(tab);
|
|||
|
|
if (Platform.OS === 'web') {
|
|||
|
|
let tabTarget: NavigatorScreenParams<MainTabParamList>;
|
|||
|
|
if (tab === 'HomeTab') {
|
|||
|
|
tabTarget = { screen: 'HomeTab', params: { screen: 'Home' } };
|
|||
|
|
} else if (tab === 'MessageTab') {
|
|||
|
|
tabTarget = { screen: 'MessageTab', params: { screen: 'MessageList' } };
|
|||
|
|
} else {
|
|||
|
|
tabTarget = { screen: 'ProfileTab', params: { screen: 'Profile' } };
|
|||
|
|
}
|
|||
|
|
navigation.navigate('Main', tabTarget);
|
|||
|
|
}
|
|||
|
|
}, [navigation]);
|
|||
|
|
|
|||
|
|
// 切换折叠状态
|
|||
|
|
const handleToggleCollapse = useCallback(() => {
|
|||
|
|
setIsCollapsed(prev => !prev);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 渲染当前 Tab 的内容
|
|||
|
|
const renderTabContent = () => {
|
|||
|
|
switch (activeTab) {
|
|||
|
|
case 'HomeTab':
|
|||
|
|
return <HomeStackNavigator />;
|
|||
|
|
case 'MessageTab':
|
|||
|
|
return <MessageStackNavigator />;
|
|||
|
|
case 'ProfileTab':
|
|||
|
|
return <ProfileStackNavigator />;
|
|||
|
|
default:
|
|||
|
|
return <HomeStackNavigator />;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 移动端:使用底部 Tab 导航
|
|||
|
|
if (isMobile) {
|
|||
|
|
return <MainTabNavigator />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 平板/桌面端:使用侧边导航
|
|||
|
|
const currentSidebarWidth = sidebarWidth();
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={styles.responsiveContainer}>
|
|||
|
|
{/* 侧边导航栏 */}
|
|||
|
|
<Sidebar
|
|||
|
|
activeTab={activeTab}
|
|||
|
|
onTabChange={handleTabChange}
|
|||
|
|
unreadCount={messageUnreadCount}
|
|||
|
|
isCollapsed={isCollapsed}
|
|||
|
|
onToggleCollapse={handleToggleCollapse}
|
|||
|
|
width={currentSidebarWidth}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* 主内容区域 */}
|
|||
|
|
<View
|
|||
|
|
style={[
|
|||
|
|
styles.mainContent,
|
|||
|
|
{
|
|||
|
|
marginLeft: 0,
|
|||
|
|
flex: 1,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{renderTabContent()}
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 底部Tab导航(移动端)====================
|
|||
|
|
function MainTabNavigator() {
|
|||
|
|
const insets = useSafeAreaInsets();
|
|||
|
|
const messageUnreadCount = useTotalUnreadCount();
|
|||
|
|
|
|||
|
|
// 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新
|
|||
|
|
useEffect(() => {
|
|||
|
|
messageManager.initialize();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
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={HomeStackNavigator}
|
|||
|
|
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={MessageStackNavigator}
|
|||
|
|
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="ProfileTab"
|
|||
|
|
component={ProfileStackNavigator}
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 根导航 ====================
|
|||
|
|
export default function MainNavigator() {
|
|||
|
|
const { isAuthenticated, fetchCurrentUser } = useAuthStore();
|
|||
|
|
const [initializing, setInitializing] = useState(true);
|
|||
|
|
|
|||
|
|
const linking: LinkingOptions<RootStackParamList> = {
|
|||
|
|
prefixes: [],
|
|||
|
|
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',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化时检查登录状态
|
|||
|
|
useEffect(() => {
|
|||
|
|
const initAuth = async () => {
|
|||
|
|
await fetchCurrentUser();
|
|||
|
|
setInitializing(false);
|
|||
|
|
};
|
|||
|
|
initAuth();
|
|||
|
|
}, [fetchCurrentUser]);
|
|||
|
|
|
|||
|
|
// 加载中显示
|
|||
|
|
if (initializing) {
|
|||
|
|
return (
|
|||
|
|
<View style={styles.loadingContainer}>
|
|||
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<NavigationContainer linking={Platform.OS === 'web' ? linking : undefined}>
|
|||
|
|
<RootStack.Navigator
|
|||
|
|
screenOptions={{
|
|||
|
|
headerShown: false,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* 根据登录状态显示不同Stack */}
|
|||
|
|
{isAuthenticated ? (
|
|||
|
|
<>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="Main"
|
|||
|
|
component={ResponsiveTabNavigator}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="PostDetail"
|
|||
|
|
component={PostDetailScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="UserProfile"
|
|||
|
|
component={UserScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '用户主页',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="CreatePost"
|
|||
|
|
component={CreatePostScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '发布帖子',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
presentation: 'modal',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="Chat"
|
|||
|
|
component={ChatScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: false,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="FollowList"
|
|||
|
|
component={FollowListScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '关注列表',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="CreateGroup"
|
|||
|
|
component={CreateGroupScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '创建群聊',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
presentation: 'modal',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="JoinGroup"
|
|||
|
|
component={JoinGroupScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '搜索群聊',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="GroupInfo"
|
|||
|
|
component={GroupInfoScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '群信息',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="GroupMembers"
|
|||
|
|
component={GroupMembersScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '群成员',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="GroupRequestDetail"
|
|||
|
|
component={GroupRequestDetailScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '入群审批',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="GroupInviteDetail"
|
|||
|
|
component={GroupInviteDetailScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '群聊邀请',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="PrivateChatInfo"
|
|||
|
|
component={PrivateChatInfoScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '聊天信息',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="Auth"
|
|||
|
|
component={AuthStackNavigator}
|
|||
|
|
options={{
|
|||
|
|
headerShown: false,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
{/* 未登录时也可以查看帖子详情 */}
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="PostDetail"
|
|||
|
|
component={PostDetailScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
animation: 'slide_from_right',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<RootStack.Screen
|
|||
|
|
name="UserProfile"
|
|||
|
|
component={UserScreen}
|
|||
|
|
options={{
|
|||
|
|
headerShown: true,
|
|||
|
|
title: '用户主页',
|
|||
|
|
headerStyle: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
},
|
|||
|
|
headerTintColor: colors.text.primary,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</RootStack.Navigator>
|
|||
|
|
</NavigationContainer>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 样式 ====================
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
// 响应式容器
|
|||
|
|
responsiveContainer: {
|
|||
|
|
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',
|
|||
|
|
},
|
|||
|
|
logoContainer: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
},
|
|||
|
|
logoText: {
|
|||
|
|
fontSize: 20,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
color: colors.primary.main,
|
|||
|
|
marginLeft: 12,
|
|||
|
|
},
|
|||
|
|
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,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 底部 Tab 样式
|
|||
|
|
tabIconContainer: {
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
width: 38,
|
|||
|
|
height: 30,
|
|||
|
|
borderRadius: 15,
|
|||
|
|
},
|
|||
|
|
tabIconActive: {
|
|||
|
|
backgroundColor: `${colors.primary.main}20`,
|
|||
|
|
transform: [{ translateY: -1 }],
|
|||
|
|
},
|
|||
|
|
loadingContainer: {
|
|||
|
|
flex: 1,
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
backgroundColor: colors.background.default,
|
|||
|
|
},
|
|||
|
|
});
|