bw-expo-app/components/templates/template-card.tsx

290 lines
7.5 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 { FullscreenMediaModal } from '@/components/media/fullscreen-media-modal';
import { VideoPlayer } from '@/components/video/video-player';
import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template';
import { isVideoTemplate } from '@/utils/media-utils';
import { Image } from 'expo-image';
// ResizeMode 兼容映射
const ResizeMode = {
CONTAIN: 'contain' as const,
COVER: 'cover' as const,
STRETCH: 'fill' as const,
};
import { useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
import { ThemedText } from '../themed-text';
import { ThemedView } from '../themed-view';
interface TemplateCardProps {
template: Template;
onPress?: (template: Template) => void;
allTemplates?: Template[];
currentIndex?: number;
onVideoChange?: (template: Template, index: number) => void;
}
export function TemplateCard({
template,
onPress,
allTemplates = [],
currentIndex = 0,
onVideoChange
}: TemplateCardProps) {
const router = useRouter();
const cardColor = useThemeColor({}, 'card');
const imagePlaceholderColor = useThemeColor({}, 'imagePlaceholder');
const [isMediaFullscreenVisible, setIsMediaFullscreenVisible] = useState(false);
const [currentMediaIndex, setCurrentMediaIndex] = useState(currentIndex);
const isVideo = useMemo(() => {
return isVideoTemplate(template);
}, [template]);
const getImageHeight = () => {
if (template.aspectRatio) {
const [width, height] = template.aspectRatio.split(':').map(Number);
if (width && height) {
return width > height ? 280 : 360;
}
}
return 280;
};
const mediaHeight = getImageHeight();
const handleUseTemplate = () => {
router.push(`/template/${template.id}/run`);
};
const handleFullscreenMedia = () => {
setIsMediaFullscreenVisible(true);
};
const handleMediaIndexChanged = (newIndex: number) => {
setCurrentMediaIndex(newIndex);
const newTemplate = allTemplates[newIndex];
onVideoChange?.(newTemplate, newIndex);
};
const handleCardPress = () => {
if (onPress) {
onPress(template);
} else {
handleFullscreenMedia();
}
};
// 获取当前媒体模板
const getCurrentMediaTemplate = () => {
return allTemplates[currentMediaIndex] || template;
};
const currentTemplate = getCurrentMediaTemplate();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: cardColor }]}
onPress={handleCardPress}
activeOpacity={1}
>
<View style={styles.mediaContainer}>
<ThemedView style={[styles.mediaContent, { height: mediaHeight }]}>
{isVideo ? (
<VideoPlayer
source={{ uri: currentTemplate.previewUrl }}
poster={currentTemplate.coverImageUrl}
style={[styles.videoPlayer, styles.videoCover]}
resizeMode={ResizeMode.COVER}
shouldPlay={true}
isLooping={true}
isMuted={true}
useNativeControls={false}
showPoster={true}
autoPlay={true}
maxHeight={mediaHeight}
onPress={handleCardPress}
/>
) : (
<View style={styles.imageContainer}>
<Image
source={{ uri: template.coverImageUrl }}
style={[
styles.image,
{ backgroundColor: imagePlaceholderColor },
]}
contentFit="cover"
placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
transition={200}
/>
</View>
)}
{template.category && (
<View style={styles.watermark} className='template-category'>
<View style={styles.categoryBadge}>
<ThemedText style={styles.categoryText}>
{template.category.name}
</ThemedText>
</View>
</View>
)}
{template.tags.length > 0 && (
<View style={styles.tagOverlay} className='template-tags'>
{template.tags.slice(0, 2).map((tag) => (
<View key={tag.id} style={styles.tagBadge}>
<ThemedText style={styles.tagText}>
{tag.name}
</ThemedText>
</View>
))}
</View>
)}
</ThemedView>
</View>
{/* 操作按钮区域 */}
<View style={styles.actionArea}>
<ThemedText style={styles.templateTitle} numberOfLines={1}>
{template.title}
</ThemedText>
<TouchableOpacity
style={styles.useButton}
onPress={handleUseTemplate}
activeOpacity={0.8}
>
<ThemedText style={styles.useButtonText}>使</ThemedText>
</TouchableOpacity>
</View>
{/* 统一全屏媒体模态框 */}
<FullscreenMediaModal
visible={isMediaFullscreenVisible}
onClose={() => setIsMediaFullscreenVisible(false)}
currentIndex={currentMediaIndex}
templates={allTemplates}
onIndexChanged={handleMediaIndexChanged}
/>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
flex: 1,
marginBottom: 14,
marginHorizontal: 5,
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 24,
elevation: 4,
},
mediaContainer: {
width: '100%',
},
mediaContent: {
position: 'relative',
width: '100%',
minHeight: 280,
},
image: {
width: '100%',
height: '100%',
},
videoPlayer: {
width: '100%',
height: '100%',
},
videoCover: {
overflow: 'hidden',
width: '100%',
height: '100%',
// React Native中object-fit对应contentFit属性但在VideoPlayer中已经处理了
...Platform.select({
web: {
objectFit: 'cover',
},
}),
},
imageContainer: {
width: '100%',
height: '100%',
},
watermark: {
position: 'absolute',
top: 12,
left: 14,
zIndex: 10,
},
categoryBadge: {
backgroundColor: 'rgba(0, 0, 0, 0.15)',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 100,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
categoryText: {
color: 'rgba(255, 255, 255, 0.85)',
fontSize: 10,
fontWeight: '400',
lineHeight: 6
},
tagOverlay: {
position: 'absolute',
bottom: 16,
left: 24,
right: 24,
zIndex: 10,
flexDirection: 'row',
gap: 8,
justifyContent: 'center',
},
tagBadge: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 100,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
backdropFilter: 'blur(5px)',
},
tagText: {
color: '#fff',
fontSize: 11,
fontWeight: '500',
lineHeight: 4
},
actionArea: {
padding: 12,
backgroundColor: 'rgba(0, 0, 0, 0.02)',
},
templateTitle: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
textAlign: 'center',
},
useButton: {
backgroundColor: '#4ECDC4',
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center',
shadowColor: '#4ECDC4',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 3,
},
useButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
});