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

@@ -9,14 +9,16 @@ import {
Alert,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { navigationService } from '../../infrastructure/navigation/navigationService';
import { useRouter } from 'expo-router';
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
import { authService } from '../../services';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { User } from '../../types';
import { useResponsive } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
export const BlockedUsersScreen: React.FC = () => {
const router = useRouter();
const { isMobile } = useResponsive();
const insets = useSafeAreaInsets();
const [users, setUsers] = useState<User[]>([]);
@@ -78,7 +80,7 @@ export const BlockedUsersScreen: React.FC = () => {
<TouchableOpacity
style={styles.item}
activeOpacity={0.75}
onPress={() => navigationService.navigate('UserProfile', { userId: item.id })}
onPress={() => router.push(hrefs.hrefUserProfile(item.id))}
>
<Avatar source={item.avatar} size={46} name={item.nickname} />
<View style={styles.content}>

View File

@@ -16,23 +16,19 @@ import {
ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { User } from '../../types';
import { useAuthStore, useUserStore } from '../../stores';
import { authService } from '../../services';
import { Avatar, Text, Button, Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { HomeStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
import { useResponsive, useColumnCount } from '../../hooks';
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'FollowList'>;
type FollowListRouteProp = RouteProp<HomeStackParamList, 'FollowList'>;
const FollowListScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<FollowListRouteProp>();
const { userId, type } = route.params;
const router = useRouter();
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
const type = typeParam === 'followers' ? 'followers' : 'following';
const { currentUser } = useAuthStore();
const { followUser, unfollowUser } = useUserStore();
@@ -135,7 +131,7 @@ const FollowListScreen: React.FC = () => {
// 跳转到用户主页
const handleUserPress = (targetUserId: string) => {
if (targetUserId !== currentUser?.id) {
navigation.push('UserProfile', { userId: targetUserId });
router.push(hrefs.hrefUserProfile(targetUserId));
}
};
@@ -182,11 +178,11 @@ const FollowListScreen: React.FC = () => {
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
{item.bio && (
{item.bio?.trim() ? (
<Text variant="caption" color={colors.text.hint} numberOfLines={1} style={styles.bio}>
{item.bio}
</Text>
)}
) : null}
</View>
{!isSelf && (
<Button
@@ -226,11 +222,11 @@ const FollowListScreen: React.FC = () => {
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
{item.bio && (
{item.bio?.trim() ? (
<Text variant="caption" color={colors.text.hint} numberOfLines={2} style={styles.userCardBio}>
{item.bio}
</Text>
)}
) : null}
</View>
{!isSelf && (
<View style={styles.userCardFooter}>

View File

@@ -5,17 +5,10 @@
*/
import React from 'react';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import UserProfileScreen from './UserProfileScreen';
import { ProfileStackParamList } from '../../navigation/types';
type ProfileNavigationProp = NativeStackNavigationProp<ProfileStackParamList, 'Profile'>;
export const ProfileScreen: React.FC = () => {
const profileNavigation = useNavigation<ProfileNavigationProp>();
return <UserProfileScreen mode="self" profileNavigation={profileNavigation} />;
return <UserProfileScreen mode="self" />;
};
export default ProfileScreen;

View File

@@ -13,7 +13,7 @@ import {
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
@@ -22,6 +22,8 @@ import { Text, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as hrefs from '../../navigation/hrefs';
interface SettingsItem {
key: string;
title: string;
@@ -65,7 +67,7 @@ const SETTINGS_GROUPS = [
];
export const SettingsScreen: React.FC = () => {
const navigation = useNavigation();
const router = useRouter();
const { logout } = useAuthStore();
const { isWideScreen } = useResponsive();
const insets = useSafeAreaInsets();
@@ -78,16 +80,16 @@ export const SettingsScreen: React.FC = () => {
const handleItemPress = (key: string) => {
switch (key) {
case 'edit_profile':
navigation.navigate('EditProfile' as never);
router.push(hrefs.hrefProfileEdit());
break;
case 'notification_settings':
navigation.navigate('NotificationSettings' as never);
router.push(hrefs.hrefProfileNotifications());
break;
case 'blocked_users':
navigation.navigate('BlockedUsers' as never);
router.push(hrefs.hrefProfileBlocked());
break;
case 'security':
navigation.navigate('AccountSecurity' as never);
router.push(hrefs.hrefProfileSecurity());
break;
case 'logout':
Alert.alert(
@@ -98,8 +100,9 @@ export const SettingsScreen: React.FC = () => {
{
text: '确定',
style: 'destructive',
onPress: () => {
logout();
onPress: async () => {
await logout();
router.replace(hrefs.hrefAuthLogin());
}
},
]

View File

@@ -12,23 +12,19 @@ import {
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { NavigationProp } from '@react-navigation/native';
import { colors } from '../../theme';
import { Post } from '../../types';
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { ProfileStackParamList } from '../../navigation/types';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, sharedStyles } from './useUserProfile';
interface UserProfileScreenProps {
mode: ProfileMode;
userId?: string; // 仅 other 模式需要
/** 使用 NavigationProp 避免与具体 screen 的 NativeStackNavigationProp 在 setParams 上不兼容 */
profileNavigation?: NavigationProp<ProfileStackParamList>; // 仅 self 模式需要
}
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, profileNavigation }) => {
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => {
const { isDesktop, isTablet } = useResponsive();
const {
@@ -51,7 +47,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
handleEditProfile,
isCurrentUser,
currentUser,
} = useUserProfile({ mode, userId, isDesktop, isTablet, profileNavigation });
} = useUserProfile({ mode, userId, isDesktop, isTablet });
// 渲染帖子列表
const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string) => {

View File

@@ -5,16 +5,13 @@
*/
import React from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import { useLocalSearchParams } from 'expo-router';
import UserProfileScreen from './UserProfileScreen';
import { HomeStackParamList } from '../../navigation/types';
import { useCurrentUser } from '../../stores/authStore';
type UserRouteProp = RouteProp<HomeStackParamList, 'UserProfile'>;
export const UserScreen: React.FC = () => {
const route = useRoute<UserRouteProp>();
const userId = route.params?.userId || '';
const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>();
const userId = userIdParam || '';
const currentUser = useCurrentUser();
const isSelfProfile = !!currentUser?.id && currentUser.id === userId;

View File

@@ -4,8 +4,7 @@
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors, spacing } from '../../theme';
import { Post, User } from '../../types';
@@ -13,7 +12,7 @@ import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
import { postService, authService, messageService } from '../../services';
import { userManager } from '../../stores/userManager';
import { HomeStackParamList, RootStackParamList, ProfileStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
import { PostCardAction } from '../../components/business/PostCard';
export type ProfileMode = 'self' | 'other';
@@ -23,7 +22,6 @@ export interface UseUserProfileOptions {
userId?: string; // 仅 other 模式需要
isDesktop?: boolean;
isTablet?: boolean;
profileNavigation?: NavigationProp<ProfileStackParamList>; // 仅 self 模式需要
}
export interface UseUserProfileReturn {
@@ -66,11 +64,10 @@ export interface UseUserProfileReturn {
}
export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileReturn => {
const { mode, userId, isDesktop = false, profileNavigation } = options;
const { mode, userId, isDesktop = false } = options;
const insets = useSafeAreaInsets();
const homeNavigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>();
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const router = useRouter();
const { followUser, unfollowUser, posts: storePosts } = useUserStore();
const currentUser = useCurrentUser();
@@ -252,32 +249,38 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
const handleFollowingPress = useCallback(() => {
const targetUserId = mode === 'self' ? currentUser?.id : userId;
if (targetUserId) {
(rootNavigation as any).navigate('FollowList', { userId: targetUserId, type: 'following' });
router.push(hrefs.hrefFollowList(targetUserId, 'following'));
}
}, [mode, currentUser?.id, userId, rootNavigation]);
}, [mode, currentUser?.id, userId, router]);
// 跳转到粉丝列表
const handleFollowersPress = useCallback(() => {
const targetUserId = mode === 'self' ? currentUser?.id : userId;
if (targetUserId) {
(rootNavigation as any).navigate('FollowList', { userId: targetUserId, type: 'followers' });
router.push(hrefs.hrefFollowList(targetUserId, 'followers'));
}
}, [mode, currentUser?.id, userId, rootNavigation]);
}, [mode, currentUser?.id, userId, router]);
// 跳转到帖子详情
const handlePostPress = useCallback((postId: string, scrollToComments: boolean = false) => {
homeNavigation.navigate('PostDetail', { postId, scrollToComments });
}, [homeNavigation]);
const handlePostPress = useCallback(
(postId: string, scrollToComments: boolean = false) => {
router.push(hrefs.hrefPostDetail(postId, scrollToComments));
},
[router]
);
// 跳转到用户主页
const handleUserPress = useCallback((postUserId: string) => {
if (mode === 'self') {
return;
}
if (postUserId !== userId) {
homeNavigation.push('UserProfile', { userId: postUserId });
}
}, [mode, userId, homeNavigation]);
const handleUserPress = useCallback(
(postUserId: string) => {
if (mode === 'self') {
return;
}
if (postUserId !== userId) {
router.push(hrefs.hrefUserProfile(postUserId));
}
},
[mode, userId, router]
);
// 删除帖子
const handleDeletePost = useCallback(async (postId: string) => {
@@ -332,15 +335,17 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
try {
const conversation = await messageService.createConversation(user.id);
if (conversation) {
(rootNavigation as any).navigate('Chat', {
conversationId: conversation.id.toString(),
userId: user.id
});
router.push(
hrefs.hrefChat({
conversationId: conversation.id.toString(),
userId: user.id,
}) as any
);
}
} catch (error) {
console.error('创建会话失败:', error);
}
}, [user, rootNavigation]);
}, [user, router]);
// 更多操作(仅 other 模式)
const handleMore = useCallback(() => {
@@ -393,17 +398,13 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
// 设置页跳转(仅 self 模式)
const handleSettings = useCallback(() => {
if (profileNavigation) {
profileNavigation.navigate('Settings');
}
}, [profileNavigation]);
router.push(hrefs.hrefProfileSettings());
}, [router]);
// 编辑资料页跳转(仅 self 模式)
const handleEditProfile = useCallback(() => {
if (profileNavigation) {
profileNavigation.navigate('EditProfile');
}
}, [profileNavigation]);
router.push(hrefs.hrefProfileEdit());
}, [router]);
// 计算滚动底部间距
const scrollBottomInset = useMemo(() => {
@@ -435,8 +436,8 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
handlePostAction,
handleMessage: mode === 'other' ? handleMessage : undefined,
handleMore: mode === 'other' ? handleMore : undefined,
handleSettings: mode === 'self' && profileNavigation ? handleSettings : undefined,
handleEditProfile: mode === 'self' && profileNavigation ? handleEditProfile : undefined,
handleSettings: mode === 'self' ? handleSettings : undefined,
handleEditProfile: mode === 'self' ? handleEditProfile : undefined,
isCurrentUser: mode === 'self',
currentUser,
};