148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
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'
|
||
|
||
export interface TemplateCardProps {
|
||
id?: string
|
||
title: 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,
|
||
previewUrl,
|
||
webpPreviewUrl,
|
||
coverImageUrl,
|
||
aspectRatio: aspectRatioString,
|
||
cardWidth,
|
||
onPress,
|
||
testID,
|
||
}) => {
|
||
const aspectRatio = useMemo(() => parseAspectRatio(aspectRatioString), [aspectRatioString])
|
||
const imageUri = useMemo(() => getImageUri(webpPreviewUrl, previewUrl, coverImageUrl), [webpPreviewUrl, previewUrl, coverImageUrl])
|
||
|
||
// 使用 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}>
|
||
{title}
|
||
</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',
|
||
},
|
||
})
|