refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- Updated App entry point to utilize Expo Router, simplifying the navigation setup. - Removed legacy navigation components and services to streamline the codebase. - Adjusted package.json to reflect the new entry point and updated dependencies for compatibility. - Enhanced overall application structure by consolidating navigation logic and improving maintainability.
This commit is contained in:
@@ -19,8 +19,7 @@ import {
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
||||
@@ -31,15 +30,12 @@ import { postService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { SearchScreen } from './SearchScreen';
|
||||
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||
import { navigationService } from '../../infrastructure/navigation/navigationService';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Home'> & NativeStackNavigationProp<RootStackParamList>;
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
const TABS = ['最新', '关注', '热门'];
|
||||
const TAB_ICONS = ['clock-outline', 'account-heart-outline', 'fire'];
|
||||
@@ -54,7 +50,7 @@ type ViewMode = 'list' | 'grid';
|
||||
type PostType = 'follow' | 'hot' | 'latest';
|
||||
|
||||
export const HomeScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
@@ -302,12 +298,12 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
// 跳转到帖子详情
|
||||
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
|
||||
navigationService.navigate('PostDetail', { postId, scrollToComments });
|
||||
router.push(hrefs.hrefPostDetail(postId, scrollToComments));
|
||||
};
|
||||
|
||||
// 跳转到用户主页
|
||||
const handleUserPress = (userId: string) => {
|
||||
navigationService.navigate('UserProfile', { userId });
|
||||
router.push(hrefs.hrefUserProfile(userId));
|
||||
};
|
||||
|
||||
// 点赞帖子
|
||||
@@ -563,10 +559,12 @@ export const HomeScreen: React.FC = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||||
{/* 加载更多指示器 */}
|
||||
{isLoading && displayPosts.length > 0 && (
|
||||
<View style={styles.loadingMore}>
|
||||
<View style={styles.waterfallColumnsRow}>
|
||||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||||
</View>
|
||||
{/* 独立底部加载区:避免参与横向列布局导致列宽抖动 */}
|
||||
{isLoadingMore && displayPosts.length > 0 && (
|
||||
<View style={styles.loadingMoreFooter}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
)}
|
||||
@@ -808,8 +806,12 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
waterfallContainer: {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
},
|
||||
waterfallColumnsRow: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
waterfallColumn: {
|
||||
@@ -844,7 +846,7 @@ const styles = StyleSheet.create({
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
},
|
||||
loadingMore: {
|
||||
loadingMoreFooter: {
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -23,8 +23,7 @@ import {
|
||||
Clipboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
@@ -35,19 +34,20 @@ import { postService, commentService, uploadService, authService, showPrompt, vo
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
|
||||
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'PostDetail'>;
|
||||
type PostDetailRouteProp = RouteProp<RootStackParamList, 'PostDetail'>;
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
export const PostDetailScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const route = useRoute<PostDetailRouteProp>();
|
||||
const navigation = useNavigation();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const postId = route.params?.postId || '';
|
||||
const shouldScrollToComments = route.params?.scrollToComments || false;
|
||||
const { postId: postIdParam, scrollToComments: scrollParam } = useLocalSearchParams<{
|
||||
postId?: string;
|
||||
scrollToComments?: string;
|
||||
}>();
|
||||
const postId = postIdParam || '';
|
||||
const shouldScrollToComments = scrollParam === '1' || scrollParam === 'true';
|
||||
|
||||
// 使用响应式 hook
|
||||
const {
|
||||
@@ -282,23 +282,18 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
const author = post.author;
|
||||
const handleBackPress = () => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack();
|
||||
if (router.canGoBack()) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
navigation.navigate('Main', {
|
||||
screen: 'HomeTab',
|
||||
params: { screen: 'Home', params: undefined },
|
||||
});
|
||||
router.replace(hrefs.hrefHome());
|
||||
};
|
||||
|
||||
navigation.setOptions({
|
||||
headerBackVisible: false,
|
||||
headerTitleAlign: 'left',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={handleBackPress} style={styles.headerBackButton} hitSlop={8}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton onPress={handleBackPress} style={styles.headerBackButton} />
|
||||
),
|
||||
headerTitle: () => (
|
||||
<View style={styles.headerContainer}>
|
||||
@@ -641,7 +636,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
onPress: () => router.back(),
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
@@ -654,15 +649,12 @@ export const PostDetailScreen: React.FC = () => {
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [post, isDeleting, currentUser?.id, navigation]);
|
||||
}, [post, isDeleting, currentUser?.id, router]);
|
||||
|
||||
const handleEditPost = useCallback(() => {
|
||||
if (!post) return;
|
||||
navigation.navigate('CreatePost', {
|
||||
mode: 'edit',
|
||||
postId: post.id,
|
||||
});
|
||||
}, [navigation, post]);
|
||||
router.push(hrefs.hrefCreatePost('edit', post.id));
|
||||
}, [router, post]);
|
||||
|
||||
// 点击图片查看大图
|
||||
const handleImagePress = useCallback((images: ImageGridItem[], index: number) => {
|
||||
@@ -999,7 +991,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
// 跳转到用户主页
|
||||
const handleUserPress = (userId: string) => {
|
||||
navigation.navigate('UserProfile', { userId });
|
||||
router.push(hrefs.hrefUserProfile(userId));
|
||||
};
|
||||
|
||||
// 渲染身份标识
|
||||
@@ -1294,27 +1286,22 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 评论标题 - 现代化设计 */}
|
||||
<View style={[styles.commentTitle, { paddingVertical: responsiveGap }]}>
|
||||
<View style={styles.commentTitleLeft}>
|
||||
<View style={[styles.commentTitleIconContainer, { width: isDesktop ? 32 : 28, height: isDesktop ? 32 : 28 }]}>
|
||||
<MaterialCommunityIcons
|
||||
name="chat-processing-outline"
|
||||
size={isDesktop ? 18 : 16}
|
||||
color={colors.primary.contrast}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
variant="body"
|
||||
style={[
|
||||
styles.commentTitleText,
|
||||
{ fontSize: isDesktop ? fontSizes.xl : fontSizes.lg }
|
||||
]}
|
||||
>
|
||||
评论
|
||||
</Text>
|
||||
<View style={styles.commentCountBadge}>
|
||||
<Text variant="caption" style={styles.commentCountText}>{post.comments_count}</Text>
|
||||
{/* 评论标题 - 现代化简洁分区头 */}
|
||||
<View style={[styles.commentSectionHeader, { marginTop: responsiveGap, marginBottom: responsiveGap }]}>
|
||||
<View style={styles.commentSectionTitleBlock}>
|
||||
<View style={styles.commentSectionTitleRow}>
|
||||
<Text
|
||||
variant="body"
|
||||
style={[
|
||||
styles.commentSectionTitle,
|
||||
{ fontSize: isDesktop ? fontSizes.xl : fontSizes.lg }
|
||||
]}
|
||||
>
|
||||
评论区
|
||||
</Text>
|
||||
<View style={styles.commentCountBadge}>
|
||||
<Text variant="caption" style={styles.commentCountText}>{post.comments_count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1396,9 +1383,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
<View style={styles.emptyCommentsContainer}>
|
||||
<View style={styles.emptyCommentsIconWrapper}>
|
||||
<MaterialCommunityIcons
|
||||
name="message-text-outline"
|
||||
size={isDesktop ? 48 : 40}
|
||||
color={colors.primary.main}
|
||||
name="message-reply-text-outline"
|
||||
size={isDesktop ? 24 : 22}
|
||||
color={colors.text.secondary}
|
||||
/>
|
||||
</View>
|
||||
<Text variant="body" style={styles.emptyCommentsTitle}>
|
||||
@@ -1831,7 +1818,7 @@ const styles = StyleSheet.create({
|
||||
postMetaInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-start',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
metaInfoMain: {
|
||||
@@ -1928,38 +1915,42 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 6,
|
||||
fontWeight: '500',
|
||||
},
|
||||
commentTitle: {
|
||||
commentSectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: colors.divider,
|
||||
paddingTop: spacing.lg,
|
||||
},
|
||||
commentSectionTitleBlock: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
commentSectionTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
commentTitleLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
commentTitleIconContainer: {
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
commentTitleText: {
|
||||
fontWeight: '700',
|
||||
commentSectionTitle: {
|
||||
fontWeight: '800',
|
||||
color: colors.text.primary,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
commentCountBadge: {
|
||||
backgroundColor: colors.primary.light + '25',
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.background.default,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
paddingHorizontal: spacing.sm + 2,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 999,
|
||||
marginLeft: spacing.sm,
|
||||
minWidth: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
commentCountText: {
|
||||
fontSize: fontSizes.sm,
|
||||
fontSize: fontSizes.xs,
|
||||
fontWeight: '700',
|
||||
color: colors.primary.main,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
@@ -2067,17 +2058,26 @@ const styles = StyleSheet.create({
|
||||
emptyCommentsContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
marginHorizontal: spacing.lg,
|
||||
marginTop: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
paddingVertical: spacing.xl + spacing.md,
|
||||
paddingHorizontal: spacing.lg,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
emptyCommentsIconWrapper: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: colors.primary.light + '18',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.background.default,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emptyCommentsTitle: {
|
||||
fontSize: fontSizes.md,
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
RefreshControl,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import { Post, User } from '../../types';
|
||||
@@ -24,12 +23,10 @@ import { postService, authService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Search'>;
|
||||
|
||||
const TABS = ['帖子', '用户'];
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
@@ -37,12 +34,10 @@ type SearchType = 'posts' | 'users';
|
||||
|
||||
interface SearchScreenProps {
|
||||
onBack?: () => void;
|
||||
navigation?: NavigationProp;
|
||||
}
|
||||
|
||||
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation: propNavigation }) => {
|
||||
// 如果传入了 navigation 则使用传入的,否则使用 hook 获取的
|
||||
const navigation = propNavigation || useNavigation<NavigationProp>();
|
||||
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore();
|
||||
|
||||
@@ -168,12 +163,12 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
|
||||
// 跳转到帖子详情
|
||||
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
|
||||
navigation.navigate('PostDetail', { postId, scrollToComments });
|
||||
router.push(hrefs.hrefPostDetail(postId, scrollToComments));
|
||||
};
|
||||
|
||||
// 跳转到用户主页
|
||||
const handleUserPress = (userId: string) => {
|
||||
navigation.navigate('UserProfile', { userId });
|
||||
router.push(hrefs.hrefUserProfile(userId));
|
||||
};
|
||||
|
||||
// 统一处理 PostCard 的操作(搜索页不支持删除)
|
||||
@@ -515,7 +510,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelButton, { marginLeft: responsiveGap }]}
|
||||
onPress={() => onBack ? onBack() : navigation.goBack()}
|
||||
onPress={() => (onBack ? onBack() : router.back())}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text
|
||||
|
||||
Reference in New Issue
Block a user