Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts. Made-with: Cursor
777 lines
24 KiB
TypeScript
777 lines
24 KiB
TypeScript
/**
|
||
* 首页 HomeScreen
|
||
* 胡萝卜BBS - 首页展示
|
||
* 支持列表和多列网格模式(响应式布局)
|
||
*/
|
||
|
||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||
import {
|
||
View,
|
||
FlatList,
|
||
ScrollView,
|
||
StyleSheet,
|
||
RefreshControl,
|
||
StatusBar,
|
||
TouchableOpacity,
|
||
NativeScrollEvent,
|
||
NativeSyntheticEvent,
|
||
Alert,
|
||
Clipboard,
|
||
} 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);
|
||
}
|
||
|
||
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 = async (post: Post) => {
|
||
if (!post?.id) return;
|
||
try {
|
||
await postService.sharePost(post.id);
|
||
} catch (error) {
|
||
console.error('上报分享次数失败:', error);
|
||
}
|
||
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) {
|
||
// 从列表中移除已删除的帖子
|
||
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,
|
||
},
|
||
});
|