diff --git a/app/searchResults.tsx b/app/searchResults.tsx index bf42bfb..262294f 100644 --- a/app/searchResults.tsx +++ b/app/searchResults.tsx @@ -3,7 +3,7 @@ import { View, StyleSheet, StatusBar as RNStatusBar, - RefreshControl, + ScrollView, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { SafeAreaView } from 'react-native-safe-area-context' @@ -11,6 +11,10 @@ import { useRouter, useLocalSearchParams } from 'expo-router' import SearchResultsGrid from '@/components/SearchResultsGrid' import SearchBar from '@/components/SearchBar' +import RefreshControl from '@/components/RefreshControl' +import LoadingState from '@/components/LoadingState' +import ErrorState from '@/components/ErrorState' +import PaginationLoader from '@/components/PaginationLoader' import { useTemplates, useSearchHistory } from '@/hooks' export default function SearchResultsScreen() { @@ -19,27 +23,20 @@ export default function SearchResultsScreen() { const [searchText, setSearchText] = useState((params.q as string) || '') const [refreshing, setRefreshing] = useState(false) - // 使用 useTemplates hook 进行搜索 - const { templates, loading, error, execute, refetch } = useTemplates() - - // 使用搜索历史 hook + const { templates, loading, loadingMore, error, execute, refetch, loadMore, hasMore } = useTemplates() const { addToHistory } = useSearchHistory() - // 当搜索关键词变化时,执行搜索 useEffect(() => { if (params.q && typeof params.q === 'string') { const query = params.q.trim() setSearchText(query) if (query) { - // 执行搜索 - execute({ search: query, limit: 50, sortBy: 'createdAt', sortOrder: 'desc', page: 1 }) - // 添加到搜索历史 + execute({ search: query, limit: 20, sortBy: 'createdAt', sortOrder: 'desc', page: 1 }) addToHistory(query) } } }, [params.q, execute, addToHistory]) - // 处理下拉刷新 const handleRefresh = async () => { setRefreshing(true) if (searchText) { @@ -48,7 +45,12 @@ export default function SearchResultsScreen() { setRefreshing(false) } - // 将模板数据转换为搜索结果格式 + const handleLoadMore = () => { + if (hasMore && !loadingMore) { + loadMore() + } + } + const searchResults = templates.map(template => ({ id: template.id, title: template.title || template.titleEn || '', @@ -58,6 +60,76 @@ export default function SearchResultsScreen() { aspectRatio: template.aspectRatio ? parseFloat(template.aspectRatio as string) : undefined, })) + if (loading && !refreshing) { + return ( + + + + { + router.push({ + pathname: '/searchTemplate', + params: { q: text, focus: 'true' }, + }) + }} + onBack={() => router.back()} + readOnly={true} + onInputPress={() => { + router.push({ + pathname: '/searchTemplate', + params: { q: searchText, focus: 'true' }, + }) + }} + onClearPress={() => { + router.push({ + pathname: '/searchTemplate', + params: { q: '', focus: 'true' }, + }) + }} + marginBottom={12} + /> + + + ) + } + + if (error) { + return ( + + + + { + router.push({ + pathname: '/searchTemplate', + params: { q: text, focus: 'true' }, + }) + }} + onBack={() => router.back()} + readOnly={true} + onInputPress={() => { + router.push({ + pathname: '/searchTemplate', + params: { q: searchText, focus: 'true' }, + }) + }} + onClearPress={() => { + router.push({ + pathname: '/searchTemplate', + params: { q: '', focus: 'true' }, + }) + }} + marginBottom={12} + /> + + + ) + } + return ( @@ -91,7 +163,10 @@ export default function SearchResultsScreen() { } + onEndReached={handleLoadMore} + ListFooterComponent={loadingMore ? : null} /> ) diff --git a/app/searchWorksResults.tsx b/app/searchWorksResults.tsx index 05f4bba..db5e350 100644 --- a/app/searchWorksResults.tsx +++ b/app/searchWorksResults.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next' import SearchBar from '@/components/SearchBar' import WorksGallery, { type Category, type WorkItem } from '@/components/WorksGallery' -// 模拟搜索结果数据 +// TODO: Replace with actual API call when backend supports works search const mockSearchResults: WorkItem[] = [ { id: 1, date: new Date(2025, 10, 28), duration: '00:05', category: '写真' }, { id: 2, date: new Date(2025, 10, 28), duration: '00:05', category: '写真' }, diff --git a/components/SearchResultsGrid.tsx b/components/SearchResultsGrid.tsx index 078e57a..0ada988 100644 --- a/components/SearchResultsGrid.tsx +++ b/components/SearchResultsGrid.tsx @@ -30,6 +30,9 @@ interface TemplateSearchResultItem { interface SearchResultsGridProps { results: TemplateSearchResultItem[] loading?: boolean + refreshControl?: React.ReactElement + onEndReached?: () => void + ListFooterComponent?: React.ReactElement | null } // 计算卡片高度的辅助函数 @@ -41,7 +44,7 @@ const calculateCardHeight = (width: number, aspectRatio?: number): number => { return width * 1.2 } -export default function SearchResultsGrid({ results, loading }: SearchResultsGridProps) { +export default function SearchResultsGrid({ results, loading, refreshControl, onEndReached, ListFooterComponent }: SearchResultsGridProps) { const { t } = useTranslation() const router = useRouter() const [gridWidth, setGridWidth] = useState(screenWidth) @@ -57,6 +60,14 @@ export default function SearchResultsGrid({ results, loading }: SearchResultsGri }) } + const handleScroll = (event: any) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent + const paddingToBottom = 20 + if (layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom) { + onEndReached?.() + } + } + if (loading) { return ( @@ -77,6 +88,9 @@ export default function SearchResultsGrid({ results, loading }: SearchResultsGri + {ListFooterComponent} ) } diff --git a/hooks/index.ts b/hooks/index.ts index cae00b8..edf15d6 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -6,3 +6,4 @@ export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './ export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations' export { useSearchHistory } from './use-search-history' export { useTags } from './use-tags' +export { useDebounce } from './use-debounce' diff --git a/hooks/use-debounce.ts b/hooks/use-debounce.ts new file mode 100644 index 0000000..7fdb859 --- /dev/null +++ b/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +export function useDebounce(value: T, delay: number = 500): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(handler) + } + }, [value, delay]) + + return debouncedValue +}