expo-popcore-app/components/blocks/home/TemplateCard.tsx

158 lines
3.9 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 React, { memo, useCallback, useMemo } from 'react'
import { Pressable, StyleSheet, Text, View, ViewStyle, ImageStyle } from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
import { useTranslation } from 'react-i18next'
export interface TemplateCardProps {
id?: string
title: string
titleEn?: string
previewUrl?: string
webpPreviewUrl?: string
coverImageUrl?: string
aspectRatio?: string
cardWidth: number
onPress: (id: string) => void
testID?: string
}
/**
* 解析 aspectRatio 字符串为数字
* 例如: "128:128" -> 1, "16:9" -> 1.777..., "1.5" -> 1.5
*/
export function parseAspectRatio(aspectRatioString?: string): number | undefined {
if (!aspectRatioString) return undefined
if (aspectRatioString.includes(':')) {
const [w, h] = aspectRatioString.split(':').map(Number)
return w / h
}
return Number(aspectRatioString)
}
/**
* 获取图片源 URI按优先级: webpPreviewUrl > previewUrl > coverImageUrl
*/
export function getImageUri(
webpPreviewUrl?: string,
previewUrl?: string,
coverImageUrl?: string
): string | undefined {
return webpPreviewUrl || previewUrl || coverImageUrl
}
// 渐变颜色常量,避免每次渲染创建新数组
const GRADIENT_COLORS = ['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)'] as const
const GRADIENT_START = { x: 0, y: 0 }
const GRADIENT_END = { x: 0, y: 1 }
const TemplateCardComponent: React.FC<TemplateCardProps> = ({
id,
title,
titleEn,
previewUrl,
webpPreviewUrl,
coverImageUrl,
aspectRatio: aspectRatioString,
cardWidth,
onPress,
testID,
}) => {
const { i18n } = useTranslation()
const aspectRatio = useMemo(() => parseAspectRatio(aspectRatioString), [aspectRatioString])
const imageUri = useMemo(() => getImageUri(webpPreviewUrl, previewUrl, coverImageUrl), [webpPreviewUrl, previewUrl, coverImageUrl])
// Select display title based on current language
const displayTitle = useMemo(() => {
const isEnglish = i18n.language === 'en-US'
return isEnglish && titleEn ? titleEn : title
}, [i18n.language, title, titleEn])
// 使用 useCallback 缓存 onPress 回调
const handlePress = useCallback(() => {
if (id) {
onPress(id)
}
}, [id, onPress])
// 缓存样式计算
const cardStyle = useMemo(() => [styles.card, { width: cardWidth }], [cardWidth])
const containerStyle = useMemo(() =>
[styles.cardImageContainer, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ViewStyle[],
[aspectRatio]
)
const imageStyle = useMemo(() =>
[styles.cardImage, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ImageStyle[],
[aspectRatio]
)
// 如果没有 id则不渲染卡片
if (!id) {
return null
}
return (
<Pressable
style={cardStyle}
onPress={handlePress}
testID={testID}
>
<View style={containerStyle}>
<Image
source={{ uri: imageUri }}
style={imageStyle}
contentFit="cover"
cachePolicy="memory-disk"
recyclingKey={id}
transition={200}
/>
<LinearGradient
colors={GRADIENT_COLORS}
start={GRADIENT_START}
end={GRADIENT_END}
style={styles.cardImageGradient}
/>
<Text style={styles.cardTitle} numberOfLines={1}>
{displayTitle}
</Text>
</View>
</Pressable>
)
}
export const TemplateCard = memo(TemplateCardComponent)
const styles = StyleSheet.create({
card: {
marginBottom: 5,
},
cardImageContainer: {
width: '100%',
borderRadius: 8,
overflow: 'hidden',
position: 'relative',
},
cardImage: {
width: '100%',
borderRadius: 8,
},
cardImageGradient: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '50%',
},
cardTitle: {
position: 'absolute',
bottom: 8,
left: 8,
right: 8,
color: '#F5F5F5',
fontSize: 12,
fontWeight: '500',
},
})