Files
frontend/src/screens/home/HomeScreen.tsx

1123 lines
35 KiB
TypeScript
Raw Normal View History

/**
* HomeScreen
* -
*
*/
import React, { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react';
import {
View,
ScrollView,
Platform,
StyleSheet,
RefreshControl,
StatusBar,
TouchableOpacity,
NativeSyntheticEvent,
NativeScrollEvent,
Alert,
Clipboard,
2026-03-18 00:25:46 +08:00
Modal,
} from 'react-native';
import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter, useFocusEffect } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
import { channelService, postService } from '../../services';
import { useChannels } from '../../hooks/useChannels';
import { PostCard, SearchBar, ShareSheet } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
refactor: restructure core architecture and responsive system This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase. Key changes include: - **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience. - **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`). - **Data Layer Improvements**: - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`. - Cleaned up repository implementations by removing redundant local utility functions. - Refactored `MessageMapper` to use the new centralized JSON utility. - **Type System Cleanup**: - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory. - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities. - **Utility Consolidation**: - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components. - Removed deprecated responsive utility files in favor of the new hook-based system. - **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
2026-05-05 19:07:33 +08:00
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
2026-04-13 01:30:37 +08:00
import { postSyncService } from '@/services/post';
2026-03-16 17:47:10 +08:00
import { SearchScreen } from './SearchScreen';
2026-03-18 00:25:46 +08:00
import { CreatePostScreen } from '../create/CreatePostScreen';
import { MarketView } from './MarketView';
import { CreateTradeScreen } from '../trade/CreateTradeScreen';
import type { TradeCategory } from '../../types/trade';
import { ALL_TRADE_CATEGORIES } from '../../types/trade';
import * as hrefs from '../../navigation/hrefs';
import { blurActiveElement } from '../../infrastructure/platform';
const SORT_OPTIONS = ['关注', '推荐', '最新'];
const SORT_ICONS = ['account-heart', 'fire', 'clock-outline'];
const SORT_TYPES: PostType[] = ['follow', 'hot', 'latest'];
const MARKET_FILTER_OPTIONS = [
{ key: 'all' as const, label: '全部', icon: 'view-grid-outline' },
{ key: 'sell' as const, label: '出售', icon: 'tag-outline' },
{ key: 'buy' as const, label: '收购', icon: 'hand-coin-outline' },
];
const DEFAULT_PAGE_SIZE = 20;
const MOBILE_TAB_BAR_HEIGHT = 64;
const MOBILE_TAB_FLOATING_MARGIN = 12;
const MOBILE_FAB_GAP = 12;
/** 首页纵向滑动超过此阈值视为明确向下/向上划,用于隐藏或显示底部 Tab */
const TAB_BAR_SCROLL_DELTA_Y = 10;
/** 停止滑动多久后自动恢复底部 Tab */
const TAB_BAR_IDLE_RESTORE_MS = 2200;
type ViewMode = 'list' | 'grid';
type PostType = 'follow' | 'hot' | 'latest';
type LatestCapsule = { id: string; name: string };
function createHomeStyles(colors: AppColors, responsivePadding: number) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
header: {
backgroundColor: colors.background.paper,
},
searchWrapper: {
paddingTop: spacing.sm,
paddingBottom: spacing.xs,
},
homeTabSwitcher: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: spacing.xs,
marginBottom: spacing.xs,
paddingHorizontal: responsivePadding + 4,
},
homeTabSwitcherLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
homeTabSwitcherRight: {
flex: 1,
marginLeft: 16,
},
homeTabItem: {
paddingVertical: spacing.sm,
alignItems: 'center',
justifyContent: 'center',
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
homeTabItemActive: {
borderBottomColor: colors.text.primary,
},
homeTabText: {
fontSize: 20,
fontWeight: '400',
color: colors.text.hint,
},
homeTabTextActive: {
fontSize: 20,
fontWeight: '700',
color: colors.text.primary,
},
homeTabBar: {
marginTop: spacing.sm,
marginBottom: spacing.md,
},
viewToggleBtn: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
sortBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
},
sortBarCapsuleScroll: {
flex: 1,
},
sortBarCapsuleContent: {
gap: spacing.sm,
alignItems: 'center',
paddingVertical: spacing.xs,
},
capsuleItem: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: 999,
},
capsuleText: {
fontSize: 13,
fontWeight: '600',
color: colors.text.secondary,
},
capsuleTextActive: {
color: colors.primary.main,
},
sortTrigger: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.xs,
},
sortTriggerText: {
fontSize: 13,
fontWeight: '600',
color: colors.text.secondary,
marginLeft: 2,
},
listContent: {
flexGrow: 1,
},
contentContainer: {
flex: 1,
},
listItem: {},
gridContent: {
flexGrow: 1,
},
gridItem: {
marginBottom: 0,
},
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,
},
loadingMoreFooter: {
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
},
});
}
export const HomeScreen: React.FC = () => {
const router = useRouter();
const insets = useSafeAreaInsets();
const storePosts = useUserStore((s) => s.posts);
const currentUser = useCurrentUser();
const isAuthenticated = useIsAuthenticated();
const isVerified = useIsVerified();
const fetchVerificationStatus = useVerificationStore((s) => s.fetchStatus);
const hasFetchedVerification = useVerificationStore((s) => s.hasFetchedStatus);
const colors = useAppColors();
const resolvedScheme = useResolvedColorScheme();
// 使用响应式 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 styles = useMemo(() => createHomeStyles(colors, responsivePadding), [colors, responsivePadding]);
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
const [sortIndex, setSortIndex] = useState(2);
const [viewMode, setViewMode] = useState<ViewMode>('list');
const { data: channelList = [] } = useChannels();
const latestCapsules = useMemo<LatestCapsule[]>(
() => [{ id: '', name: '全部' }, ...channelList.map(item => ({ id: item.id, name: item.name }))],
[channelList]
);
const [activeCapsuleId, setActiveCapsuleId] = useState('');
// 图片查看器状态
const [showImageViewer, setShowImageViewer] = useState(false);
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const stableCloseImageViewer = useCallback(() => setShowImageViewer(false), []);
const stableGalleryImages = useMemo(() => postImages.map((img, i) => ({
id: img.id || img.url || `img-${i}`,
url: img.url || img.uri || ''
})), [postImages]);
2026-03-16 17:47:10 +08:00
// 搜索显示状态(用于内嵌搜索页面)
const [showSearch, setShowSearch] = useState(false);
2026-03-18 00:25:46 +08:00
// 发帖弹窗状态
const [showCreatePost, setShowCreatePost] = useState(false);
// 广场/市集 Tab
const [homeTab, setHomeTab] = useState<'square' | 'market'>('square');
const [showCreateTrade, setShowCreateTrade] = useState(false);
// 市集筛选状态
const [marketFilterType, setMarketFilterType] = useState<'all' | 'sell' | 'buy'>('all');
const [marketCategory, setMarketCategory] = useState<TradeCategory | 'all'>('all');
// 分享面板状态
const [showShareSheet, setShowShareSheet] = useState(false);
const [sharePost, setSharePost] = useState<Post | null>(null);
useEffect(() => {
if (isAuthenticated && !hasFetchedVerification) {
fetchVerificationStatus();
}
}, [isAuthenticated, hasFetchedVerification, fetchVerificationStatus]);
useEffect(() => {
if (showCreatePost) {
blurActiveElement();
}
}, [showCreatePost]);
2026-03-18 00:25:46 +08:00
// 用于跟踪当前页面显示的帖子 ID以便从 store 同步状态
const postIdsRef = useRef<Set<string>>(new Set());
const isLoadingMoreRef = useRef(false);
const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll);
const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
const homeTabPressCount = useHomeTabPressStore((s) => s.pressCount);
const homeListScrollYRef = useRef(0);
const tabBarIdleRestoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// FlashList 和 ScrollView 的 ref用于滚动到顶部
const flashListRef = useRef<FlashListRef<Post> | null>(null);
const scrollViewRef = useRef<ScrollView | null>(null);
const clearTabBarIdleRestoreTimer = useCallback(() => {
if (tabBarIdleRestoreTimerRef.current != null) {
clearTimeout(tabBarIdleRestoreTimerRef.current);
tabBarIdleRestoreTimerRef.current = null;
}
}, []);
const scheduleTabBarIdleRestore = useCallback(() => {
clearTabBarIdleRestoreTimer();
tabBarIdleRestoreTimerRef.current = setTimeout(() => {
setBottomTabBarHiddenByScroll(false);
tabBarIdleRestoreTimerRef.current = null;
}, TAB_BAR_IDLE_RESTORE_MS);
}, [clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll]);
const handleHomeVerticalScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const y = event.nativeEvent.contentOffset.y;
const dy = y - homeListScrollYRef.current;
homeListScrollYRef.current = y;
scheduleTabBarIdleRestore();
if (y <= 0) {
setBottomTabBarHiddenByScroll(false);
return;
}
if (dy > TAB_BAR_SCROLL_DELTA_Y) {
setBottomTabBarHiddenByScroll(true);
} else if (dy < -TAB_BAR_SCROLL_DELTA_Y) {
setBottomTabBarHiddenByScroll(false);
}
},
[scheduleTabBarIdleRestore, setBottomTabBarHiddenByScroll]
);
useFocusEffect(
useCallback(() => {
return () => {
clearTabBarIdleRestoreTimer();
homeListScrollYRef.current = 0;
setBottomTabBarHiddenByScroll(false);
};
}, [clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll])
);
useEffect(() => {
if (showSearch) {
clearTabBarIdleRestoreTimer();
setBottomTabBarHiddenByScroll(false);
}
}, [showSearch, clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll]);
// 监听首页 Tab 点击事件,滚动到顶部
useEffect(() => {
if (homeTabPressCount > 0) {
// 滚动 FlashList 到顶部
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
// 滚动 ScrollView 到顶部(网格模式)
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
// 重置底部 Tab 栏状态
setBottomTabBarHiddenByScroll(false);
homeListScrollYRef.current = 0;
}
}, [homeTabPressCount, setBottomTabBarHiddenByScroll]);
/** 横向胶囊条滚动位置:切换频道刷新列表时不重置 */
const capsuleHScrollRef = useRef<ScrollView | null>(null);
const capsuleScrollXRef = useRef(0);
const restoreCapsuleStripScroll = useCallback(() => {
const x = capsuleScrollXRef.current;
if (x <= 0) return;
const scroll = () => capsuleHScrollRef.current?.scrollTo({ x, animated: false });
scroll();
requestAnimationFrame(() => {
requestAnimationFrame(scroll);
});
}, []);
const onCapsuleHorizontalScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
capsuleScrollXRef.current = e.nativeEvent.contentOffset.x;
}, []);
// 构建一个以 postId 为 key 的 map用于快速查找
const postsMap = useMemo(() => {
const map = new Map<string, Post>();
storePosts.forEach(post => {
map.set(post.id, post);
});
return map;
}, [storePosts]);
// 获取当前排序对应的帖子类型
const getPostType = useCallback((): PostType => {
return SORT_TYPES[sortIndex] ?? 'hot';
}, [sortIndex]);
const currentChannelId = activeCapsuleId || undefined;
useEffect(() => {
homeListScrollYRef.current = 0;
setBottomTabBarHiddenByScroll(false);
}, [sortIndex, currentChannelId, setBottomTabBarHiddenByScroll]);
useEffect(() => {
requestAnimationFrame(() => {
flashListRef.current?.scrollToOffset({ offset: 0, animated: false });
scrollViewRef.current?.scrollTo({ y: 0, animated: false });
});
}, [viewMode]);
useLayoutEffect(() => {
restoreCapsuleStripScroll();
}, [activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]);
// 使用差异更新 Hook 获取帖子列表
const listKey = useMemo(
() => `home_${getPostType()}_${currentChannelId || 'all'}`,
[getPostType, currentChannelId]
);
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 postSyncService.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]);
// 网格模式滚动处理 - 底部 Tab 显隐(加载更多由 FlashList onEndReached 处理)
const handleGridScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
handleHomeVerticalScroll(event);
},
[handleHomeVerticalScroll]
);
// 刷新方法 - 先获取正确类型的帖子,再刷新
const refresh = useCallback(async () => {
try {
// 先获取正确类型的帖子
await postSyncService.fetchPosts(
{
page: 1,
pageSize: DEFAULT_PAGE_SIZE,
post_type: getPostType(),
channelId: currentChannelId,
},
listKey
);
} catch (err) {
console.error('刷新失败:', err);
}
}, [listKey, getPostType, currentChannelId]);
useEffect(() => {
let cancelled = false;
const run = async () => {
reset();
await new Promise(resolve => requestAnimationFrame(resolve));
if (!cancelled) {
await refresh();
}
};
run();
return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortIndex, currentChannelId]);
// 将获取到的原始帖子与 store 中的状态合并
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
const displayPosts = useMemo(() => {
return posts.map(post => {
const storePost = postsMap.get(post.id);
if (storePost) {
// store 同步点赞等状态channel 等列表专用字段在 store 里常缺失,从列表帖合并
return {
...post,
...storePost,
channel: storePost.channel ?? post.channel,
};
}
return post;
});
}, [posts, postsMap]);
/** 按 id 取当前列表中的最新 post配合 PostCard memo 忽略 onAction 引用 */
const postByIdRef = useRef<Map<string, Post>>(new Map());
postByIdRef.current = new Map(displayPosts.map((p) => [p.id, p]));
// 根据屏幕尺寸确定网格列数
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 1200;
if (isDesktop) return 1000;
if (isTablet) return 800;
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;
}
if (bottomTabBarHiddenByScroll) {
return MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP + insets.bottom;
}
// TabBar 悬浮在底部,发帖按钮需要在 TabBar 上方
return MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN * 2 + MOBILE_FAB_GAP + insets.bottom;
}, [isMobile, insets.bottom, bottomTabBarHiddenByScroll]);
// 切换视图模式
const toggleViewMode = () => {
setViewMode(prev => prev === 'list' ? 'grid' : 'list');
};
// 切换排序
const changeSort = useCallback((nextIndex: number) => {
if (nextIndex < 0 || nextIndex >= SORT_OPTIONS.length || nextIndex === sortIndex) {
return;
}
setSortIndex(nextIndex);
}, [sortIndex]);
const cycleMarketFilter = useCallback(() => {
setMarketFilterType(prev => {
const idx = MARKET_FILTER_OPTIONS.findIndex(f => f.key === prev);
return MARKET_FILTER_OPTIONS[(idx + 1) % MARKET_FILTER_OPTIONS.length].key;
});
}, []);
2026-03-16 17:47:10 +08:00
// 跳转到搜索页(使用内嵌模式,不再依赖导航)
const handleSearchPress = () => {
2026-03-16 17:47:10 +08:00
setShowSearch(true);
};
// 跳转到帖子详情
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
router.push(hrefs.hrefPostDetail(postId, scrollToComments));
};
// 跳转到用户主页
const handleUserPress = (userId: string) => {
router.push(hrefs.hrefUserProfile(userId));
};
// 点赞帖子
const handleLike = async (post: Post) => {
try {
if (post.is_liked) {
await postSyncService.unlikePost(post.id);
} else {
await postSyncService.likePost(post.id);
}
} catch (error) {
console.error('点赞操作失败:', error);
}
};
// 收藏帖子
const handleBookmark = async (post: Post) => {
try {
if (post.is_favorited) {
await postSyncService.unfavoritePost(post.id);
} else {
await postSyncService.favoritePost(post.id);
}
} catch (error) {
console.error('收藏操作失败:', error);
}
};
// 分享帖子 - 打开分享面板
const handleShare = (post: Post) => {
setSharePost(post);
setShowShareSheet(true);
};
// 删除帖子 - 删除后刷新列表
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);
};
// 统一处理 PostCard 的所有操作
const handlePostAction = (post: Post, action: PostCardAction) => {
switch (action.type) {
case 'press':
handlePostPress(post.id);
break;
case 'userPress':
const authorId = post.author?.id;
if (authorId) handleUserPress(authorId);
break;
case 'like':
case 'unlike':
handleLike(post);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
case 'unbookmark':
handleBookmark(post);
break;
case 'share':
handleShare(post);
break;
case 'imagePress':
if (action.payload?.images && action.payload?.imageIndex !== undefined) {
handleImagePress(action.payload.images, action.payload.imageIndex);
}
break;
case 'delete':
handleDeletePost(post.id);
break;
}
};
const handlePostActionRef = useRef(handlePostAction);
handlePostActionRef.current = handlePostAction;
const stableOnPostAction = useCallback((postId: string, action: PostCardAction) => {
const post = postByIdRef.current.get(postId);
if (post) {
handlePostActionRef.current(post, action);
}
}, []);
2026-03-18 00:25:46 +08:00
// 跳转到发帖页面(使用 Modal 方式)
const handleCreatePost = () => {
if (!isAuthenticated) {
router.push('/login');
return;
}
if (!isVerified) {
router.push(hrefs.hrefVerificationGuide());
return;
}
2026-03-18 00:25:46 +08:00
setShowCreatePost(true);
};
const renderChannelCapsules = () => {
return (
<ScrollView
ref={capsuleHScrollRef}
horizontal
showsHorizontalScrollIndicator={false}
nestedScrollEnabled
style={styles.sortBarCapsuleScroll}
contentContainerStyle={styles.sortBarCapsuleContent}
onScroll={onCapsuleHorizontalScroll}
scrollEventThrottle={16}
onContentSizeChange={restoreCapsuleStripScroll}
>
{latestCapsules.map((item) => {
const isActive = item.id === activeCapsuleId;
return (
<TouchableOpacity
key={item.id || 'all'}
activeOpacity={0.85}
onPress={() => setActiveCapsuleId(item.id)}
style={styles.capsuleItem}
>
<Text
style={
isActive
? { ...styles.capsuleText, ...styles.capsuleTextActive }
: styles.capsuleText
}
>
{item.name}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
);
};
// 渲染帖子卡片(列表模式)
const keyExtractor = useCallback((item: Post) => item.id, []);
const renderPostList = useCallback<ListRenderItem<Post>>(({ item }) => {
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}
onAction={(action) => stableOnPostAction(item.id, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
}, [currentUser?.id, stableOnPostAction, isMobile, listItemWidth, responsiveGap]);
// 渲染网格项(用于 FlashList 多列模式)
const renderGridItem = useCallback<ListRenderItem<Post>>(({ item }) => {
const authorId = item.author?.id || '';
const isPostAuthor = currentUser?.id === authorId;
return (
<View style={{ marginBottom: 2, paddingHorizontal: 1 }}>
<PostCard
post={item}
variant="grid"
onAction={(action) => stableOnPostAction(item.id, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
}, [currentUser?.id, stableOnPostAction]);
// 渲染响应式网格布局(所有端统一使用 FlashList masonry 模式)
const renderResponsiveGrid = () => {
return (
<FlashList
key="home-grid"
ref={scrollViewRef as any}
data={displayPosts}
extraData={listKey}
renderItem={renderGridItem}
keyExtractor={keyExtractor}
numColumns={gridColumns}
masonry
optimizeItemArrangement
contentContainerStyle={{
paddingHorizontal: responsivePadding,
paddingBottom: 80 + responsivePadding,
}}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={refresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
onScroll={handleGridScroll}
scrollEventThrottle={16}
ListEmptyComponent={renderEmpty}
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
drawDistance={250}
/>
);
};
// 渲染空状态
const renderEmpty = useCallback(() => {
if (isInitialLoading) return null;
return (
<EmptyState
title="暂无内容"
description="暂无帖子,快去发布第一条内容吧"
/>
);
}, [isInitialLoading]);
// 渲染列表内容
const renderListContent = () => {
if (useMultiColumnList) {
// 平板/桌面端使用多列网格
return renderResponsiveGrid();
}
// 移动端和宽屏都使用单列 FlashList宽屏下居中显示
return (
<FlashList
key="home-list"
ref={flashListRef}
data={displayPosts}
extraData={listKey}
renderItem={renderPostList}
keyExtractor={keyExtractor}
contentContainerStyle={{
paddingHorizontal: listHorizontalPadding,
paddingBottom: 80 + responsivePadding,
}}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={refresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
onScroll={handleHomeVerticalScroll}
scrollEventThrottle={16}
ListEmptyComponent={renderEmpty}
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
drawDistance={250}
/>
);
};
2026-03-16 17:47:10 +08:00
// 搜索页面内嵌模式
if (showSearch) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
2026-03-16 17:47:10 +08:00
<SearchScreen
onBack={() => setShowSearch(false)}
/>
</SafeAreaView>
);
}
// 正常首页内容
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
{/* 顶部Header */}
<View style={styles.header}>
{/* 广场/市集切换 */}
<View style={styles.homeTabSwitcher}>
<View style={styles.homeTabSwitcherLeft}>
<TouchableOpacity
activeOpacity={0.7}
style={[styles.homeTabItem, homeTab === 'square' && styles.homeTabItemActive]}
onPress={() => setHomeTab('square')}
>
<Text style={homeTab === 'square' ? styles.homeTabTextActive : styles.homeTabText}>
广
</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
style={[styles.homeTabItem, homeTab === 'market' && styles.homeTabItemActive]}
onPress={() => setHomeTab('market')}
>
<Text style={homeTab === 'market' ? styles.homeTabTextActive : styles.homeTabText}>
</Text>
</TouchableOpacity>
</View>
<View style={styles.homeTabSwitcherRight}>
<SearchBar
value=""
onChangeText={() => {}}
onSubmit={handleSearchPress}
onFocus={handleSearchPress}
placeholder="搜索"
compact
/>
</View>
</View>
{/* 排序筛选 + 频道(仅广场模式显示) */}
{homeTab === 'square' && (
<View style={styles.sortBar}>
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
<MaterialCommunityIcons
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
size={22}
color="#666"
/>
</TouchableOpacity>
{renderChannelCapsules()}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => changeSort((sortIndex + 1) % SORT_OPTIONS.length)}
style={styles.sortTrigger}
>
<MaterialCommunityIcons
name={SORT_ICONS[sortIndex] as any}
size={18}
color={colors.text.secondary}
/>
<Text style={styles.sortTriggerText}>
{SORT_OPTIONS[sortIndex]}
</Text>
</TouchableOpacity>
</View>
)}
{/* 市集分类 + 筛选(仅市集模式显示) */}
{homeTab === 'market' && (
<View style={styles.sortBar}>
<FlashList
horizontal
data={[{ key: 'all', label: '全部' }, ...ALL_TRADE_CATEGORIES]}
renderItem={({ item }: { item: { key: string; label: string } }) => (
<TouchableOpacity
activeOpacity={0.7}
style={styles.capsuleItem}
onPress={() => setMarketCategory(item.key as TradeCategory | 'all')}
>
<Text style={[
styles.capsuleText,
marketCategory === item.key ? styles.capsuleTextActive : {},
]}>
{item.label}
</Text>
</TouchableOpacity>
)}
keyExtractor={(item: { key: string; label: string }) => item.key}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.sortBarCapsuleContent}
style={styles.sortBarCapsuleScroll}
/>
<TouchableOpacity
activeOpacity={0.7}
onPress={cycleMarketFilter}
style={styles.sortTrigger}
>
<MaterialCommunityIcons
name={(MARKET_FILTER_OPTIONS.find(f => f.key === marketFilterType)?.icon || 'view-grid-outline') as any}
size={18}
color={colors.text.secondary}
/>
<Text style={styles.sortTriggerText}>
{MARKET_FILTER_OPTIONS.find(f => f.key === marketFilterType)?.label || '全部'}
</Text>
</TouchableOpacity>
</View>
)}
</View>
{/* 内容区域 */}
{homeTab === 'square' ? (
<View style={styles.contentContainer}>
{isInitialLoading ? (
<Loading fullScreen />
) : viewMode === 'list' ? (
renderListContent()
) : (
renderResponsiveGrid()
)}
</View>
) : (
<MarketView
onCreateTrade={() => setShowCreateTrade(true)}
filterType={marketFilterType}
onFilterTypeChange={setMarketFilterType}
selectedCategory={marketCategory}
onSelectedCategoryChange={setMarketCategory}
/>
)}
{/* 漂浮发帖/发布按钮 */}
<TouchableOpacity
style={[
styles.floatingButton,
isDesktop && styles.floatingButtonDesktop,
isWideScreen && styles.floatingButtonWide,
floatingButtonBottom !== undefined && { bottom: floatingButtonBottom },
]}
onPress={homeTab === 'market' ? () => {
if (!isAuthenticated) { router.push('/login'); return; }
if (!isVerified) { router.push(hrefs.hrefVerificationGuide()); return; }
setShowCreateTrade(true);
} : handleCreatePost}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="plus" size={28} color={colors.text.inverse} />
</TouchableOpacity>
{/* 图片查看器 */}
<ImageGallery
visible={showImageViewer}
images={stableGalleryImages}
initialIndex={selectedImageIndex}
onClose={stableCloseImageViewer}
enableSave
/>
2026-03-18 00:25:46 +08:00
{/* 发帖弹窗 */}
<Modal
visible={showCreatePost}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={() => setShowCreatePost(false)}
>
<CreatePostScreen
onClose={() => setShowCreatePost(false)}
/>
</Modal>
{/* 发布交易弹窗 */}
<Modal
visible={showCreateTrade}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={() => setShowCreateTrade(false)}
>
<CreateTradeScreen
onClose={() => setShowCreateTrade(false)}
/>
</Modal>
{/* 分享面板 */}
<ShareSheet
visible={showShareSheet}
postUrl={sharePost ? `https://browser.littlelan.cn/post/${encodeURIComponent(sharePost.id)}` : undefined}
postTitle={sharePost?.title}
onClose={() => {
setShowShareSheet(false);
setSharePost(null);
}}
onShareAction={(actionKey) => {
if (sharePost) {
(async () => {
try {
const res = await postService.sharePost(sharePost.id, { channel: actionKey });
if (res.ok && res.shares_count != null) {
postSyncService.applyShareCountUpdate(sharePost.id, res.shares_count);
}
} catch (e) {
console.error('上报分享次数失败:', e);
}
})();
}
}}
/>
</SafeAreaView>
);
};