expo-popcore-app/components/WorksGallery.tsx

264 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
View,
Text,
StyleSheet,
ScrollView,
Dimensions,
Pressable,
} from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
import { useTranslation } from 'react-i18next'
const { width: screenWidth } = Dimensions.get('window')
const GALLERY_GAP = 1
const GALLERY_COLUMNS = 4
const GALLERY_ITEM_SIZE = Math.floor(
(screenWidth - GALLERY_GAP * (GALLERY_COLUMNS - 1)) / GALLERY_COLUMNS
)
type Category = '全部' | '萌宠' | '写真' | '合拍'
interface WorkItem {
id: number
date: Date | string
duration: string
category: Category
}
interface WorksGalleryProps {
categories: Category[]
selectedCategory: Category
onCategoryChange: (category: Category) => void
groupedWorks: Record<string, WorkItem[]>
onWorkPress: (id: number) => void
onEndReached?: () => void
ListFooterComponent?: React.ReactElement
}
export default function WorksGallery({
categories,
selectedCategory,
onCategoryChange,
groupedWorks,
onWorkPress,
onEndReached,
ListFooterComponent,
}: WorksGalleryProps) {
const { i18n } = useTranslation()
// 格式化日期函数
const formatDate = (date: Date | string): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date
const locale = i18n.language === 'zh-CN' ? 'zh-CN' : 'en-US'
if (locale === 'zh-CN') {
// 中文格式2025年11月28日
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
}).format(dateObj).replace(/\//g, '年').replace(/(\d+)$/, '$1日')
} else {
// 英文格式November 28, 2025
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(dateObj)
}
}
return (
<>
{/* 分类标签 */}
<View style={styles.categoryContainer}>
{categories.map((category) => {
const isSelected = selectedCategory === category
return (
<Pressable
key={category}
style={styles.categoryTagWrapper}
onPress={() => onCategoryChange(category)}
>
{isSelected ? (
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.categoryTagGradient}
>
<View style={styles.categoryTag}>
<Text
style={[
styles.categoryTagText,
styles.categoryTagTextActive,
]}
>
{category}
</Text>
</View>
</LinearGradient>
) : (
<View style={styles.categoryTag}>
<Text style={styles.categoryTagText}>
{category}
</Text>
</View>
)}
</Pressable>
)
})}
</View>
{/* 作品列表 */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={(e) => {
const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 100
if (isCloseToBottom && onEndReached) {
onEndReached()
}
}}
scrollEventThrottle={400}
>
{Object.entries(groupedWorks).map(([dateKey, works]) => {
// 从第一个作品获取日期对象
const dateObj = works[0]?.date
const formattedDate = dateObj ? formatDate(dateObj) : dateKey
return (
<View key={dateKey}>
<Text style={styles.dateText}>{formattedDate}</Text>
<View style={styles.galleryGrid}>
{works.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
// 每行的前几个 item 有右边距最后一个没有4 列)
index % GALLERY_COLUMNS !== GALLERY_COLUMNS - 1 &&
styles.galleryItemMarginRight,
// 所有item都有下边距最后一行也会有但影响不大
styles.galleryItemMarginBottom,
]}
onPress={() => onWorkPress(item.id)}
>
<Image
source={require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
<View style={styles.durationBadge}>
<Text style={styles.durationText}>
{item.duration}
</Text>
</View>
</Pressable>
))}
</View>
</View>
)
})}
{ListFooterComponent}
</ScrollView>
</>
)
}
const styles = StyleSheet.create({
categoryContainer: {
flexDirection: 'row',
paddingHorizontal: 14,
paddingTop: 16,
gap: 8,
},
categoryTagWrapper: {
minWidth: 50,
height: 30,
},
categoryTagGradient: {
width: '100%',
height: '100%',
borderRadius: 8,
padding: 1,
},
categoryTag: {
flex: 1,
borderRadius: 8,
backgroundColor: '#1C1E22',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 12,
},
categoryTagText: {
color: '#F5F5F5',
fontSize: 11,
fontWeight: '500',
},
categoryTagTextActive: {
color: '#F5F5F5',
fontWeight: '500',
},
scrollView: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
paddingBottom: 20,
},
dateText: {
color: '#F5F5F5',
fontSize: 12,
fontWeight: '500',
marginBottom: 8,
marginTop: 16,
paddingLeft: 14,
},
galleryGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
galleryItem: {
width: GALLERY_ITEM_SIZE,
// 使用等比例 1:1保证容器永远是正方形
aspectRatio: 1,
overflow: 'hidden',
backgroundColor: '#1C1E22',
position: 'relative',
},
galleryItemMarginRight: {
marginRight: GALLERY_GAP,
},
galleryItemMarginBottom: {
marginBottom: GALLERY_GAP,
},
galleryImage: {
width: '100%',
// 高度由 aspectRatio 决定,避免拉伸
height: undefined,
aspectRatio: 1,
},
durationBadge: {
position: 'absolute',
right: 2,
bottom: 4,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
backgroundColor: '#00000080',
},
durationText: {
color: '#F5F5F5',
fontSize: 10,
fontWeight: '500',
},
})
export type { Category, WorkItem }