Initial commit: Expo app with Better Auth integration
- Complete Expo React Native app setup with TypeScript - Better Auth authentication system integration - Secure storage implementation for session tokens - Authentication flow with login/logout functionality - API client configuration for backend communication - Responsive UI components with themed styling - Expo Router navigation setup - Development configuration and scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
235
components/templates/template-card.tsx
Normal file
235
components/templates/template-card.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Video, ResizeMode } from 'expo-av';
|
||||
import { VideoPlayer } from '@/components/video/video-player';
|
||||
import { ThemedText } from '../themed-text';
|
||||
import { ThemedView } from '../themed-view';
|
||||
import { Template } from '@/lib/types/template';
|
||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template;
|
||||
onPress?: (template: Template) => void;
|
||||
}
|
||||
|
||||
export function TemplateCard({ template, onPress }: TemplateCardProps) {
|
||||
const router = useRouter();
|
||||
const cardColor = useThemeColor({}, 'card');
|
||||
const imagePlaceholderColor = useThemeColor({}, 'imagePlaceholder');
|
||||
|
||||
const isVideo = useMemo(() => {
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.previewUrl || '');
|
||||
}, [template.previewUrl]);
|
||||
|
||||
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 handleCardPress = () => {
|
||||
if (onPress) {
|
||||
onPress(template);
|
||||
} else {
|
||||
// 默认行为:跳转到运行页面
|
||||
handleUseTemplate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.card, { backgroundColor: cardColor }]}>
|
||||
<View style={styles.mediaContainer}>
|
||||
<ThemedView style={[styles.mediaContent, { height: mediaHeight }]}>
|
||||
{isVideo ? (
|
||||
<VideoPlayer
|
||||
source={{ uri: template.previewUrl }}
|
||||
poster={template.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}
|
||||
/>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleCardPress} activeOpacity={0.9} style={styles.imageContainer}>
|
||||
<Image
|
||||
source={{ uri: template.coverImageUrl }}
|
||||
style={[
|
||||
styles.image,
|
||||
{ backgroundColor: imagePlaceholderColor },
|
||||
]}
|
||||
contentFit="cover"
|
||||
placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
|
||||
transition={200}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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中已经处理了
|
||||
},
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user