Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
767
src/screens/home/HomeScreen.tsx
Normal file
767
src/screens/home/HomeScreen.tsx
Normal file
@@ -0,0 +1,767 @@
|
||||
/**
|
||||
* 首页 HomeScreen
|
||||
* 胡萝卜BBS - 首页展示
|
||||
* 支持列表和多列网格模式(响应式布局)
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
RefreshControl,
|
||||
StatusBar,
|
||||
TouchableOpacity,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
} 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';
|
||||
|
||||
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 SCROLL_BOTTOM_THRESHOLD = 240;
|
||||
const LOAD_MORE_COOLDOWN_MS = 800;
|
||||
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';
|
||||
|
||||
export const HomeScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { fetchPosts, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 使用响应式 hook
|
||||
const {
|
||||
width,
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
isWideScreen,
|
||||
breakpoint,
|
||||
orientation,
|
||||
isLandscape
|
||||
} = 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 [posts, setPosts] = useState<Post[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
|
||||
// 图片查看器状态
|
||||
const [showImageViewer, setShowImageViewer] = useState(false);
|
||||
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
|
||||
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
||||
const postIdsRef = React.useRef<Set<string>>(new Set());
|
||||
const inFlightRequestKeysRef = React.useRef<Set<string>>(new Set());
|
||||
const lastLoadMoreTriggerAtRef = useRef(0);
|
||||
const lastSwipeAtRef = useRef(0);
|
||||
|
||||
// 用 ref 同步关键状态,避免 onWaterfallScroll 的陈旧闭包问题
|
||||
const pageRef = useRef(page);
|
||||
const loadingMoreRef = useRef(loadingMore);
|
||||
const hasMoreRef = useRef(hasMore);
|
||||
pageRef.current = page;
|
||||
loadingMoreRef.current = loadingMore;
|
||||
hasMoreRef.current = hasMore;
|
||||
|
||||
// 根据屏幕尺寸确定网格列数
|
||||
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;
|
||||
}
|
||||
return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP;
|
||||
}, [isMobile, insets.bottom]);
|
||||
|
||||
const appendUniquePosts = useCallback((prevPosts: Post[], incomingPosts: Post[]) => {
|
||||
if (incomingPosts.length === 0) return prevPosts;
|
||||
const seenIds = new Set(prevPosts.map(item => item.id));
|
||||
const dedupedIncoming = incomingPosts.filter(item => {
|
||||
if (seenIds.has(item.id)) return false;
|
||||
seenIds.add(item.id);
|
||||
return true;
|
||||
});
|
||||
return dedupedIncoming.length > 0 ? [...prevPosts, ...dedupedIncoming] : prevPosts;
|
||||
}, []);
|
||||
|
||||
const uniquePostsById = useCallback((items: Post[]) => {
|
||||
if (items.length <= 1) return items;
|
||||
const map = new Map<string, Post>();
|
||||
for (const item of items) {
|
||||
map.set(item.id, item);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}, []);
|
||||
|
||||
const getPostType = (): 'recommend' | 'follow' | 'hot' | 'latest' => {
|
||||
switch (activeIndex) {
|
||||
case 0: return 'recommend';
|
||||
case 1: return 'follow';
|
||||
case 2: return 'hot';
|
||||
case 3: return 'latest';
|
||||
default: return 'recommend';
|
||||
}
|
||||
};
|
||||
|
||||
// 加载帖子列表
|
||||
const loadPosts = useCallback(async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||
const postType = getPostType();
|
||||
const requestKey = `${postType}:${pageNum}`;
|
||||
if (inFlightRequestKeysRef.current.has(requestKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inFlightRequestKeysRef.current.add(requestKey);
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else if (pageNum === 1) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoadingMore(true);
|
||||
}
|
||||
|
||||
console.log('[HomeScreen] loadPosts - activeIndex:', activeIndex, 'postType:', postType);
|
||||
const response = await fetchPosts(postType, pageNum);
|
||||
const newPosts = response.list || [];
|
||||
|
||||
if (isRefresh) {
|
||||
setPosts(uniquePostsById(newPosts));
|
||||
setPage(1);
|
||||
} else if (pageNum === 1) {
|
||||
setPosts(uniquePostsById(newPosts));
|
||||
setPage(1);
|
||||
} else {
|
||||
setPosts(prev => appendUniquePosts(prev, newPosts));
|
||||
setPage(pageNum);
|
||||
}
|
||||
|
||||
const hasMoreByPage = response.total_pages > 0 ? response.page < response.total_pages : false;
|
||||
const hasMoreBySize = newPosts.length >= (response.page_size || DEFAULT_PAGE_SIZE);
|
||||
setHasMore(hasMoreByPage || hasMoreBySize);
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts:', error);
|
||||
} finally {
|
||||
inFlightRequestKeysRef.current.delete(requestKey);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [fetchPosts, activeIndex, appendUniquePosts, uniquePostsById]);
|
||||
|
||||
// 切换Tab时重新加载
|
||||
useEffect(() => {
|
||||
loadPosts(1, true);
|
||||
}, [activeIndex]);
|
||||
|
||||
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
|
||||
useEffect(() => {
|
||||
if (posts.length === 0) return;
|
||||
|
||||
// 更新 postIdsRef
|
||||
const currentPostIds = new Set(posts.map(p => p.id));
|
||||
postIdsRef.current = currentPostIds;
|
||||
|
||||
// 从 store 中找到对应的帖子并同步状态
|
||||
let hasChanges = false;
|
||||
const updatedPosts = posts.map(localPost => {
|
||||
const storePost = storePosts.find(sp => sp.id === localPost.id);
|
||||
if (storePost && (
|
||||
storePost.is_liked !== localPost.is_liked ||
|
||||
storePost.is_favorited !== localPost.is_favorited ||
|
||||
storePost.likes_count !== localPost.likes_count ||
|
||||
storePost.favorites_count !== localPost.favorites_count
|
||||
)) {
|
||||
hasChanges = true;
|
||||
return {
|
||||
...localPost,
|
||||
is_liked: storePost.is_liked,
|
||||
is_favorited: storePost.is_favorited,
|
||||
likes_count: storePost.likes_count,
|
||||
favorites_count: storePost.favorites_count,
|
||||
};
|
||||
}
|
||||
return localPost;
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
setPosts(updatedPosts);
|
||||
}
|
||||
}, [storePosts]);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = useCallback(() => {
|
||||
loadPosts(1, true);
|
||||
}, [loadPosts]);
|
||||
|
||||
// 上拉加载更多
|
||||
const onEndReached = useCallback(() => {
|
||||
if (!loadingMoreRef.current && hasMoreRef.current) {
|
||||
loadPosts(pageRef.current + 1);
|
||||
}
|
||||
}, [loadPosts]);
|
||||
|
||||
const onWaterfallScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (loadingMoreRef.current || !hasMoreRef.current) return;
|
||||
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
||||
const distanceToBottom = contentSize.height - (contentOffset.y + layoutMeasurement.height);
|
||||
const now = Date.now();
|
||||
if (distanceToBottom <= SCROLL_BOTTOM_THRESHOLD) {
|
||||
if (now - lastLoadMoreTriggerAtRef.current < LOAD_MORE_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
lastLoadMoreTriggerAtRef.current = now;
|
||||
loadPosts(pageRef.current + 1);
|
||||
}
|
||||
}, [loadPosts]);
|
||||
|
||||
// 切换视图模式
|
||||
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])
|
||||
.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 = () => {
|
||||
navigation.navigate('Search');
|
||||
};
|
||||
|
||||
// 跳转到帖子详情
|
||||
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
|
||||
navigation.getParent()?.navigate('PostDetail', { postId, scrollToComments });
|
||||
};
|
||||
|
||||
// 跳转到用户主页
|
||||
const handleUserPress = (userId: string) => {
|
||||
navigation.getParent()?.navigate('UserProfile', { userId });
|
||||
};
|
||||
|
||||
// 点赞帖子
|
||||
const handleLike = (post: Post) => {
|
||||
if (post.is_liked) {
|
||||
unlikePost(post.id);
|
||||
} else {
|
||||
likePost(post.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏帖子
|
||||
const handleBookmark = (post: Post) => {
|
||||
if (post.is_favorited) {
|
||||
unfavoritePost(post.id);
|
||||
} else {
|
||||
favoritePost(post.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = (post: Post) => {
|
||||
console.log('Share post:', post.id);
|
||||
};
|
||||
|
||||
// 删除帖子
|
||||
const handleDeletePost = async (postId: string) => {
|
||||
try {
|
||||
const success = await postService.deletePost(postId);
|
||||
if (success) {
|
||||
// 从列表中移除已删除的帖子
|
||||
setPosts(prev => prev.filter(p => p.id !== postId));
|
||||
} else {
|
||||
console.error('删除帖子失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除帖子失败:', error);
|
||||
throw error; // 重新抛出错误,让 PostCard 处理错误提示
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片点击 - 打开图片查看器
|
||||
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
||||
setPostImages(images);
|
||||
setSelectedImageIndex(index);
|
||||
setShowImageViewer(true);
|
||||
};
|
||||
|
||||
// 跳转到发帖页面
|
||||
const handleCreatePost = () => {
|
||||
navigation.getParent()?.navigate('CreatePost');
|
||||
};
|
||||
|
||||
// 渲染帖子卡片(列表模式)
|
||||
const renderPostList = ({ 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>
|
||||
);
|
||||
};
|
||||
|
||||
// 估算帖子在瀑布流中的高度(用于均匀分配)
|
||||
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);
|
||||
|
||||
// 计算单列宽度
|
||||
const totalGap = (gridColumns - 1) * responsiveGap;
|
||||
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
|
||||
|
||||
posts.forEach((post) => {
|
||||
const postHeight = estimatePostHeight(post, columnWidth);
|
||||
|
||||
// 找到当前高度最小的列
|
||||
const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
||||
columns[minHeightIndex].push(post);
|
||||
columnHeights[minHeightIndex] += postHeight;
|
||||
});
|
||||
|
||||
return columns;
|
||||
}, [posts, 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}
|
||||
onScroll={onWaterfallScroll}
|
||||
scrollEventThrottle={100}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||||
</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 }}
|
||||
>
|
||||
{posts.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 = () => {
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
title="暂无内容"
|
||||
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染列表内容
|
||||
const renderListContent = () => {
|
||||
if (useMultiColumnList) {
|
||||
// 平板/桌面端使用多列网格
|
||||
return renderResponsiveGrid();
|
||||
}
|
||||
|
||||
// 移动端和宽屏都使用单列 FlatList,宽屏下居中显示
|
||||
return (
|
||||
<FlatList
|
||||
data={posts}
|
||||
renderItem={renderPostList}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={[
|
||||
styles.listContent,
|
||||
{
|
||||
paddingHorizontal: listHorizontalPadding,
|
||||
paddingBottom: 80 + responsivePadding,
|
||||
alignItems: 'center', // 确保子项居中
|
||||
}
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.3}
|
||||
ListEmptyComponent={renderEmpty}
|
||||
ListFooterComponent={loadingMore ? <Loading size="sm" /> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<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>
|
||||
|
||||
{/* 帖子列表 */}
|
||||
<GestureDetector gesture={swipeGesture}>
|
||||
<View style={styles.contentContainer}>
|
||||
{viewMode === 'list' ? (
|
||||
renderListContent()
|
||||
) : (
|
||||
// 网格模式:使用响应式网格
|
||||
renderResponsiveGrid()
|
||||
)}
|
||||
</View>
|
||||
</GestureDetector>
|
||||
|
||||
{/* 漂浮发帖按钮 */}
|
||||
<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
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
searchWrapper: {
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user