2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 搜索页 SearchScreen(响应式版本)
|
|
|
|
|
|
* 胡萝卜BBS - 搜索帖子、用户
|
|
|
|
|
|
* 支持响应式布局,宽屏下显示更大的搜索结果区域
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
ScrollView,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
RefreshControl,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} 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';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { HomeStackParamList } from '../../navigation/types';
|
|
|
|
|
|
import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Search'>;
|
|
|
|
|
|
|
|
|
|
|
|
const TABS = ['帖子', '用户'];
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const DEFAULT_PAGE_SIZE = 20;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
type SearchType = 'posts' | 'users';
|
|
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
interface SearchScreenProps {
|
|
|
|
|
|
onBack?: () => void;
|
|
|
|
|
|
navigation?: NavigationProp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation: propNavigation }) => {
|
|
|
|
|
|
// 如果传入了 navigation 则使用传入的,否则使用 hook 获取的
|
|
|
|
|
|
const navigation = propNavigation || useNavigation<NavigationProp>();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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('');
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 使用游标分页进行帖子搜索
|
|
|
|
|
|
const {
|
|
|
|
|
|
items: searchResults,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
isRefreshing,
|
|
|
|
|
|
hasMore,
|
|
|
|
|
|
loadMore,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
reset,
|
|
|
|
|
|
} = useCursorPagination<Post, { query: string }>(
|
|
|
|
|
|
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<User[]>([]);
|
|
|
|
|
|
const [userLoading, setUserLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 当搜索词变化时重置
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (currentKeyword) {
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reset();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentKeyword]);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 执行搜索 - 根据当前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') {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 帖子搜索由 useCursorPagination 处理,这里只需触发刷新
|
|
|
|
|
|
refresh();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} else if (searchType === 'users') {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 用户搜索保持原有方式
|
|
|
|
|
|
setUserLoading(true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
setUserResults(usersResponse.list || []);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('搜索失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setHasSearched(true);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
}, [addSearchHistory, activeIndex, refresh]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理搜索提交
|
|
|
|
|
|
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 = () => {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const posts = searchResults;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
if (posts.length === 0 && !isLoading) {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="未找到相关帖子"
|
|
|
|
|
|
description="试试其他关键词吧"
|
|
|
|
|
|
icon="file-search-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 平板/桌面端使用网格布局
|
|
|
|
|
|
if (isTablet || isDesktop || isWideScreen) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
contentContainerStyle={{ paddingBottom: responsivePadding }}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={isRefreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
>
|
|
|
|
|
|
<ResponsiveGrid
|
|
|
|
|
|
columns={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, '2xl': 4, '3xl': 4, '4xl': 5 }}
|
|
|
|
|
|
gap={{ xs: 12, sm: 14, md: 16, lg: 20, xl: 24 }}
|
|
|
|
|
|
containerStyle={{ paddingHorizontal: responsivePadding }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{posts.map(post => (
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
key={post.id}
|
|
|
|
|
|
post={post}
|
|
|
|
|
|
onPress={() => handlePostPress(post.id)}
|
|
|
|
|
|
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
|
|
|
|
|
onLike={() => {}}
|
|
|
|
|
|
onComment={() => handlePostPress(post.id, true)}
|
|
|
|
|
|
onBookmark={() => {}}
|
|
|
|
|
|
onShare={() => {}}
|
|
|
|
|
|
compact={isMobile}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ResponsiveGrid>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{isLoading && (
|
|
|
|
|
|
<View style={styles.loadingMore}>
|
|
|
|
|
|
<Loading size="sm" />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 移动端使用列表布局
|
|
|
|
|
|
return (
|
|
|
|
|
|
<FlatList
|
|
|
|
|
|
data={posts}
|
|
|
|
|
|
renderItem={({ item }) => (
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
post={item}
|
|
|
|
|
|
onPress={() => 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}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={isRefreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
onEndReached={loadMore}
|
|
|
|
|
|
onEndReachedThreshold={0.3}
|
|
|
|
|
|
ListFooterComponent={isLoading ? <Loading size="sm" /> : null}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染用户搜索结果
|
|
|
|
|
|
const renderUserResults = () => {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const users = userResults;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
if (users.length === 0 && !userLoading) {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="未找到相关用户"
|
|
|
|
|
|
description="试试其他关键词吧"
|
|
|
|
|
|
icon="account-search-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 平板/桌面端使用网格布局
|
|
|
|
|
|
if (isTablet || isDesktop || isWideScreen) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
contentContainerStyle={{ paddingBottom: responsivePadding }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ResponsiveGrid
|
|
|
|
|
|
columns={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, '2xl': 4, '3xl': 4, '4xl': 5 }}
|
|
|
|
|
|
gap={{ xs: 12, sm: 14, md: 16, lg: 20, xl: 24 }}
|
|
|
|
|
|
containerStyle={{ paddingHorizontal: responsivePadding }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{users.map(item => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.userCard,
|
|
|
|
|
|
{ padding: responsiveGap }
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={() => handleUserPress(item.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
source={item.avatar}
|
|
|
|
|
|
size={isDesktop ? 70 : isTablet ? 60 : 50}
|
|
|
|
|
|
name={item.nickname}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<View style={styles.userCardInfo}>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="body"
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.userCardName,
|
|
|
|
|
|
{ fontSize: isDesktop ? fontSizes.lg : fontSizes.md }
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.nickname}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="caption"
|
|
|
|
|
|
color={colors.text.secondary}
|
|
|
|
|
|
style={{ fontSize: isDesktop ? fontSizes.md : fontSizes.sm }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.bio || '@' + item.username}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{item.is_following && (
|
|
|
|
|
|
<View style={styles.followingBadge}>
|
|
|
|
|
|
<MaterialCommunityIcons name="check" size={14} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ResponsiveGrid>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{userLoading && (
|
|
|
|
|
|
<View style={styles.loadingMore}>
|
|
|
|
|
|
<Loading size="sm" />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 移动端使用列表布局
|
|
|
|
|
|
return (
|
|
|
|
|
|
<FlatList
|
|
|
|
|
|
data={users}
|
|
|
|
|
|
renderItem={({ item }) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.userItem,
|
|
|
|
|
|
{
|
|
|
|
|
|
marginHorizontal: responsivePadding,
|
|
|
|
|
|
marginVertical: responsiveGap / 2,
|
|
|
|
|
|
padding: responsiveGap
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={() => handleUserPress(item.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
source={item.avatar}
|
|
|
|
|
|
size={50}
|
|
|
|
|
|
name={item.nickname}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<View style={styles.userInfo}>
|
|
|
|
|
|
<Text variant="body" style={styles.userName}>{item.nickname}</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>{item.bio || '@' + item.username}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{item.is_following && (
|
|
|
|
|
|
<View style={styles.followingBadge}>
|
|
|
|
|
|
<MaterialCommunityIcons name="check" size={14} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
)}
|
|
|
|
|
|
keyExtractor={item => item.id}
|
|
|
|
|
|
contentContainerStyle={{ paddingVertical: responsiveGap }}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
ListFooterComponent={userLoading ? <Loading size="sm" /> : null}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染搜索结果
|
|
|
|
|
|
const renderSearchResults = () => {
|
|
|
|
|
|
const searchType = getSearchType();
|
|
|
|
|
|
|
|
|
|
|
|
if (searchType === 'posts') {
|
|
|
|
|
|
return renderPostResults();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (searchType === 'users') {
|
|
|
|
|
|
return renderUserResults();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染搜索历史和热门搜索
|
|
|
|
|
|
const renderSearchSuggestions = () => {
|
|
|
|
|
|
if (hasSearched) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={[styles.suggestionsContainer, { paddingHorizontal: responsivePadding }]}>
|
|
|
|
|
|
{/* 搜索历史 */}
|
|
|
|
|
|
{history.length > 0 && (
|
|
|
|
|
|
<View style={[styles.section, { marginTop: responsiveGap }]}>
|
|
|
|
|
|
<View style={styles.sectionHeader}>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="body"
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.sectionTitle,
|
|
|
|
|
|
{ fontSize: isDesktop ? fontSizes.lg : fontSizes.md }
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
搜索历史
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<TouchableOpacity onPress={handleClearHistory}>
|
|
|
|
|
|
<MaterialCommunityIcons name="delete-outline" size={isDesktop ? 22 : 18} color={colors.text.secondary} />
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.tagContainer}>
|
|
|
|
|
|
{history.map((keyword, index) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.tag,
|
|
|
|
|
|
{
|
|
|
|
|
|
paddingHorizontal: responsiveGap,
|
|
|
|
|
|
paddingVertical: responsiveGap / 2,
|
|
|
|
|
|
marginRight: responsiveGap / 2,
|
|
|
|
|
|
marginBottom: responsiveGap / 2
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={() => handleHistoryPress(keyword)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons name="history" size={isDesktop ? 16 : 14} color={colors.text.secondary} />
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="caption"
|
|
|
|
|
|
color={colors.text.secondary}
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.tagText,
|
|
|
|
|
|
{ fontSize: isDesktop ? fontSizes.md : fontSizes.sm }
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
{keyword}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-18 10:58:41 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
{/* 搜索输入框 */}
|
|
|
|
|
|
<View
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.searchHeader,
|
|
|
|
|
|
{
|
|
|
|
|
|
paddingTop: Math.max(spacing.sm, insets.top + spacing.xs),
|
|
|
|
|
|
paddingHorizontal: responsivePadding,
|
|
|
|
|
|
paddingBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<View style={[styles.searchShell, { maxWidth: searchBarMaxWidth }]}>
|
|
|
|
|
|
<SearchBar
|
|
|
|
|
|
value={searchText}
|
|
|
|
|
|
onChangeText={setSearchText}
|
|
|
|
|
|
onSubmit={handleSearch}
|
|
|
|
|
|
placeholder="搜索帖子、用户"
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-16 17:47:10 +08:00
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[styles.cancelButton, { marginLeft: responsiveGap }]}
|
|
|
|
|
|
onPress={() => onBack ? onBack() : navigation.goBack()}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
activeOpacity={0.85}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="body"
|
|
|
|
|
|
color={colors.primary.main}
|
|
|
|
|
|
style={styles.cancelText}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Tab切换 */}
|
|
|
|
|
|
<View style={styles.tabWrapper}>
|
|
|
|
|
|
<TabBar
|
|
|
|
|
|
tabs={TABS}
|
|
|
|
|
|
activeIndex={activeIndex}
|
|
|
|
|
|
onTabChange={(index) => {
|
|
|
|
|
|
setActiveIndex(index);
|
|
|
|
|
|
// 如果已经搜索过,切换Tab时重新搜索
|
|
|
|
|
|
if (currentKeyword && hasSearched) {
|
|
|
|
|
|
performSearch(currentKeyword);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 内容区域 */}
|
|
|
|
|
|
{hasSearched ? renderSearchResults() : renderSearchSuggestions()}
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-03-16 17:47:10 +08:00
|
|
|
|
// 移除阴影效果
|
|
|
|
|
|
shadowColor: 'transparent',
|
|
|
|
|
|
shadowOffset: { width: 0, height: 0 },
|
|
|
|
|
|
shadowOpacity: 0,
|
|
|
|
|
|
shadowRadius: 0,
|
|
|
|
|
|
elevation: 0,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
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,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: `${colors.divider}50`,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
suggestionsContainer: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
section: {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
marginTop: spacing.md,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
sectionHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'space-between',
|
2026-03-20 23:00:27 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginBottom: spacing.sm,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
sectionTitle: {
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
tagContainer: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
},
|
|
|
|
|
|
tag: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: colors.divider,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
tagText: {
|
|
|
|
|
|
marginLeft: spacing.xs,
|
|
|
|
|
|
},
|
2026-03-20 23:00:27 +08:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
2026-03-09 21:29:03 +08:00
|
|
|
|
userItem: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
borderRadius: borderRadius.lg,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
userInfo: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
marginLeft: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
userName: {
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
followingBadge: {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
width: 20,
|
|
|
|
|
|
height: 20,
|
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
|
backgroundColor: `${colors.primary.main}14`,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
2026-03-20 23:00:27 +08:00
|
|
|
|
loadingMore: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|