- Introduced new methods for merging posts and comments efficiently, improving performance during data updates. - Updated loading state management in hooks to include initial loading and loading more states for better user feedback. - Refactored HomeScreen and PostDetailScreen to utilize the new loading states and merging logic, enhancing user experience during data fetching. - Improved pagination handling in useCursorPagination and useDifferentialPosts to ensure consistent data management across components.
822 lines
26 KiB
TypeScript
822 lines
26 KiB
TypeScript
/**
|
||
* 首页 HomeScreen
|
||
* 胡萝卜BBS - 首页展示
|
||
* 支持列表和多列网格模式(响应式布局)
|
||
*/
|
||
|
||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||
import {
|
||
View,
|
||
FlatList,
|
||
ScrollView,
|
||
StyleSheet,
|
||
RefreshControl,
|
||
StatusBar,
|
||
TouchableOpacity,
|
||
NativeSyntheticEvent,
|
||
Alert,
|
||
Clipboard,
|
||
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 { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
||
import { Post } from '../../types';
|
||
import { useUserStore } from '../../stores';
|
||
import { useCurrentUser } from '../../stores/authStore';
|
||
import { postService } from '../../services';
|
||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||
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>;
|
||
|
||
const TABS = ['推荐', '关注', '热门', '最新'];
|
||
const TAB_ICONS = ['compass-outline', 'account-heart-outline', 'fire', 'clock-outline'];
|
||
const DEFAULT_PAGE_SIZE = 20;
|
||
const SWIPE_TRANSLATION_THRESHOLD = 40;
|
||
const SWIPE_COOLDOWN_MS = 300;
|
||
const MOBILE_TAB_BAR_HEIGHT = 64;
|
||
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
||
const MOBILE_FAB_GAP = 12;
|
||
|
||
type ViewMode = 'list' | 'grid';
|
||
type PostType = 'recommend' | 'follow' | 'hot' | 'latest';
|
||
|
||
export const HomeScreen: React.FC = () => {
|
||
const navigation = useNavigation<NavigationProp>();
|
||
const insets = useSafeAreaInsets();
|
||
const { posts: storePosts } = useUserStore();
|
||
const currentUser = useCurrentUser();
|
||
|
||
// 使用响应式 hook
|
||
const {
|
||
width,
|
||
isMobile,
|
||
isTablet,
|
||
isDesktop,
|
||
isWideScreen,
|
||
} = useResponsive();
|
||
|
||
// 响应式间距
|
||
const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
|
||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||
|
||
const [activeIndex, setActiveIndex] = useState(0);
|
||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||
|
||
// 图片查看器状态
|
||
const [showImageViewer, setShowImageViewer] = useState(false);
|
||
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||
|
||
// 搜索显示状态(用于内嵌搜索页面)
|
||
const [showSearch, setShowSearch] = useState(false);
|
||
|
||
// 发帖弹窗状态
|
||
const [showCreatePost, setShowCreatePost] = useState(false);
|
||
|
||
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
||
const postIdsRef = useRef<Set<string>>(new Set());
|
||
const lastSwipeAtRef = useRef(0);
|
||
|
||
// 网格模式滚动位置检测
|
||
const gridScrollYRef = useRef(0);
|
||
const isLoadingMoreRef = useRef(false);
|
||
|
||
// 构建一个以 postId 为 key 的 map,用于快速查找
|
||
const postsMap = useMemo(() => {
|
||
const map = new Map<string, Post>();
|
||
storePosts.forEach(post => {
|
||
map.set(post.id, post);
|
||
});
|
||
return map;
|
||
}, [storePosts]);
|
||
|
||
// 获取当前 tab 对应的帖子类型
|
||
const getPostType = useCallback((): PostType => {
|
||
switch (activeIndex) {
|
||
case 0: return 'recommend';
|
||
case 1: return 'follow';
|
||
case 2: return 'hot';
|
||
case 3: return 'latest';
|
||
default: return 'recommend';
|
||
}
|
||
}, [activeIndex]);
|
||
|
||
// 使用差异更新 Hook 获取帖子列表
|
||
const listKey = useMemo(() => `home_${getPostType()}`, [getPostType]);
|
||
|
||
const {
|
||
posts,
|
||
loading: isLoading,
|
||
isInitialLoading,
|
||
isLoadingMore,
|
||
refreshing: isRefreshing,
|
||
hasMore,
|
||
error,
|
||
reset,
|
||
} = useDifferentialPosts<Post>(
|
||
[],
|
||
{
|
||
listKey,
|
||
enableDiff: true,
|
||
enableBatching: true,
|
||
autoSubscribe: true,
|
||
}
|
||
);
|
||
|
||
// 加载更多方法
|
||
const loadMore = useCallback(async () => {
|
||
if (hasMore && !isLoadingMore && !isLoadingMoreRef.current) {
|
||
isLoadingMoreRef.current = true;
|
||
const startedAt = Date.now();
|
||
try {
|
||
await processPostUseCase.loadMorePosts(listKey);
|
||
console.log('[PostPerf] home loadMore', {
|
||
listKey,
|
||
costMs: Date.now() - startedAt,
|
||
totalItems: posts.length,
|
||
});
|
||
} catch (err) {
|
||
console.error('加载更多失败:', err);
|
||
} finally {
|
||
isLoadingMoreRef.current = false;
|
||
}
|
||
}
|
||
}, [posts.length, listKey, hasMore, isLoadingMore]);
|
||
|
||
// 网格模式滚动处理 - 检测是否滚动到底部
|
||
const handleGridScroll = useCallback((event: any) => {
|
||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
||
const scrollY = contentOffset.y;
|
||
const visibleHeight = layoutMeasurement.height;
|
||
const contentHeight = contentSize.height;
|
||
|
||
// 当滚动到距离底部 200px 时触发加载更多
|
||
if (scrollY + visibleHeight >= contentHeight - 200) {
|
||
loadMore();
|
||
}
|
||
}, [loadMore]);
|
||
|
||
// 刷新方法 - 先获取正确类型的帖子,再刷新
|
||
const refresh = useCallback(async () => {
|
||
try {
|
||
// 先获取正确类型的帖子
|
||
await processPostUseCase.fetchPosts(
|
||
{ page: 1, pageSize: DEFAULT_PAGE_SIZE, post_type: getPostType() },
|
||
listKey
|
||
);
|
||
} catch (err) {
|
||
console.error('刷新失败:', err);
|
||
}
|
||
}, [listKey, getPostType]);
|
||
|
||
// Tab 切换时刷新数据并重置列表
|
||
useEffect(() => {
|
||
reset();
|
||
refresh();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [activeIndex]);
|
||
|
||
// 将获取到的原始帖子与 store 中的状态合并
|
||
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
||
const displayPosts = useMemo(() => {
|
||
return posts.map(post => {
|
||
const storePost = postsMap.get(post.id);
|
||
if (storePost) {
|
||
// 如果 store 中有这个帖子,使用 store 的最新状态
|
||
return storePost;
|
||
}
|
||
// 如果 store 中没有这个帖子,使用原始数据
|
||
return post;
|
||
});
|
||
}, [posts, postsMap]);
|
||
|
||
// 根据屏幕尺寸确定网格列数
|
||
const gridColumns = useMemo(() => {
|
||
if (isWideScreen || width >= 1440) return 4;
|
||
if (isDesktop || width >= 1024) return 3;
|
||
if (isTablet || width >= 768) return 2;
|
||
return 2; // 移动端瀑布流保持2列
|
||
}, [width, isTablet, isDesktop, isWideScreen]);
|
||
|
||
// 列表模式下始终使用单列,宽屏下居中显示
|
||
const useMultiColumnList = useMemo(() => {
|
||
return false; // 改为始终返回false,使用单列布局
|
||
}, []);
|
||
|
||
// 宽屏下内容最大宽度
|
||
const contentMaxWidth = useMemo(() => {
|
||
if (isWideScreen) return 800;
|
||
if (isDesktop) return 720;
|
||
if (isTablet) return 640;
|
||
return width; // 移动端使用全宽
|
||
}, [width, isTablet, isDesktop, isWideScreen]);
|
||
|
||
// 列表模式横向内边距:移动端适当收窄,减少两侧空白
|
||
const listHorizontalPadding = useMemo(() => {
|
||
if (isMobile) {
|
||
return Math.max(6, responsivePadding - responsiveGap);
|
||
}
|
||
return responsivePadding;
|
||
}, [isMobile, responsivePadding, responsiveGap]);
|
||
|
||
// 列表模式卡片宽度:宽屏限宽并居中,移动端占用更多可用宽度
|
||
const listItemWidth = useMemo(() => {
|
||
const availableWidth = Math.max(0, width - listHorizontalPadding * 2);
|
||
if (isDesktop || isWideScreen) {
|
||
return Math.min(contentMaxWidth, availableWidth);
|
||
}
|
||
return availableWidth;
|
||
}, [width, listHorizontalPadding, isDesktop, isWideScreen, contentMaxWidth]);
|
||
|
||
const floatingButtonBottom = useMemo(() => {
|
||
if (!isMobile) {
|
||
return undefined;
|
||
}
|
||
// TabBar 悬浮在底部,发帖按钮需要在 TabBar 上方
|
||
// TabBar 高度 64 + TabBar 浮动间距 12 + 按钮与 TabBar 间距 16
|
||
return MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN * 2 + MOBILE_FAB_GAP + insets.bottom;
|
||
}, [isMobile, insets.bottom]);
|
||
|
||
// 切换视图模式
|
||
const toggleViewMode = () => {
|
||
setViewMode(prev => prev === 'list' ? 'grid' : 'list');
|
||
};
|
||
|
||
// 切换Tab(手势/点击共用)
|
||
const changeTab = useCallback((nextIndex: number) => {
|
||
if (nextIndex < 0 || nextIndex >= TABS.length || nextIndex === activeIndex) {
|
||
return;
|
||
}
|
||
setActiveIndex(nextIndex);
|
||
}, [activeIndex]);
|
||
|
||
const handleSwipeTabChange = useCallback((translationX: number) => {
|
||
setActiveIndex(prev => (
|
||
translationX < 0
|
||
? Math.min(prev + 1, TABS.length - 1)
|
||
: Math.max(prev - 1, 0)
|
||
));
|
||
}, []);
|
||
|
||
const swipeGesture = useMemo(() => (
|
||
Gesture.Pan()
|
||
.runOnJS(true)
|
||
.activeOffsetX([-15, 15])
|
||
.failOffsetY([-20, 20])
|
||
.shouldCancelWhenOutside(true)
|
||
.onEnd((event) => {
|
||
const now = Date.now();
|
||
if (now - lastSwipeAtRef.current < SWIPE_COOLDOWN_MS) {
|
||
return;
|
||
}
|
||
|
||
if (Math.abs(event.translationX) < SWIPE_TRANSLATION_THRESHOLD) {
|
||
return;
|
||
}
|
||
|
||
lastSwipeAtRef.current = now;
|
||
handleSwipeTabChange(event.translationX);
|
||
})
|
||
), [handleSwipeTabChange]);
|
||
|
||
// 跳转到搜索页(使用内嵌模式,不再依赖导航)
|
||
const handleSearchPress = () => {
|
||
setShowSearch(true);
|
||
};
|
||
|
||
// 跳转到帖子详情
|
||
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
|
||
navigationService.navigate('PostDetail', { postId, scrollToComments });
|
||
};
|
||
|
||
// 跳转到用户主页
|
||
const handleUserPress = (userId: string) => {
|
||
navigationService.navigate('UserProfile', { userId });
|
||
};
|
||
|
||
// 点赞帖子
|
||
const handleLike = async (post: Post) => {
|
||
try {
|
||
if (post.is_liked) {
|
||
await processPostUseCase.unlikePost(post.id);
|
||
} else {
|
||
await processPostUseCase.likePost(post.id);
|
||
}
|
||
} catch (error) {
|
||
console.error('点赞操作失败:', error);
|
||
}
|
||
};
|
||
|
||
// 收藏帖子
|
||
const handleBookmark = async (post: Post) => {
|
||
try {
|
||
if (post.is_favorited) {
|
||
await processPostUseCase.unfavoritePost(post.id);
|
||
} else {
|
||
await processPostUseCase.favoritePost(post.id);
|
||
}
|
||
} catch (error) {
|
||
console.error('收藏操作失败:', error);
|
||
}
|
||
};
|
||
|
||
// 分享帖子
|
||
const handleShare = async (post: Post) => {
|
||
if (!post?.id) return;
|
||
try {
|
||
await postService.sharePost(post.id);
|
||
} catch (shareError) {
|
||
console.error('上报分享次数失败:', shareError);
|
||
}
|
||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||
Clipboard.setString(postUrl);
|
||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||
};
|
||
|
||
// 删除帖子 - 删除后刷新列表
|
||
const handleDeletePost = async (postId: string) => {
|
||
try {
|
||
const success = await postService.deletePost(postId);
|
||
if (success) {
|
||
// 刷新列表以移除已删除的帖子
|
||
refresh();
|
||
} else {
|
||
console.error('删除帖子失败');
|
||
}
|
||
} catch (deleteError) {
|
||
console.error('删除帖子失败:', deleteError);
|
||
throw deleteError; // 重新抛出错误,让 PostCard 处理错误提示
|
||
}
|
||
};
|
||
|
||
// 处理图片点击 - 打开图片查看器
|
||
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
||
setPostImages(images);
|
||
setSelectedImageIndex(index);
|
||
setShowImageViewer(true);
|
||
};
|
||
|
||
// 跳转到发帖页面(使用 Modal 方式)
|
||
const handleCreatePost = () => {
|
||
setShowCreatePost(true);
|
||
};
|
||
|
||
// 渲染帖子卡片(列表模式)
|
||
const keyExtractor = useCallback((item: Post) => item.id, []);
|
||
|
||
const renderPostList = useCallback(({ item }: { item: Post }) => {
|
||
const authorId = item.author?.id || '';
|
||
const isPostAuthor = currentUser?.id === authorId;
|
||
return (
|
||
<View style={[
|
||
styles.listItem,
|
||
{
|
||
marginBottom: responsiveGap,
|
||
width: listItemWidth,
|
||
alignSelf: 'center',
|
||
borderRadius: isMobile ? borderRadius.lg : 0,
|
||
overflow: isMobile ? 'hidden' : 'visible',
|
||
}
|
||
]}>
|
||
<PostCard
|
||
post={item}
|
||
onPress={() => handlePostPress(item.id)}
|
||
onUserPress={() => authorId && handleUserPress(authorId)}
|
||
onLike={() => handleLike(item)}
|
||
onComment={() => handlePostPress(item.id, true)}
|
||
onBookmark={() => handleBookmark(item)}
|
||
onShare={() => handleShare(item)}
|
||
onImagePress={(images, index) => handleImagePress(images, index)}
|
||
onDelete={() => handleDeletePost(item.id)}
|
||
isPostAuthor={isPostAuthor}
|
||
/>
|
||
</View>
|
||
);
|
||
}, [currentUser?.id, handleBookmark, handleDeletePost, handleImagePress, handleLike, handlePostPress, handleShare, handleUserPress, isMobile, listItemWidth, responsiveGap]);
|
||
|
||
// 估算帖子在瀑布流中的高度(用于均匀分配)
|
||
const estimatePostHeight = (post: Post, columnWidth: number): number => {
|
||
const hasImage = post.images && post.images.length > 0;
|
||
const hasTitle = !!post.title;
|
||
const hasContent = !!post.content;
|
||
|
||
let height = 0;
|
||
|
||
// 图片区域高度(如果有图)
|
||
if (hasImage) {
|
||
// 使用帖子 ID 生成一致的宽高比
|
||
const hash = post.id.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
||
const aspectRatios = [0.7, 0.75, 0.8, 0.85, 0.9, 1, 1.1, 1.2];
|
||
const aspectRatio = aspectRatios[hash % aspectRatios.length];
|
||
height += columnWidth / aspectRatio;
|
||
} else {
|
||
// 无图帖子显示正文预览区域
|
||
if (hasContent) {
|
||
// 根据内容长度估算高度(每行约20像素,最多6行)
|
||
const contentLength = post.content?.length || 0;
|
||
const estimatedLines = Math.min(6, Math.max(3, Math.ceil(contentLength / 20)));
|
||
height += 16 + estimatedLines * 20; // padding + 文本高度
|
||
}
|
||
}
|
||
|
||
// 标题高度
|
||
if (hasTitle) {
|
||
const titleLines = hasImage ? 2 : 3;
|
||
height += 8 + titleLines * 20; // paddingTop + 文本高度
|
||
}
|
||
|
||
// 底部信息栏高度
|
||
height += 40; // 用户信息 + 点赞数
|
||
|
||
// 间距
|
||
height += 2; // marginBottom
|
||
|
||
return height;
|
||
};
|
||
|
||
// 将帖子分成多列(瀑布流)- 使用贪心算法使各列高度尽量均匀
|
||
const distributePostsToColumns = useMemo(() => {
|
||
const columns: Post[][] = Array.from({ length: gridColumns }, () => []);
|
||
const columnHeights: number[] = Array(gridColumns).fill(0);
|
||
|
||
// 防御性检查:确保 posts 存在且是数组
|
||
if (!displayPosts || !Array.isArray(displayPosts) || displayPosts.length === 0) {
|
||
return columns;
|
||
}
|
||
|
||
// 计算单列宽度
|
||
const totalGap = (gridColumns - 1) * responsiveGap;
|
||
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
|
||
|
||
displayPosts.forEach((post) => {
|
||
const postHeight = estimatePostHeight(post, columnWidth);
|
||
|
||
// 找到当前高度最小的列
|
||
const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
||
columns[minHeightIndex].push(post);
|
||
columnHeights[minHeightIndex] += postHeight;
|
||
});
|
||
|
||
return columns;
|
||
}, [displayPosts, gridColumns, width, responsiveGap, responsivePadding]);
|
||
|
||
// 渲染单列帖子
|
||
const renderWaterfallColumn = (column: Post[], columnIndex: number) => (
|
||
<View key={`column-${columnIndex}`} style={[styles.waterfallColumn, { marginRight: columnIndex < gridColumns - 1 ? responsiveGap : 0 }]}>
|
||
{column.map(post => {
|
||
const authorId = post.author?.id || '';
|
||
const isPostAuthor = currentUser?.id === authorId;
|
||
return (
|
||
<View key={post.id} style={[styles.waterfallItem, { marginBottom: responsiveGap }]}>
|
||
<PostCard
|
||
post={post}
|
||
variant="grid"
|
||
onPress={() => handlePostPress(post.id)}
|
||
onUserPress={() => authorId && handleUserPress(authorId)}
|
||
onLike={() => handleLike(post)}
|
||
onComment={() => handlePostPress(post.id, true)}
|
||
onBookmark={() => handleBookmark(post)}
|
||
onShare={() => handleShare(post)}
|
||
onImagePress={(images, index) => handleImagePress(images, index)}
|
||
onDelete={() => handleDeletePost(post.id)}
|
||
isPostAuthor={isPostAuthor}
|
||
/>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
);
|
||
|
||
// 渲染响应式网格布局(平板/桌面端使用多列)
|
||
const renderResponsiveGrid = () => {
|
||
if (isMobile && !isTablet) {
|
||
// 移动端使用瀑布流布局(2列)
|
||
return (
|
||
<ScrollView
|
||
style={styles.waterfallScroll}
|
||
contentContainerStyle={[
|
||
styles.waterfallContainer,
|
||
{
|
||
paddingHorizontal: responsivePadding,
|
||
paddingBottom: 80 + responsivePadding,
|
||
}
|
||
]}
|
||
showsVerticalScrollIndicator={false}
|
||
scrollEventThrottle={100}
|
||
onScroll={handleGridScroll}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={isRefreshing}
|
||
onRefresh={refresh}
|
||
colors={[colors.primary.main]}
|
||
tintColor={colors.primary.main}
|
||
/>
|
||
}
|
||
>
|
||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||
{/* 加载更多指示器 */}
|
||
{isLoading && displayPosts.length > 0 && (
|
||
<View style={styles.loadingMore}>
|
||
<Loading size="sm" />
|
||
</View>
|
||
)}
|
||
</ScrollView>
|
||
);
|
||
}
|
||
|
||
// 平板/桌面端使用 ResponsiveGrid 组件
|
||
return (
|
||
<ResponsiveGrid
|
||
columns={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, '2xl': 4, '3xl': 4, '4xl': 5 }}
|
||
gap={{ xs: 8, sm: 12, md: 16, lg: 20, xl: 24 }}
|
||
containerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 }}
|
||
>
|
||
{displayPosts.map(post => {
|
||
const authorId = post.author?.id || '';
|
||
const isPostAuthor = currentUser?.id === authorId;
|
||
return (
|
||
<PostCard
|
||
key={post.id}
|
||
post={post}
|
||
variant={viewMode === 'grid' ? 'grid' : 'default'}
|
||
onPress={() => handlePostPress(post.id)}
|
||
onUserPress={() => authorId && handleUserPress(authorId)}
|
||
onLike={() => handleLike(post)}
|
||
onComment={() => handlePostPress(post.id, true)}
|
||
onBookmark={() => handleBookmark(post)}
|
||
onShare={() => handleShare(post)}
|
||
onImagePress={(images, index) => handleImagePress(images, index)}
|
||
onDelete={() => handleDeletePost(post.id)}
|
||
isPostAuthor={isPostAuthor}
|
||
/>
|
||
);
|
||
})}
|
||
</ResponsiveGrid>
|
||
);
|
||
};
|
||
|
||
// 渲染空状态
|
||
const renderEmpty = useCallback(() => {
|
||
if (isInitialLoading) return null;
|
||
|
||
return (
|
||
<EmptyState
|
||
title="暂无内容"
|
||
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'}
|
||
/>
|
||
);
|
||
}, [activeIndex, isInitialLoading]);
|
||
|
||
// 渲染列表内容
|
||
const renderListContent = () => {
|
||
if (useMultiColumnList) {
|
||
// 平板/桌面端使用多列网格
|
||
return renderResponsiveGrid();
|
||
}
|
||
|
||
// 移动端和宽屏都使用单列 FlatList,宽屏下居中显示
|
||
return (
|
||
<FlatList
|
||
data={displayPosts}
|
||
renderItem={renderPostList}
|
||
keyExtractor={keyExtractor}
|
||
contentContainerStyle={[
|
||
styles.listContent,
|
||
{
|
||
paddingHorizontal: listHorizontalPadding,
|
||
paddingBottom: 80 + responsivePadding,
|
||
alignItems: 'center', // 确保子项居中
|
||
}
|
||
]}
|
||
showsVerticalScrollIndicator={false}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={isRefreshing}
|
||
onRefresh={refresh}
|
||
colors={[colors.primary.main]}
|
||
tintColor={colors.primary.main}
|
||
/>
|
||
}
|
||
onEndReached={loadMore}
|
||
onEndReachedThreshold={0.3}
|
||
ListEmptyComponent={renderEmpty}
|
||
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
|
||
initialNumToRender={8}
|
||
maxToRenderPerBatch={8}
|
||
updateCellsBatchingPeriod={60}
|
||
windowSize={7}
|
||
removeClippedSubviews
|
||
/>
|
||
);
|
||
};
|
||
|
||
// 搜索页面内嵌模式
|
||
if (showSearch) {
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
|
||
<SearchScreen
|
||
onBack={() => setShowSearch(false)}
|
||
/>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
// 正常首页内容
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
|
||
|
||
{/* 顶部Header */}
|
||
<View style={styles.header}>
|
||
{/* 搜索栏 */}
|
||
<View style={[styles.searchWrapper, { paddingHorizontal: responsivePadding }]}>
|
||
<SearchBar
|
||
value=""
|
||
onChangeText={() => {}}
|
||
onSubmit={handleSearchPress}
|
||
onFocus={handleSearchPress}
|
||
placeholder="搜索帖子、用户"
|
||
/>
|
||
</View>
|
||
|
||
{/* Tab切换 */}
|
||
<TabBar
|
||
tabs={TABS}
|
||
icons={TAB_ICONS}
|
||
activeIndex={activeIndex}
|
||
onTabChange={changeTab}
|
||
variant="modern"
|
||
rightContent={
|
||
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
|
||
<MaterialCommunityIcons
|
||
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
|
||
size={22}
|
||
color="#666"
|
||
/>
|
||
</TouchableOpacity>
|
||
}
|
||
/>
|
||
</View>
|
||
|
||
{/* 帖子列表 */}
|
||
{isMobile ? (
|
||
// 移动端:使用 GestureDetector 支持手势切换 Tab
|
||
<GestureDetector gesture={swipeGesture}>
|
||
<View style={styles.contentContainer}>
|
||
{isInitialLoading ? (
|
||
<Loading fullScreen />
|
||
) : viewMode === 'list' ? (
|
||
renderListContent()
|
||
) : (
|
||
// 网格模式:使用响应式网格
|
||
renderResponsiveGrid()
|
||
)}
|
||
</View>
|
||
</GestureDetector>
|
||
) : (
|
||
// 桌面端/平板端:不使用 GestureDetector,避免拦截侧边栏点击
|
||
<View style={styles.contentContainer}>
|
||
{isInitialLoading ? (
|
||
<Loading fullScreen />
|
||
) : viewMode === 'list' ? (
|
||
renderListContent()
|
||
) : (
|
||
// 网格模式:使用响应式网格
|
||
renderResponsiveGrid()
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
{/* 漂浮发帖按钮 */}
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.floatingButton,
|
||
isDesktop && styles.floatingButtonDesktop,
|
||
isWideScreen && styles.floatingButtonWide,
|
||
floatingButtonBottom !== undefined && { bottom: floatingButtonBottom },
|
||
]}
|
||
onPress={handleCreatePost}
|
||
activeOpacity={0.8}
|
||
>
|
||
<MaterialCommunityIcons name="plus" size={28} color={colors.text.inverse} />
|
||
</TouchableOpacity>
|
||
|
||
{/* 图片查看器 */}
|
||
<ImageGallery
|
||
visible={showImageViewer}
|
||
images={postImages.map(img => ({
|
||
id: img.id || img.url || String(Math.random()),
|
||
url: img.url || img.uri || ''
|
||
}))}
|
||
initialIndex={selectedImageIndex}
|
||
onClose={() => setShowImageViewer(false)}
|
||
enableSave
|
||
/>
|
||
|
||
{/* 发帖弹窗 */}
|
||
<Modal
|
||
visible={showCreatePost}
|
||
animationType="slide"
|
||
presentationStyle="pageSheet"
|
||
onRequestClose={() => setShowCreatePost(false)}
|
||
>
|
||
<CreatePostScreen
|
||
onClose={() => setShowCreatePost(false)}
|
||
/>
|
||
</Modal>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.background.default,
|
||
},
|
||
header: {
|
||
backgroundColor: colors.background.paper,
|
||
},
|
||
searchWrapper: {
|
||
paddingBottom: spacing.sm,
|
||
// 移除阴影效果
|
||
shadowColor: 'transparent',
|
||
shadowOffset: { width: 0, height: 0 },
|
||
shadowOpacity: 0,
|
||
shadowRadius: 0,
|
||
elevation: 0,
|
||
},
|
||
viewToggleBtn: {
|
||
width: 44,
|
||
height: 44,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
listContent: {
|
||
flexGrow: 1,
|
||
},
|
||
contentContainer: {
|
||
flex: 1,
|
||
},
|
||
listItem: {
|
||
// 动态设置 marginBottom
|
||
},
|
||
waterfallScroll: {
|
||
flex: 1,
|
||
},
|
||
waterfallContainer: {
|
||
flexDirection: 'row',
|
||
flexGrow: 1,
|
||
alignItems: 'flex-start',
|
||
},
|
||
waterfallColumn: {
|
||
flex: 1,
|
||
},
|
||
waterfallItem: {
|
||
// 动态设置 marginBottom
|
||
},
|
||
floatingButton: {
|
||
position: 'absolute',
|
||
right: 20,
|
||
bottom: 20,
|
||
width: 56,
|
||
height: 56,
|
||
borderRadius: 28,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
...shadows.lg,
|
||
},
|
||
floatingButtonDesktop: {
|
||
right: 40,
|
||
bottom: 40,
|
||
width: 64,
|
||
height: 64,
|
||
borderRadius: 32,
|
||
},
|
||
floatingButtonWide: {
|
||
right: 60,
|
||
bottom: 60,
|
||
width: 72,
|
||
height: 72,
|
||
borderRadius: 36,
|
||
},
|
||
loadingMore: {
|
||
paddingVertical: 20,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
});
|