Files
bw-expo-app/components/templates/template-card.tsx
imeepos b9399bf4cf feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面
- 更新底部导航:将explore改为content,index改为home
- 新增多个页面:积分、兑换、历史记录、结果展示、设置等
- 优化UI组件:新增通用按钮、分类标签、加载状态等组件
- 清理冗余文件:删除旧的认证文档和积分详情页面
- 更新主题配置:调整配色方案和视觉风格
- 修复导入路径:优化组件导入结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 17:53:56 +08:00

284 lines
7.4 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 { ResizeMode } from 'expo-av';
import { Image } from 'expo-image';
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',
},
});