feat: add TemplateGrid and TitleBar components with tests

- Implemented TemplateGrid component for displaying templates in a grid layout.
- Added calculateCardWidth helper function for dynamic card sizing.
- Created TitleBar component for displaying the title and points with interaction.
- Added unit tests for TemplateGrid and TitleBar components to ensure proper functionality.
- Introduced useStickyTabs and useTabNavigation hooks with tests for managing sticky tab behavior and navigation logic.
- Implemented useTemplateFilter hook for filtering templates based on video content.
- Added comprehensive tests for all new hooks and components to validate behavior and edge cases.
This commit is contained in:
imeepos
2026-01-26 12:43:20 +08:00
parent bf11241d68
commit 2757b68756
23 changed files with 2637 additions and 586 deletions

View File

@@ -0,0 +1,124 @@
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)
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',
},
})