290 lines
7.4 KiB
TypeScript
290 lines
7.4 KiB
TypeScript
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}>
|
||
<View style={styles.categoryBadge}>
|
||
<ThemedText style={styles.categoryText}>
|
||
{template.category.name}
|
||
</ThemedText>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{template.tags.length > 0 && (
|
||
<View style={styles.tagOverlay}>
|
||
{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',
|
||
},
|
||
});
|