325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react'
|
|
import {
|
|
StyleSheet,
|
|
StatusBar as RNStatusBar,
|
|
View,
|
|
Text,
|
|
ActivityIndicator,
|
|
} from 'react-native'
|
|
import { StatusBar } from 'expo-status-bar'
|
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
import { useRouter, useLocalSearchParams } from 'expo-router'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import SearchBar from '@/components/SearchBar'
|
|
import WorksGallery, { type Category, type WorkItem } from '@/components/WorksGallery'
|
|
import { useWorksSearch, type WorksSearchResult } from '@/hooks/use-works-search'
|
|
|
|
/**
|
|
* 将 API 返回的作品数据转换为 WorkItem 格式
|
|
*/
|
|
function convertToWorkItem(apiWork: WorksSearchResult): WorkItem {
|
|
return {
|
|
id: parseInt(apiWork.id, 10),
|
|
date: apiWork.createdAt,
|
|
duration: `${String(Math.floor(apiWork.duration / 60)).padStart(2, '0')}:${String(
|
|
apiWork.duration % 60
|
|
).padStart(2, '0')}`,
|
|
// TODO: 从 API 响应中获取分类信息
|
|
category: '写真' as Category,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 按日期分组作品
|
|
*/
|
|
function groupWorksByDate(works: WorkItem[]): Record<string, WorkItem[]> {
|
|
return works.reduce((acc, work) => {
|
|
const dateKey =
|
|
work.date instanceof Date
|
|
? work.date.toISOString().split('T')[0]
|
|
: new Date(work.date).toISOString().split('T')[0]
|
|
|
|
if (!acc[dateKey]) {
|
|
acc[dateKey] = []
|
|
}
|
|
acc[dateKey].push(work)
|
|
return acc
|
|
}, {} as Record<string, WorkItem[]>)
|
|
}
|
|
|
|
export default function SearchWorksResultsScreen() {
|
|
console.log('========== SearchWorksResultsScreen 组件渲染 ==========')
|
|
|
|
const { t } = useTranslation()
|
|
const router = useRouter()
|
|
const params = useLocalSearchParams()
|
|
const [searchText, setSearchText] = useState((params.q as string) || '')
|
|
const [page, setPage] = useState(1)
|
|
const [allWorks, setAllWorks] = useState<WorksSearchResult[]>([])
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
|
|
|
console.log('========== 当前状态:', { searchText, page, allWorksLength: allWorks.length, isLoadingMore })
|
|
|
|
const categories: Category[] = [
|
|
t('worksList.all') as Category,
|
|
t('worksList.pets') as Category,
|
|
t('worksList.portrait') as Category,
|
|
t('worksList.together') as Category,
|
|
]
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState<Category>(categories[0])
|
|
|
|
// 使用真实的搜索接口
|
|
const { data, works, isLoading, error } = useWorksSearch({
|
|
keyword: searchText,
|
|
category: selectedCategory,
|
|
page,
|
|
limit: 20,
|
|
})
|
|
|
|
// 当搜索关键词或分类变化时,重置页码和累积数据
|
|
useEffect(() => {
|
|
console.log('[SearchResults] 重置: searchText=', searchText, 'category=', selectedCategory)
|
|
setPage(1)
|
|
setAllWorks([])
|
|
}, [searchText, selectedCategory])
|
|
|
|
// 累积新数据
|
|
useEffect(() => {
|
|
console.log('[SearchResults] works变化:', {
|
|
worksLength: works?.length,
|
|
page,
|
|
allWorksLength: allWorks.length,
|
|
isLoading,
|
|
data: data ? { total: data.total, totalPages: data.totalPages } : null
|
|
})
|
|
|
|
if (works) {
|
|
if (works.length > 0) {
|
|
if (page === 1) {
|
|
console.log('[SearchResults] 设置第1页数据')
|
|
setAllWorks(works)
|
|
} else {
|
|
console.log('[SearchResults] 追加第', page, '页数据')
|
|
setAllWorks((prev) => [...prev, ...works])
|
|
}
|
|
} else if (page === 1) {
|
|
console.log('[SearchResults] 第1页无结果,清空数据')
|
|
setAllWorks([])
|
|
}
|
|
setIsLoadingMore(false)
|
|
}
|
|
}, [works])
|
|
|
|
// 加载更多函数
|
|
const handleLoadMore = () => {
|
|
console.log('[SearchResults] handleLoadMore调用:', {
|
|
isLoading,
|
|
isLoadingMore,
|
|
hasData: !!data,
|
|
page,
|
|
totalPages: data?.totalPages
|
|
})
|
|
|
|
if (isLoading || isLoadingMore || !data) {
|
|
console.log('[SearchResults] 跳过加载: 正在加载或无数据')
|
|
return
|
|
}
|
|
|
|
// 检查是否还有更多数据
|
|
if (page >= data.totalPages) {
|
|
console.log('[SearchResults] 跳过加载: 已到最后一页')
|
|
return
|
|
}
|
|
|
|
console.log('[SearchResults] 开始加载第', page + 1, '页')
|
|
setIsLoadingMore(true)
|
|
setPage((prev) => prev + 1)
|
|
}
|
|
|
|
// 根据分类过滤结果(如果 API 不支持分类筛选,则在前端过滤)
|
|
const filteredWorks =
|
|
selectedCategory === categories[0]
|
|
? allWorks
|
|
: allWorks.filter((work) => {
|
|
const convertedWork = convertToWorkItem(work)
|
|
return convertedWork.category === selectedCategory
|
|
})
|
|
|
|
// 转换为 WorkItem 格式
|
|
const workItems: WorkItem[] = filteredWorks.map(convertToWorkItem)
|
|
|
|
// 按日期分组
|
|
const groupedWorks = groupWorksByDate(workItems)
|
|
|
|
// 处理错误状态
|
|
if (error) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<StatusBar style="light" />
|
|
<RNStatusBar barStyle="light-content" />
|
|
|
|
<SearchBar
|
|
searchText={searchText}
|
|
onSearchTextChange={setSearchText}
|
|
onSearch={(text) => {
|
|
router.push({
|
|
pathname: '/searchWorksResults',
|
|
params: { q: text },
|
|
})
|
|
}}
|
|
onBack={() => router.back()}
|
|
placeholder={t('search.searchWorks')}
|
|
marginBottom={0}
|
|
readOnly={true}
|
|
onInputPress={() => {
|
|
router.push({
|
|
pathname: '/searchWorks',
|
|
params: { q: searchText },
|
|
})
|
|
}}
|
|
onClearPress={() => {
|
|
router.push({
|
|
pathname: '/searchWorks',
|
|
params: { q: '' },
|
|
})
|
|
}}
|
|
/>
|
|
|
|
<View style={styles.errorContainer}>
|
|
<Text style={styles.errorText}>
|
|
{error instanceof Error ? error.message : t('search.errorOccurred')}
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<StatusBar style="light" />
|
|
<RNStatusBar barStyle="light-content" />
|
|
|
|
{/* Top Bar with Search */}
|
|
<SearchBar
|
|
searchText={searchText}
|
|
onSearchTextChange={setSearchText}
|
|
onSearch={(text) => {
|
|
router.push({
|
|
pathname: '/searchWorksResults',
|
|
params: { q: text },
|
|
})
|
|
}}
|
|
onBack={() => router.back()}
|
|
placeholder={t('search.searchWorks')}
|
|
marginBottom={0}
|
|
readOnly={true}
|
|
onInputPress={() => {
|
|
router.push({
|
|
pathname: '/searchWorks',
|
|
params: { q: searchText },
|
|
})
|
|
}}
|
|
onClearPress={() => {
|
|
router.push({
|
|
pathname: '/searchWorks',
|
|
params: { q: '' },
|
|
})
|
|
}}
|
|
/>
|
|
|
|
{/* Loading State */}
|
|
{isLoading && (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color="#FF6699" />
|
|
<Text style={styles.loadingText}>{t('search.loading')}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!isLoading && workItems.length === 0 && searchText.trim() && (
|
|
<View style={styles.emptyContainer}>
|
|
<Text style={styles.emptyText}>{t('search.noResults')}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{!isLoading && workItems.length > 0 && (
|
|
<WorksGallery
|
|
categories={categories}
|
|
selectedCategory={selectedCategory}
|
|
onCategoryChange={setSelectedCategory}
|
|
groupedWorks={groupedWorks}
|
|
onWorkPress={(id) => {
|
|
router.push({
|
|
pathname: '/generationRecord' as any,
|
|
params: { id: id.toString() },
|
|
})
|
|
}}
|
|
onEndReached={handleLoadMore}
|
|
ListFooterComponent={
|
|
isLoadingMore ? (
|
|
<View style={styles.loadingMoreContainer} testID="loading-more-indicator">
|
|
<ActivityIndicator size="small" color="#FF6699" />
|
|
<Text style={styles.loadingMoreText}>{t('search.loadingMore')}</Text>
|
|
</View>
|
|
) : undefined
|
|
}
|
|
/>
|
|
)}
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#090A0B',
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#090A0B',
|
|
},
|
|
loadingText: {
|
|
color: '#F5F5F5',
|
|
fontSize: 14,
|
|
marginTop: 12,
|
|
},
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#090A0B',
|
|
paddingHorizontal: 20,
|
|
},
|
|
errorText: {
|
|
color: '#FF6666',
|
|
fontSize: 14,
|
|
textAlign: 'center',
|
|
},
|
|
emptyContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#090A0B',
|
|
},
|
|
emptyText: {
|
|
color: '#8A8A8A',
|
|
fontSize: 14,
|
|
},
|
|
loadingMoreContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
paddingVertical: 20,
|
|
gap: 8,
|
|
},
|
|
loadingMoreText: {
|
|
color: '#F5F5F5',
|
|
fontSize: 12,
|
|
},
|
|
})
|