diff --git a/app/generationRecord.test.tsx b/app/generationRecord.test.tsx
new file mode 100644
index 0000000..528bc46
--- /dev/null
+++ b/app/generationRecord.test.tsx
@@ -0,0 +1,276 @@
+import React from 'react'
+import { render, fireEvent, waitFor } from '@testing-library/react-native'
+import { View } from 'react-native'
+import GenerationRecordScreen from './generationRecord'
+
+jest.mock('expo-router', () => ({
+ useRouter: jest.fn(() => ({
+ back: jest.fn(),
+ push: jest.fn(),
+ })),
+}))
+
+jest.mock('react-i18next', () => ({
+ useTranslation: jest.fn(() => ({
+ t: (key: string) => key,
+ })),
+}))
+
+jest.mock('expo-status-bar', () => ({
+ StatusBar: 'StatusBar',
+}))
+
+jest.mock('react-native-safe-area-context', () => ({
+ SafeAreaView: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+
+jest.mock('expo-image', () => ({
+ Image: 'Image',
+}))
+
+jest.mock('@/components/icon', () => ({
+ LeftArrowIcon: () => null,
+ DeleteIcon: () => null,
+ EditIcon: () => null,
+ ChangeIcon: () => null,
+ WhiteStarIcon: () => null,
+}))
+
+jest.mock('@/components/ui/delete-confirm-dialog', () => ({
+ DeleteConfirmDialog: () => null,
+}))
+
+jest.mock('@/components/LoadingState', () => ({
+ __esModule: true,
+ default: ({ testID }: any) => ,
+}))
+
+jest.mock('@/components/ErrorState', () => ({
+ __esModule: true,
+ default: ({ testID }: any) => ,
+}))
+
+jest.mock('@/components/PaginationLoader', () => ({
+ __esModule: true,
+ default: ({ testID }: any) => ,
+}))
+
+jest.mock('@/components/RefreshControl', () => ({
+ __esModule: true,
+ default: () => null,
+}))
+
+jest.mock('@/hooks', () => ({
+ useTemplateGenerations: jest.fn(),
+}))
+
+import { useTemplateGenerations } from '@/hooks'
+
+const mockUseTemplateGenerations = useTemplateGenerations as jest.MockedFunction
+
+describe('GenerationRecordScreen', () => {
+ const mockExecute = jest.fn()
+ const mockRefetch = jest.fn()
+ const mockLoadMore = jest.fn()
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('shows loading state on initial load', () => {
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: [],
+ loading: true,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: true,
+ data: undefined,
+ })
+
+ const { getByTestId } = render()
+ expect(getByTestId('loading-state')).toBeTruthy()
+ })
+
+ it('shows error state when error occurs', () => {
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: [],
+ loading: false,
+ loadingMore: false,
+ error: { message: 'Failed to load' } as any,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: false,
+ data: undefined,
+ })
+
+ const { getByTestId } = render()
+ expect(getByTestId('error-state')).toBeTruthy()
+ })
+
+ it('renders generation list when data is loaded', () => {
+ const mockGenerations = [
+ {
+ id: '1',
+ template: { title: 'Test Template' },
+ resultUrl: ['https://example.com/result.jpg'],
+ originalUrl: 'https://example.com/original.jpg',
+ },
+ ]
+
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: mockGenerations as any,
+ loading: false,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: true,
+ data: { data: mockGenerations } as any,
+ })
+
+ const { getByText } = render()
+ expect(getByText('Test Template')).toBeTruthy()
+ })
+
+ it('shows pagination loader when loading more', () => {
+ const mockGenerations = [
+ {
+ id: '1',
+ template: { title: 'Test Template' },
+ resultUrl: ['https://example.com/result.jpg'],
+ },
+ ]
+
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: mockGenerations as any,
+ loading: false,
+ loadingMore: true,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: true,
+ data: { data: mockGenerations } as any,
+ })
+
+ const { getByTestId } = render()
+ expect(getByTestId('pagination-loader')).toBeTruthy()
+ })
+
+ it('calls execute on mount', () => {
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: [],
+ loading: false,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: false,
+ data: undefined,
+ })
+
+ render()
+ expect(mockExecute).toHaveBeenCalledWith({ page: 1, limit: 20 })
+ })
+
+ it('calls refetch on pull to refresh', async () => {
+ const mockGenerations = [
+ {
+ id: '1',
+ template: { title: 'Test Template' },
+ resultUrl: ['https://example.com/result.jpg'],
+ },
+ ]
+
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: mockGenerations as any,
+ loading: false,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: true,
+ data: { data: mockGenerations } as any,
+ })
+
+ const { getByTestId } = render()
+ const flatList = getByTestId('generation-list')
+
+ fireEvent(flatList, 'refresh')
+
+ await waitFor(() => {
+ expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
+ })
+ })
+
+ it('calls loadMore when reaching end of list', async () => {
+ const mockGenerations = [
+ {
+ id: '1',
+ template: { title: 'Test Template' },
+ resultUrl: ['https://example.com/result.jpg'],
+ },
+ ]
+
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: mockGenerations as any,
+ loading: false,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: true,
+ data: { data: mockGenerations } as any,
+ })
+
+ const { getByTestId } = render()
+ const flatList = getByTestId('generation-list')
+
+ fireEvent(flatList, 'endReached')
+
+ await waitFor(() => {
+ expect(mockLoadMore).toHaveBeenCalledWith({ limit: 20 })
+ })
+ })
+
+ it('does not call loadMore when hasMore is false', async () => {
+ const mockGenerations = [
+ {
+ id: '1',
+ template: { title: 'Test Template' },
+ resultUrl: ['https://example.com/result.jpg'],
+ },
+ ]
+
+ mockUseTemplateGenerations.mockReturnValue({
+ generations: mockGenerations as any,
+ loading: false,
+ loadingMore: false,
+ error: null,
+ execute: mockExecute,
+ refetch: mockRefetch,
+ loadMore: mockLoadMore,
+ hasMore: false,
+ data: { data: mockGenerations } as any,
+ })
+
+ const { getByTestId } = render()
+ const flatList = getByTestId('generation-list')
+
+ fireEvent(flatList, 'endReached')
+
+ await waitFor(() => {
+ expect(mockLoadMore).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/app/generationRecord.tsx b/app/generationRecord.tsx
index 2128e65..75ef02d 100644
--- a/app/generationRecord.tsx
+++ b/app/generationRecord.tsx
@@ -2,7 +2,7 @@ import {
View,
Text,
StyleSheet,
- ScrollView,
+ FlatList,
Dimensions,
Pressable,
StatusBar as RNStatusBar,
@@ -10,107 +10,156 @@ import {
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image'
-import { useRouter, useLocalSearchParams } from 'expo-router'
-import { useState } from 'react'
+import { useRouter } from 'expo-router'
+import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LeftArrowIcon, DeleteIcon, EditIcon, ChangeIcon, WhiteStarIcon } from '@/components/icon'
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog'
+import LoadingState from '@/components/LoadingState'
+import ErrorState from '@/components/ErrorState'
+import PaginationLoader from '@/components/PaginationLoader'
+import RefreshControl from '@/components/RefreshControl'
+import { useTemplateGenerations, type TemplateGeneration } from '@/hooks'
const { width: screenWidth } = Dimensions.get('window')
export default function GenerationRecordScreen() {
const { t } = useTranslation()
const router = useRouter()
- const params = useLocalSearchParams()
- const workId = params.id as string
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+
+ const { generations, loading, loadingMore, error, execute, refetch, loadMore, hasMore } = useTemplateGenerations()
+
+ useEffect(() => {
+ execute({ page: 1, limit: 20 })
+ }, [execute])
const handleDelete = () => {
- // TODO: 实现删除逻辑
- console.log('删除记录:', workId)
+ console.log('删除记录:', selectedId)
+ setDeleteDialogOpen(false)
}
- return (
-
-
-
-
- {/* 顶部导航栏 */}
+ const handleRefresh = () => {
+ refetch({ page: 1, limit: 20 })
+ }
+
+ const handleLoadMore = () => {
+ if (hasMore && !loadingMore) {
+ loadMore({ limit: 20 })
+ }
+ }
+
+ if (loading && generations.length === 0) {
+ return (
+
+
+
- router.back()}
- >
+ router.back()}>
{t('generationRecord.title')}
+
+
+ )
+ }
- {/* AI 视频标签 */}
-
-
-
-
- {t('generationRecord.aiVideo')}
-
-
- {/* 原图标签 */}
-
- {t('generationRecord.originalImage')}
-
-
-
- {/* 主图片/视频预览区域 */}
-
-
- {/* 视频时长显示 */}
-
-
-
- 00:03
- {/* 底部操作按钮 */}
-
- {
- router.push({
- pathname: '/generateVideo' as any,
- params: {
- template: JSON.stringify({
- id: 1,
- title: 'AI 视频',
- image: require('@/assets/images/android-icon-background.png'),
- users: 6349,
- height: 214,
- }),
- },
- })
- }}>
-
- {t('generationRecord.reEdit')}
-
-
-
- {t('generationRecord.regenerate')}
-
- setDeleteDialogOpen(true)}
- >
-
+ if (error && generations.length === 0) {
+ return (
+
+
+
+
+ router.back()}>
+
+ {t('generationRecord.title')}
+
-
+
+
+ )
+ }
+
+ const renderItem = ({ item }: { item: TemplateGeneration }) => (
+
+
+
+
+
+ {item.template?.title || t('generationRecord.aiVideo')}
+
+
+
+ {t('generationRecord.originalImage')}
+
+
+
+
+
+
+ 00:03
+
+
+ {
+ router.push({
+ pathname: '/generateVideo' as any,
+ params: { template: JSON.stringify(item.template) },
+ })
+ }}>
+
+ {t('generationRecord.reEdit')}
+
+
+
+ {t('generationRecord.regenerate')}
+
+ {
+ setSelectedId(item.id)
+ setDeleteDialogOpen(true)
+ }}
+ >
+
+
+
+
+ )
+
+ return (
+
+
+
+
+ router.back()}>
+
+
+ {t('generationRecord.title')}
+
+
+
+ item.id}
+ contentContainerStyle={styles.scrollContent}
+ showsVerticalScrollIndicator={false}
+ refreshControl={}
+ onEndReached={handleLoadMore}
+ onEndReachedThreshold={0.5}
+ ListFooterComponent={loadingMore ? : null}
+ />
- {/* 删除确认弹窗 */}