fix: search bug

This commit is contained in:
imeepos
2026-02-02 15:21:59 +08:00
parent 59ba6a3c56
commit 95dd556ded
2 changed files with 295 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import {
StyleSheet,
StatusBar as RNStatusBar,
@@ -53,6 +53,9 @@ export default function SearchWorksResultsScreen() {
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)
const categories: Category[] = [
t('worksList.all') as Category,
@@ -67,15 +70,44 @@ export default function SearchWorksResultsScreen() {
const { data, works, isLoading, error } = useWorksSearch({
keyword: searchText,
category: selectedCategory,
page: 1,
page,
limit: 20,
})
// 当搜索关键词或分类变化时,重置页码和累积数据
useEffect(() => {
setPage(1)
setAllWorks([])
}, [searchText, selectedCategory])
// 累积新数据
useEffect(() => {
if (works && works.length > 0) {
if (page === 1) {
setAllWorks(works)
} else {
setAllWorks((prev) => [...prev, ...works])
}
setIsLoadingMore(false)
}
}, [works, page])
// 加载更多函数
const handleLoadMore = () => {
if (isLoading || isLoadingMore || !data) return
// 检查是否还有更多数据
if (page >= data.totalPages) return
setIsLoadingMore(true)
setPage((prev) => prev + 1)
}
// 根据分类过滤结果(如果 API 不支持分类筛选,则在前端过滤)
const filteredWorks =
selectedCategory === categories[0]
? works
: works.filter((work) => {
? allWorks
: allWorks.filter((work) => {
const convertedWork = convertToWorkItem(work)
return convertedWork.category === selectedCategory
})
@@ -190,6 +222,15 @@ export default function SearchWorksResultsScreen() {
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>
@@ -234,4 +275,15 @@ const styles = StyleSheet.create({
color: '#8A8A8A',
fontSize: 14,
},
loadingMoreContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 20,
gap: 8,
},
loadingMoreText: {
color: '#F5F5F5',
fontSize: 12,
},
})

View File

@@ -0,0 +1,239 @@
/**
* TDD Phase 1: RED - Write failing tests first
*
* 测试搜索结果页面的分页功能
*/
import React from 'react'
import { render, waitFor, fireEvent } from '@testing-library/react-native'
import { useRouter, useLocalSearchParams } from 'expo-router'
import SearchWorksResultsScreen from '@/app/searchWorksResults'
import { useWorksSearch } from '@/hooks/use-works-search'
// Mock dependencies
jest.mock('expo-router', () => ({
useRouter: jest.fn(),
useLocalSearchParams: jest.fn(),
}))
jest.mock('@/hooks/use-works-search', () => ({
useWorksSearch: jest.fn(),
}))
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { language: 'zh-CN' },
}),
}))
jest.mock('@/components/SearchBar', () => 'SearchBar')
jest.mock('@/components/WorksGallery', () => 'WorksGallery')
describe('SearchWorksResultsScreen - Pagination', () => {
const mockRouter = {
push: jest.fn(),
back: jest.fn(),
}
beforeEach(() => {
jest.clearAllMocks()
;(useRouter as jest.Mock).mockReturnValue(mockRouter)
;(useLocalSearchParams as jest.Mock).mockReturnValue({ q: '测试' })
})
describe('Load More Functionality', () => {
it('should load first page on initial render', () => {
const mockUseWorksSearch = useWorksSearch as jest.Mock
mockUseWorksSearch.mockReturnValue({
data: {
data: [
{
id: '1',
createdAt: new Date('2025-01-15'),
template: { id: 't1', name: 'Template 1' },
status: 'completed',
duration: 5,
},
],
total: 50,
page: 1,
limit: 20,
totalPages: 3,
},
works: [
{
id: '1',
createdAt: new Date('2025-01-15'),
template: { id: 't1', name: 'Template 1' },
status: 'completed',
duration: 5,
},
],
isLoading: false,
error: null,
})
render(<SearchWorksResultsScreen />)
// 验证初始调用使用 page: 1
expect(mockUseWorksSearch).toHaveBeenCalledWith(
expect.objectContaining({
page: 1,
})
)
})
it('should accumulate results when loading more pages', async () => {
const mockUseWorksSearch = useWorksSearch as jest.Mock
// 第一页数据
const page1Data = [
{
id: '1',
createdAt: new Date('2025-01-15'),
template: { id: 't1', name: 'Template 1' },
status: 'completed',
duration: 5,
},
]
// 第二页数据
const page2Data = [
{
id: '2',
createdAt: new Date('2025-01-14'),
template: { id: 't2', name: 'Template 2' },
status: 'completed',
duration: 10,
},
]
mockUseWorksSearch.mockReturnValue({
data: {
data: page1Data,
total: 50,
page: 1,
limit: 20,
totalPages: 3,
},
works: page1Data,
isLoading: false,
error: null,
loadMore: jest.fn(),
})
const { rerender } = render(<SearchWorksResultsScreen />)
// 模拟加载第二页
mockUseWorksSearch.mockReturnValue({
data: {
data: [...page1Data, ...page2Data],
total: 50,
page: 2,
limit: 20,
totalPages: 3,
},
works: [...page1Data, ...page2Data],
isLoading: false,
error: null,
loadMore: jest.fn(),
})
rerender(<SearchWorksResultsScreen />)
// 验证结果累积
expect(mockUseWorksSearch).toHaveBeenCalled()
})
it('should not load more when already loading', () => {
const mockLoadMore = jest.fn()
const mockUseWorksSearch = useWorksSearch as jest.Mock
mockUseWorksSearch.mockReturnValue({
data: {
data: [],
total: 50,
page: 1,
limit: 20,
totalPages: 3,
},
works: [],
isLoading: true,
error: null,
loadMore: mockLoadMore,
})
render(<SearchWorksResultsScreen />)
// 验证加载中时不应触发新的加载
expect(mockLoadMore).not.toHaveBeenCalled()
})
it('should not load more when all pages loaded', () => {
const mockLoadMore = jest.fn()
const mockUseWorksSearch = useWorksSearch as jest.Mock
mockUseWorksSearch.mockReturnValue({
data: {
data: [
{
id: '1',
createdAt: new Date('2025-01-15'),
template: { id: 't1', name: 'Template 1' },
status: 'completed',
duration: 5,
},
],
total: 1,
page: 1,
limit: 20,
totalPages: 1,
},
works: [
{
id: '1',
createdAt: new Date('2025-01-15'),
template: { id: 't1', name: 'Template 1' },
status: 'completed',
duration: 5,
},
],
isLoading: false,
error: null,
loadMore: mockLoadMore,
hasMore: false,
})
render(<SearchWorksResultsScreen />)
// 验证没有更多数据时不应触发加载
expect(mockLoadMore).not.toHaveBeenCalled()
})
})
describe('Loading State', () => {
it('should show loading indicator when loading more', () => {
const mockUseWorksSearch = useWorksSearch as jest.Mock
mockUseWorksSearch.mockReturnValue({
data: {
data: [],
total: 50,
page: 2,
limit: 20,
totalPages: 3,
},
works: [],
isLoading: true,
error: null,
isLoadingMore: true,
})
const { getByTestId } = render(<SearchWorksResultsScreen />)
// 验证显示加载指示器
expect(() => getByTestId('loading-more-indicator')).not.toThrow()
})
})
})