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

@@ -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,