Files
bw-expo-app/app/result.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

543 lines
14 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 { ThemedView } from '@/components/themed-view';
import { ResizeMode, Video } from 'expo-av';
import * as MediaLibrary from 'expo-media-library';
import { router, useLocalSearchParams } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
Copy,
Download,
Maximize2,
Plus,
X,
ArrowLeft,
} from 'lucide-react';
import React, { useEffect, useState } from 'react';
import {
Alert,
Dimensions,
Image,
ImageStyle,
Modal,
ScrollView,
StyleSheet,
StyleProp,
Text,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native';
const { width: screenWidth } = Dimensions.get('window');
const HERO_HEIGHT = screenWidth * 0.58;
const DETAIL_HEIGHT = screenWidth * 0.95;
interface ResultWithTemplate {
id: string;
userId: string;
templateId: string;
type: 'TEXT' | 'IMAGE' | 'VIDEO';
resultUrl: string[];
status: 'pending' | 'running' | 'completed' | 'failed';
creditsCost?: number;
creditsTransactionId?: string;
createdAt: string;
updatedAt: string;
templateName?: string;
templateThumbnail?: string;
templateDescription?: string;
}
export default function ResultPage() {
const { id } = useLocalSearchParams<{ id: string }>();
const insets = useSafeAreaInsets();
const [result, setResult] = useState<ResultWithTemplate | null>(null);
const [fullscreenVisible, setFullscreenVisible] = useState(false);
const [fullscreenContent, setFullscreenContent] = useState<{
type: 'image' | 'video';
url: string;
} | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadResult();
}, [id]);
const loadResult = async () => {
try {
const typeParam = typeof id === 'string' ? id.split('_')[1] : 'text';
const resultId = typeof id === 'string' ? id : 'res1_text';
const getResultByType = () => {
switch (typeParam) {
case 'image':
return {
id: resultId,
userId: 'user1',
templateId: 'tpl2',
type: 'IMAGE' as const,
resultUrl: ['https://picsum.photos/800/600?random=1'],
status: 'completed' as const,
creditsCost: 20,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateName: 'AI图片生成',
templateThumbnail: 'https://picsum.photos/60/60?random=2',
templateDescription: '使用AI生成高质量图片',
};
case 'video':
return {
id: resultId,
userId: 'user1',
templateId: 'tpl3',
type: 'VIDEO' as const,
resultUrl: ['https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'],
status: 'completed' as const,
creditsCost: 50,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateName: 'AI视频生成',
templateThumbnail: 'https://picsum.photos/60/60?random=3',
templateDescription: '基于文本生成视频内容',
};
default:
return {
id: resultId,
userId: 'user1',
templateId: 'tpl1',
type: 'TEXT' as const,
resultUrl: [
'这是一段AI生成的营销文案专门为您的产品精心打造。它突出了产品的核心优势采用了吸引人的语言风格能够有效提升用户的购买欲望和品牌认知度。通过深入分析目标受众的需求和痛点我们精心设计了这份文案旨在最大化品牌影响力和转化效果。',
],
status: 'completed' as const,
creditsCost: 10,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateName: 'AI文案生成',
templateThumbnail: 'https://picsum.photos/60/60?random=1',
templateDescription: '基于关键词生成营销文案',
};
}
};
setResult(getResultByType());
} catch (error) {
Alert.alert('错误', '无法加载结果详情');
}
};
const handleRerun = () => {
router.push(`/templates/${result?.templateId}/form`);
};
const handleSaveToGallery = async () => {
if (!result || result.type === 'TEXT') return;
try {
setSaving(true);
const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== 'granted') {
Alert.alert('权限请求', '需要媒体库权限才能保存文件');
return;
}
Alert.alert('保存成功', `${result.type === 'IMAGE' ? '图片' : '视频'}已保存到相册`);
} catch (error) {
Alert.alert('保存失败', '无法保存文件,请检查网络连接');
} finally {
setSaving(false);
}
};
const handleCopyText = async () => {
if (!result || result.type !== 'TEXT') return;
try {
await navigator.clipboard?.writeText?.(result.resultUrl[0]);
Alert.alert('复制成功', '文本已复制到剪贴板');
} catch (error) {
Alert.alert('复制失败', '无法复制文本');
}
};
const openFullscreen = (type: 'image' | 'video', url: string) => {
setFullscreenContent({ type, url });
setFullscreenVisible(true);
};
const handleDownloadPress = () => {
if (!result) return;
if (result.type === 'TEXT') {
handleCopyText();
return;
}
handleSaveToGallery();
};
const renderVisualMedia = (url: string, style: StyleProp<ImageStyle> | StyleProp<ViewStyle>) => {
if (!url) return null;
if (result?.type === 'VIDEO') {
return (
<Video
source={{ uri: url }}
style={style}
resizeMode={ResizeMode.COVER}
shouldPlay={false}
isMuted
/>
);
}
return (
<Image
source={{ uri: url }}
style={style}
resizeMode="cover"
/>
);
};
if (!result) {
return (
<ThemedView style={styles.screen}>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
</ThemedView>
);
}
const isTextResult = result.type === 'TEXT';
const primaryPreviewUrl = result.resultUrl[0] ?? '';
return (
<ThemedView style={styles.screen}>
<View style={[styles.safeGuard, { paddingTop: insets.top || 16 }]} />
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.headerRow}>
<TouchableOpacity
onPress={() => router.back()}
activeOpacity={0.7}
style={styles.backButton}
>
<ArrowLeft size={22} color="#F6F6F8" />
</TouchableOpacity>
</View>
<Text style={styles.title}>
INSEAR YOUR PRODUCT INTO AN ASMR VIDEO
</Text>
<Text style={styles.subtitle}>
ASMR-Add-On keeps your original image the same while seamlessly adding our uploaded product into the ASMR scene.
</Text>
<View style={styles.heroCard}>
{isTextResult ? (
<ScrollView style={styles.textScroll} showsVerticalScrollIndicator={false}>
<Text style={styles.generatedText}>{primaryPreviewUrl}</Text>
</ScrollView>
) : (
<>
{renderVisualMedia(primaryPreviewUrl, styles.heroMedia)}
{result.templateThumbnail && (
<View style={styles.referencePreview}>
<Image
source={{ uri: result.templateThumbnail }}
style={styles.referenceImage}
/>
</View>
)}
</>
)}
</View>
{!isTextResult && (
<TouchableOpacity
activeOpacity={0.85}
style={styles.detailCard}
onPress={() => openFullscreen(result.type === 'VIDEO' ? 'video' : 'image', primaryPreviewUrl)}
>
<View style={styles.expandIcon}>
<Maximize2 size={18} color="#F5F5F7" />
</View>
<View style={styles.detailMediaWrapper}>
{renderVisualMedia(primaryPreviewUrl, styles.detailMedia)}
</View>
</TouchableOpacity>
)}
</ScrollView>
<Modal
visible={fullscreenVisible}
transparent={false}
animationType="fade"
onRequestClose={() => setFullscreenVisible(false)}
>
<View style={styles.fullscreenContainer}>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setFullscreenVisible(false)}
>
<X size={30} color="#FFFFFF" />
</TouchableOpacity>
{fullscreenContent && (
<View style={styles.fullscreenMedia}>
{fullscreenContent.type === 'image' ? (
<Image
source={{ uri: fullscreenContent.url }}
style={styles.fullscreenMediaContent}
resizeMode="contain"
/>
) : (
<Video
source={{ uri: fullscreenContent.url }}
style={styles.fullscreenMediaContent}
shouldPlay
useNativeControls
resizeMode={ResizeMode.CONTAIN}
/>
)}
</View>
)}
</View>
</Modal>
<View
style={[
styles.bottomBar,
{
paddingBottom: Math.max(insets.bottom, 20),
},
]}
>
<TouchableOpacity
activeOpacity={0.8}
style={styles.secondaryAction}
onPress={handleRerun}
>
<Plus size={20} color="#F6F6F8" />
<Text style={styles.secondaryText}>Regenerate</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.primaryAction,
saving && { opacity: 0.7 },
]}
onPress={handleDownloadPress}
disabled={saving}
>
{isTextResult ? (
<Copy size={20} color="#1C1C1E" />
) : (
<Download size={20} color="#1C1C1E" />
)}
<Text style={styles.primaryText}>
{isTextResult ? 'Copy Text' : 'Download'}
</Text>
</TouchableOpacity>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#09090B',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#09090B',
},
loadingText: {
color: '#F5F5F7',
fontSize: 16,
},
safeGuard: {
backgroundColor: 'transparent',
},
scrollContent: {
paddingHorizontal: 24,
paddingBottom: 160,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
marginBottom: 24,
},
backButton: {
width: 44,
height: 44,
borderRadius: 22,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.12)',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(32, 32, 36, 0.6)',
},
title: {
color: '#F5F5F7',
fontSize: 22,
fontWeight: '700',
lineHeight: 30,
marginBottom: 12,
letterSpacing: 0.4,
},
subtitle: {
color: '#9A9AA2',
fontSize: 14,
lineHeight: 20,
},
heroCard: {
marginTop: 32,
borderRadius: 32,
backgroundColor: '#131317',
padding: 24,
position: 'relative',
overflow: 'hidden',
},
heroMedia: {
width: '100%',
height: HERO_HEIGHT,
borderRadius: 28,
},
textScroll: {
maxHeight: HERO_HEIGHT,
},
generatedText: {
color: '#F5F5F7',
fontSize: 16,
lineHeight: 24,
},
referencePreview: {
position: 'absolute',
bottom: 24,
left: 24,
width: 74,
height: 74,
borderRadius: 20,
padding: 4,
backgroundColor: '#050506',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
referenceImage: {
width: '100%',
height: '100%',
borderRadius: 16,
},
detailCard: {
marginTop: 28,
borderRadius: 28,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
backgroundColor: '#101012',
padding: 20,
position: 'relative',
},
detailMediaWrapper: {
borderRadius: 24,
overflow: 'hidden',
},
detailMedia: {
width: '100%',
height: DETAIL_HEIGHT,
},
expandIcon: {
position: 'absolute',
right: 20,
top: 20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(24, 24, 28, 0.86)',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
},
fullscreenContainer: {
flex: 1,
backgroundColor: '#000000',
justifyContent: 'center',
alignItems: 'center',
},
closeButton: {
position: 'absolute',
top: 50,
right: 20,
zIndex: 1,
width: 50,
height: 50,
justifyContent: 'center',
alignItems: 'center',
},
fullscreenMedia: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
fullscreenMediaContent: {
width: '100%',
height: '100%',
},
bottomBar: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 24,
paddingTop: 16,
backgroundColor: 'rgba(6, 6, 7, 0.94)',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
secondaryAction: {
flex: 1,
height: 60,
borderRadius: 30,
backgroundColor: '#151519',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
},
secondaryText: {
color: '#F5F5F7',
fontSize: 16,
fontWeight: '600',
marginLeft: 8,
},
primaryAction: {
flex: 1,
height: 60,
borderRadius: 30,
backgroundColor: '#D9FF3F',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
},
primaryText: {
color: '#1C1C1E',
fontSize: 16,
fontWeight: '700',
marginLeft: 10,
},
});