expo-popcore-app/components/SearchResultsGrid.tsx

192 lines
5.3 KiB
TypeScript

import { useState } from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
Dimensions,
Pressable,
ActivityIndicator,
} from 'react-native'
import { Image } from 'expo-image'
import { useRouter } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { WhiteStarIcon } from '@/components/icon'
import type { TemplateDetail } from '@/hooks'
const { width: screenWidth } = Dimensions.get('window')
interface TemplateSearchResultItem {
id: string
title: string
image: string | { uri: string }
previewUrl?: string
coverImageUrl?: string
aspectRatio?: number
height?: number
}
interface SearchResultsGridProps {
results: TemplateSearchResultItem[]
loading?: boolean
onEndReached?: () => void
ListFooterComponent?: React.ReactElement | null
}
// 计算卡片高度的辅助函数
const calculateCardHeight = (width: number, aspectRatio?: number): number => {
if (aspectRatio) {
return width / aspectRatio
}
// 默认宽高比
return width * 1.2
}
export default function SearchResultsGrid({ results, loading, onEndReached, ListFooterComponent }: SearchResultsGridProps) {
const { t } = useTranslation()
const router = useRouter()
const [gridWidth, setGridWidth] = useState(screenWidth)
const horizontalPadding = 8 * 2
const cardGap = 5
const cardWidth = (gridWidth - horizontalPadding - cardGap) / 2
const handleCardPress = (item: TemplateSearchResultItem) => {
router.push({
pathname: '/templateDetail' as any,
params: { id: item.id.toString() },
})
}
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 (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#FF6699" />
</View>
)
}
if (results.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>{t('searchResults.noResults')}</Text>
</View>
)
}
return (
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={400}
>
<View
style={styles.gridContainer}
onLayout={(event) => {
const { width } = event.nativeEvent.layout
setGridWidth(width)
}}
>
{results.map((item, index) => {
const height = item.height || calculateCardHeight(cardWidth, item.aspectRatio)
return (
<Pressable
key={item.id}
style={[
styles.card,
{ width: cardWidth },
index % 2 === 0 ? styles.cardLeft : styles.cardRight,
]}
onPress={() => handleCardPress(item)}
>
<View style={[styles.cardImageContainer, { height }]}>
<Image
source={item.image}
style={styles.cardImage}
contentFit="cover"
/>
<Text style={styles.cardTitle} numberOfLines={1}>
{item.title}
</Text>
</View>
</Pressable>
)
})}
</View>
{ListFooterComponent}
</ScrollView>
)
}
const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 40,
},
scrollView: {
flex: 1,
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 8,
justifyContent: 'space-between',
},
card: {
backgroundColor: '#16181B',
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
marginBottom: 12,
overflow: 'hidden',
},
cardLeft: {
marginRight: 0,
},
cardRight: {
marginLeft: 0,
},
cardImageContainer: {
width: '100%',
borderRadius: 12,
overflow: 'hidden',
marginBottom: 8,
position: 'relative',
},
cardImage: {
width: '100%',
height: '100%',
},
cardTitle: {
position: 'absolute',
bottom: 8,
left: 8,
color: '#FFFFFF',
fontSize: 12,
fontWeight: '500',
},
emptyContainer: {
flex: 1,
backgroundColor: '#090A0B',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
},
emptyText: {
color: '#8A8A8A',
fontSize: 14,
},
})