/** * 搜索页 SearchScreen(响应式版本) * 胡萝卜BBS - 搜索帖子、用户 * 支持响应式布局,宽屏下显示更大的搜索结果区域 */ import React, { useState, useCallback, useEffect } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, ScrollView, RefreshControl, } 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 { colors, spacing, fontSizes, borderRadius } from '../../theme'; import { Post, User } from '../../types'; import { useUserStore } from '../../stores'; import { postService, authService } from '../../services'; import { PostCard, TabBar, SearchBar } from '../../components/business'; import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common'; import { HomeStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive'; import { useCursorPagination } from '../../hooks/useCursorPagination'; type NavigationProp = NativeStackNavigationProp; const TABS = ['帖子', '用户']; const DEFAULT_PAGE_SIZE = 20; type SearchType = 'posts' | 'users'; interface SearchScreenProps { onBack?: () => void; navigation?: NavigationProp; } export const SearchScreen: React.FC = ({ onBack, navigation: propNavigation }) => { // 如果传入了 navigation 则使用传入的,否则使用 hook 获取的 const navigation = propNavigation || useNavigation(); const insets = useSafeAreaInsets(); const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore(); // 使用响应式 hook const { width, isMobile, isTablet, isDesktop, isWideScreen } = useResponsive(); // 响应式间距 const responsivePadding = useResponsiveSpacing({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, '2xl': 32, '3xl': 40, '4xl': 48 }); const responsiveGap = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 28, '4xl': 32 }); // 响应式搜索栏最大宽度(使用数字) const searchBarMaxWidth = useResponsiveValue({ xs: 600, sm: 600, md: 600, lg: 700, xl: 800, '2xl': 900, '3xl': 1000, '4xl': 1200, }); const [searchText, setSearchText] = useState(''); const [activeIndex, setActiveIndex] = useState(0); const [hasSearched, setHasSearched] = useState(false); // 保存当前搜索关键词,用于Tab切换时重新搜索 const [currentKeyword, setCurrentKeyword] = useState(''); // 使用游标分页进行帖子搜索 const { items: searchResults, isLoading, isRefreshing, hasMore, loadMore, refresh, reset, } = useCursorPagination( async ({ cursor, pageSize, extraParams }) => { if (!extraParams?.query) { return { items: [], next_cursor: null, prev_cursor: null, has_more: false }; } const response = await postService.searchPostsCursor(extraParams.query, { cursor, page_size: pageSize, }); return response; }, { pageSize: DEFAULT_PAGE_SIZE }, { query: '' } ); // 用户搜索结果(保持原有分页方式) const [userResults, setUserResults] = useState([]); const [userLoading, setUserLoading] = useState(false); // 当搜索词变化时重置 useEffect(() => { if (currentKeyword) { refresh(); } else { reset(); } }, [currentKeyword]); // 执行搜索 - 根据当前Tab执行对应类型的搜索 const performSearch = useCallback(async (keyword: string) => { if (!keyword.trim()) return; const trimmedKeyword = keyword.trim(); // 保存当前搜索关键词 setCurrentKeyword(trimmedKeyword); // 添加到搜索历史 addSearchHistory(trimmedKeyword); try { const searchType = getSearchType(); if (searchType === 'posts') { // 帖子搜索由 useCursorPagination 处理,这里只需触发刷新 refresh(); } else if (searchType === 'users') { // 用户搜索保持原有方式 setUserLoading(true); const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20); setUserResults(usersResponse.list || []); } } catch (error) { console.error('搜索失败:', error); } setHasSearched(true); }, [addSearchHistory, activeIndex, refresh]); // 处理搜索提交 const handleSearch = () => { performSearch(searchText); }; // 处理搜索历史点击 const handleHistoryPress = (keyword: string) => { setSearchText(keyword); performSearch(keyword); }; // 清除搜索历史 const handleClearHistory = () => { clearSearchHistory(); }; // 跳转到帖子详情 const handlePostPress = (postId: string, scrollToComments: boolean = false) => { navigation.navigate('PostDetail', { postId, scrollToComments }); }; // 跳转到用户主页 const handleUserPress = (userId: string) => { navigation.navigate('UserProfile', { userId }); }; // 获取当前搜索类型 const getSearchType = (): SearchType => { switch (activeIndex) { case 0: return 'posts'; case 1: return 'users'; default: return 'posts'; } }; // 渲染帖子搜索结果(使用响应式网格) const renderPostResults = () => { const posts = searchResults; if (posts.length === 0 && !isLoading) { return ( ); } // 平板/桌面端使用网格布局 if (isTablet || isDesktop || isWideScreen) { return ( } > {posts.map(post => ( handlePostPress(post.id)} onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}} onLike={() => {}} onComment={() => handlePostPress(post.id, true)} onBookmark={() => {}} onShare={() => {}} compact={isMobile} /> ))} {isLoading && ( )} ); } // 移动端使用列表布局 return ( ( handlePostPress(item.id)} onUserPress={() => item.author ? handleUserPress(item.author.id) : () => {}} onLike={() => {}} onComment={() => handlePostPress(item.id, true)} onBookmark={() => {}} onShare={() => {}} compact /> )} keyExtractor={item => item.id} contentContainerStyle={{ paddingBottom: responsivePadding }} showsVerticalScrollIndicator={false} refreshControl={ } onEndReached={loadMore} onEndReachedThreshold={0.3} ListFooterComponent={isLoading ? : null} /> ); }; // 渲染用户搜索结果 const renderUserResults = () => { const users = userResults; if (users.length === 0 && !userLoading) { return ( ); } // 平板/桌面端使用网格布局 if (isTablet || isDesktop || isWideScreen) { return ( {users.map(item => ( handleUserPress(item.id)} > {item.nickname} {item.bio || '@' + item.username} {item.is_following && ( )} ))} {userLoading && ( )} ); } // 移动端使用列表布局 return ( ( handleUserPress(item.id)} > {item.nickname} {item.bio || '@' + item.username} {item.is_following && ( )} )} keyExtractor={item => item.id} contentContainerStyle={{ paddingVertical: responsiveGap }} showsVerticalScrollIndicator={false} ListFooterComponent={userLoading ? : null} /> ); }; // 渲染搜索结果 const renderSearchResults = () => { const searchType = getSearchType(); if (searchType === 'posts') { return renderPostResults(); } if (searchType === 'users') { return renderUserResults(); } return null; }; // 渲染搜索历史和热门搜索 const renderSearchSuggestions = () => { if (hasSearched) return null; return ( {/* 搜索历史 */} {history.length > 0 && ( 搜索历史 {history.map((keyword, index) => ( handleHistoryPress(keyword)} > {keyword} ))} )} ); }; return ( {/* 搜索输入框 */} onBack ? onBack() : navigation.goBack()} activeOpacity={0.85} > 取消 {/* Tab切换 */} { setActiveIndex(index); // 如果已经搜索过,切换Tab时重新搜索 if (currentKeyword && hasSearched) { performSearch(currentKeyword); } }} /> {/* 内容区域 */} {hasSearched ? renderSearchResults() : renderSearchSuggestions()} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, searchHeader: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: `${colors.divider}70`, }, searchShell: { flex: 1, borderRadius: borderRadius.xl, backgroundColor: `${colors.primary.main}08`, paddingHorizontal: spacing.xs, paddingVertical: spacing.xs, // 移除阴影效果 shadowColor: 'transparent', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0, shadowRadius: 0, elevation: 0, }, cancelButton: { backgroundColor: `${colors.primary.main}14`, borderRadius: borderRadius.full, paddingHorizontal: spacing.md, paddingVertical: spacing.xs, }, cancelText: { fontSize: fontSizes.md, fontWeight: '600', }, tabWrapper: { backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: `${colors.divider}50`, }, suggestionsContainer: { flex: 1, }, section: { marginTop: spacing.md, }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.sm, }, sectionTitle: { fontWeight: '600', color: colors.text.primary, }, tagContainer: { flexDirection: 'row', flexWrap: 'wrap', }, tag: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, borderWidth: 1, borderColor: colors.divider, }, tagText: { marginLeft: spacing.xs, }, userCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, flexDirection: 'row', alignItems: 'center', }, userCardInfo: { flex: 1, marginLeft: spacing.md, }, userCardName: { fontWeight: '600', color: colors.text.primary, }, userItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, }, userInfo: { flex: 1, marginLeft: spacing.md, }, userName: { fontWeight: '600', color: colors.text.primary, }, followingBadge: { width: 20, height: 20, borderRadius: 10, backgroundColor: `${colors.primary.main}14`, alignItems: 'center', justifyContent: 'center', }, loadingMore: { paddingVertical: spacing.md, alignItems: 'center', }, });