Files
frontend/src/screens/home/HomeScreen.tsx
lafay 5c9cb6acea
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m57s
Frontend CI / ota-android (push) Successful in 10m48s
Frontend CI / build-android-apk (push) Successful in 52m58s
refactor(ProcessPostUseCase, useCursorPagination, useDifferentialPosts): enhance post merging and loading states
- 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.
2026-03-24 01:19:09 +08:00

822 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 首页 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',
},
});