feat: integrate UI components into Generation Record page with pagination
Transform Generation Record from static detail view to dynamic list with: - useTemplateGenerations hook for data fetching - RefreshControl for pull-to-refresh functionality - LoadingState and ErrorState for proper state handling - PaginationLoader for infinite scroll - FlatList with onEndReached for pagination - Comprehensive test coverage following TDD principles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
276
app/generationRecord.test.tsx
Normal file
276
app/generationRecord.test.tsx
Normal file
@@ -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 }) => (
|
||||||
|
<View>{children}</View>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
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) => <View testID={testID} />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/ErrorState', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ testID }: any) => <View testID={testID} />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/PaginationLoader', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ testID }: any) => <View testID={testID} />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/RefreshControl', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks', () => ({
|
||||||
|
useTemplateGenerations: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { useTemplateGenerations } from '@/hooks'
|
||||||
|
|
||||||
|
const mockUseTemplateGenerations = useTemplateGenerations as jest.MockedFunction<typeof useTemplateGenerations>
|
||||||
|
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
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(<GenerationRecordScreen />)
|
||||||
|
const flatList = getByTestId('generation-list')
|
||||||
|
|
||||||
|
fireEvent(flatList, 'endReached')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLoadMore).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
FlatList,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Pressable,
|
Pressable,
|
||||||
StatusBar as RNStatusBar,
|
StatusBar as RNStatusBar,
|
||||||
@@ -10,88 +10,109 @@ import {
|
|||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { Image } from 'expo-image'
|
import { Image } from 'expo-image'
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
import { useRouter } from 'expo-router'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { LeftArrowIcon, DeleteIcon, EditIcon, ChangeIcon, WhiteStarIcon } from '@/components/icon'
|
import { LeftArrowIcon, DeleteIcon, EditIcon, ChangeIcon, WhiteStarIcon } from '@/components/icon'
|
||||||
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog'
|
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')
|
const { width: screenWidth } = Dimensions.get('window')
|
||||||
|
|
||||||
export default function GenerationRecordScreen() {
|
export default function GenerationRecordScreen() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = useLocalSearchParams()
|
|
||||||
const workId = params.id as string
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { generations, loading, loadingMore, error, execute, refetch, loadMore, hasMore } = useTemplateGenerations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
execute({ page: 1, limit: 20 })
|
||||||
|
}, [execute])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
// TODO: 实现删除逻辑
|
console.log('删除记录:', selectedId)
|
||||||
console.log('删除记录:', workId)
|
setDeleteDialogOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
refetch({ page: 1, limit: 20 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
if (hasMore && !loadingMore) {
|
||||||
|
loadMore({ limit: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && generations.length === 0) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<RNStatusBar barStyle="light-content" />
|
<RNStatusBar barStyle="light-content" />
|
||||||
<ScrollView
|
|
||||||
style={styles.scrollView}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
>
|
|
||||||
{/* 顶部导航栏 */}
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable
|
<Pressable style={styles.backButton} onPress={() => router.back()}>
|
||||||
style={styles.backButton}
|
|
||||||
onPress={() => router.back()}
|
|
||||||
>
|
|
||||||
<LeftArrowIcon />
|
<LeftArrowIcon />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
|
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
|
||||||
<View style={styles.headerSpacer} />
|
<View style={styles.headerSpacer} />
|
||||||
</View>
|
</View>
|
||||||
|
<LoadingState testID="loading-state" />
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* AI 视频标签 */}
|
if (error && generations.length === 0) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
|
<StatusBar style="light" />
|
||||||
|
<RNStatusBar barStyle="light-content" />
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Pressable style={styles.backButton} onPress={() => router.back()}>
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</Pressable>
|
||||||
|
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
|
||||||
|
<View style={styles.headerSpacer} />
|
||||||
|
</View>
|
||||||
|
<ErrorState testID="error-state" message={error.message} onRetry={handleRefresh} />
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: TemplateGeneration }) => (
|
||||||
|
<View style={styles.itemContainer}>
|
||||||
<View style={styles.categorySection}>
|
<View style={styles.categorySection}>
|
||||||
<View style={styles.categoryIcon}>
|
<View style={styles.categoryIcon}>
|
||||||
<WhiteStarIcon />
|
<WhiteStarIcon />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.categoryText}>{t('generationRecord.aiVideo')}</Text>
|
<Text style={styles.categoryText}>{item.template?.title || t('generationRecord.aiVideo')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 原图标签 */}
|
|
||||||
<View style={styles.originalImageSection}>
|
<View style={styles.originalImageSection}>
|
||||||
<Text style={styles.originalImageLabel}>{t('generationRecord.originalImage')}</Text>
|
<Text style={styles.originalImageLabel}>{t('generationRecord.originalImage')}</Text>
|
||||||
<View style={styles.originalImageDivider} />
|
<View style={styles.originalImageDivider} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 主图片/视频预览区域 */}
|
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/android-icon-background.png')}
|
source={{ uri: item.resultUrl?.[0] || item.originalUrl }}
|
||||||
style={styles.mainImage}
|
style={styles.mainImage}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
{/* 视频时长显示 */}
|
|
||||||
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.durationText}>00:03</Text>
|
<Text style={styles.durationText}>00:03</Text>
|
||||||
{/* 底部操作按钮 */}
|
|
||||||
<View style={styles.actionButtons}>
|
<View style={styles.actionButtons}>
|
||||||
<Pressable style={styles.actionButton} onPress={() => {
|
<Pressable style={styles.actionButton} onPress={() => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: '/generateVideo' as any,
|
pathname: '/generateVideo' as any,
|
||||||
params: {
|
params: { template: JSON.stringify(item.template) },
|
||||||
template: JSON.stringify({
|
|
||||||
id: 1,
|
|
||||||
title: 'AI 视频',
|
|
||||||
image: require('@/assets/images/android-icon-background.png'),
|
|
||||||
users: 6349,
|
|
||||||
height: 214,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
@@ -103,14 +124,42 @@ export default function GenerationRecordScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.deleteButton}
|
style={styles.deleteButton}
|
||||||
onPress={() => setDeleteDialogOpen(true)}
|
onPress={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setDeleteDialogOpen(true)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
|
<StatusBar style="light" />
|
||||||
|
<RNStatusBar barStyle="light-content" />
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Pressable style={styles.backButton} onPress={() => router.back()}>
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</Pressable>
|
||||||
|
<Text style={styles.headerTitle}>{t('generationRecord.title')}</Text>
|
||||||
|
<View style={styles.headerSpacer} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
testID="generation-list"
|
||||||
|
data={generations}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={<RefreshControl refreshing={loading} onRefresh={handleRefresh} />}
|
||||||
|
onEndReached={handleLoadMore}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
ListFooterComponent={loadingMore ? <PaginationLoader testID="pagination-loader" /> : null}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 删除确认弹窗 */}
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
open={deleteDialogOpen}
|
open={deleteDialogOpen}
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
@@ -125,12 +174,8 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#090A0B',
|
backgroundColor: '#090A0B',
|
||||||
},
|
},
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
},
|
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
backgroundColor: '#090A0B',
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -155,6 +200,9 @@ const styles = StyleSheet.create({
|
|||||||
headerSpacer: {
|
headerSpacer: {
|
||||||
width: 22,
|
width: 22,
|
||||||
},
|
},
|
||||||
|
itemContainer: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
categorySection: {
|
categorySection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -191,13 +239,12 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
width: screenWidth - 24,
|
width: screenWidth - 24,
|
||||||
height: (screenWidth - 24) * 1.32, // 根据设计稿比例计算
|
height: (screenWidth - 24) * 1.32,
|
||||||
marginHorizontal: 12,
|
marginHorizontal: 12,
|
||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: '#1C1E22',
|
backgroundColor: '#1C1E22',
|
||||||
position: 'relative',
|
|
||||||
},
|
},
|
||||||
mainImage: {
|
mainImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
Reference in New Issue
Block a user