130 lines
2.9 KiB
TypeScript
130 lines
2.9 KiB
TypeScript
import React, { memo } from 'react'
|
||
import { Pressable, StyleSheet, Text, View, ViewStyle } 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 TemplateCardComponent: React.FC<TemplateCardProps> = ({
|
||
id,
|
||
title,
|
||
previewUrl,
|
||
webpPreviewUrl,
|
||
coverImageUrl,
|
||
aspectRatio: aspectRatioString,
|
||
cardWidth,
|
||
onPress,
|
||
testID,
|
||
}) => {
|
||
const aspectRatio = parseAspectRatio(aspectRatioString)
|
||
const imageUri = getImageUri(webpPreviewUrl, previewUrl, coverImageUrl)
|
||
|
||
// 如果没有 id,则不渲染卡片
|
||
if (!id) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<Pressable
|
||
style={[styles.card, { width: cardWidth }]}
|
||
onPress={() => onPress(id)}
|
||
testID={testID}
|
||
>
|
||
<View
|
||
style={[
|
||
styles.cardImageContainer,
|
||
aspectRatio ? { aspectRatio } : undefined,
|
||
].filter(Boolean) as ViewStyle[]}
|
||
>
|
||
<Image
|
||
source={{ uri: imageUri }}
|
||
style={[
|
||
styles.cardImage,
|
||
aspectRatio ? { aspectRatio } : undefined,
|
||
].filter(Boolean) as ViewStyle[]}
|
||
contentFit="cover"
|
||
/>
|
||
<LinearGradient
|
||
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
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',
|
||
},
|
||
})
|