expo-duooomi-app/app/generationRecord.tsx

417 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
Dimensions,
Pressable,
StatusBar as RNStatusBar,
ActivityIndicator,
Alert,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { LeftArrowIcon, DeleteIcon, EditIcon, ChangeIcon, WhiteStarIcon } from '@/components/icon'
import { useTemplateGenerationDetail } from '@/hooks/data/use-template-generation-detail'
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
import { useAigcTask } from '@/hooks/actions/use-aigc-task'
import { useUserBalance } from '@/hooks/core/use-user-balance'
import { useAuth } from '@/hooks/core/use-auth'
const { width: screenWidth } = Dimensions.get('window')
const CREDITS_COST = 10
export default function GenerationRecordScreen() {
const router = useRouter()
const params = useLocalSearchParams()
const { user } = useAuth()
const { balance, load: loadBalance } = useUserBalance()
const generationId = params.id as string
const { data: generation, loading, load, refetch } = useTemplateGenerationDetail()
const { deleteGeneration, runTemplate, createGeneration } = useTemplateActions()
const { submitTask, startPolling, stopPolling } = useAigcTask()
const [isRegenerating, setIsRegenerating] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
useEffect(() => {
if (generationId) {
load({ id: generationId })
}
}, [generationId])
useEffect(() => {
if (user?.id) {
loadBalance(user.id)
}
}, [user?.id])
useEffect(() => {
return () => {
stopPolling()
}
}, [])
const handleRegenerate = async () => {
if (!generation) return
if (balance < CREDITS_COST) {
Alert.alert('积分不足', `生成视频需要 ${CREDITS_COST} 积分,当前积分:${balance}`)
return
}
setIsRegenerating(true)
const { taskId, error: submitError } = await submitTask({
model_name: 'video_generation',
prompt: '再次生成',
img_url: generation.originalUrl || '',
duration: '3',
resolution: '720p',
watermark: false,
webhook_flag: false,
})
if (submitError) {
Alert.alert('生成失败', submitError.message || '提交任务失败')
setIsRegenerating(false)
return
}
if (!taskId) {
Alert.alert('生成失败', '任务ID获取失败')
setIsRegenerating(false)
return
}
startPolling(
taskId,
async (result) => {
const videoUrls = result.data || []
const { generation: newGeneration, error: createError } = await createGeneration({
templateId: generation.templateId,
type: 'VIDEO',
resultUrl: videoUrls,
originalUrl: generation.originalUrl || '',
status: 'completed',
creditsCost: CREDITS_COST,
})
if (createError) {
Alert.alert('保存失败', createError.message || '生成记录保存失败')
} else {
Alert.alert('生成成功', '视频已生成完成')
if (user?.id) {
loadBalance(user.id)
}
if (newGeneration?.id) {
router.replace({
pathname: '/generationRecord' as any,
params: { id: newGeneration.id },
})
}
}
setIsRegenerating(false)
},
(error) => {
Alert.alert('生成失败', error.message || '视频生成失败')
setIsRegenerating(false)
}
)
}
const handleEdit = () => {
if (!generation) return
router.push({
pathname: '/generateVideo' as any,
params: {
template: JSON.stringify({
id: generation.templateId,
title: 'AI 视频',
thumbnailUrl: generation.originalUrl,
duration: '3',
}),
},
})
}
const handleDelete = async () => {
if (!generationId) return
Alert.alert('确认删除', '确定要删除这条生成记录吗?', [
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: async () => {
setIsDeleting(true)
const { success, error } = await deleteGeneration(generationId)
if (error) {
Alert.alert('删除失败', error.message || '删除失败,请重试')
setIsDeleting(false)
} else {
Alert.alert('删除成功', '生成记录已删除', [
{
text: '确定',
onPress: () => router.back(),
},
])
}
},
},
])
}
const resultUrl = generation?.resultUrl?.[0] || generation?.originalUrl
const isLoading = loading || isRegenerating || isDeleting
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 顶部导航栏 */}
<View style={styles.header}>
<Pressable
style={styles.backButton}
onPress={() => router.back()}
>
<LeftArrowIcon />
</Pressable>
<Text style={styles.headerTitle}></Text>
<View style={styles.headerSpacer} />
</View>
{/* AI 视频标签 */}
<View style={styles.categorySection}>
<View style={styles.categoryIcon}>
<WhiteStarIcon />
</View>
<Text style={styles.categoryText}>AI </Text>
</View>
{/* 原图标签 */}
<View style={styles.originalImageSection}>
<Text style={styles.originalImageLabel}></Text>
<View style={styles.originalImageDivider} />
</View>
{/* 主图片/视频预览区域 */}
<View style={styles.imageContainer}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#9966FF" />
</View>
) : resultUrl ? (
<Image
source={{ uri: resultUrl }}
style={styles.mainImage}
contentFit="cover"
/>
) : (
<View style={styles.placeholderContainer}>
<Text style={styles.placeholderText}></Text>
</View>
)}
</View>
<Text style={styles.durationText}>
{generation?.createdAt ? new Date(generation.createdAt).toLocaleString('zh-CN') : ''}
</Text>
{/* 底部操作按钮 */}
<View style={styles.actionButtons}>
<Pressable
style={[styles.actionButton, isLoading && styles.buttonDisabled]}
onPress={handleEdit}
disabled={isLoading || !generation}
>
<EditIcon />
<Text style={styles.actionButtonText}></Text>
</Pressable>
<Pressable
style={[styles.actionButton, isLoading && styles.buttonDisabled]}
onPress={handleRegenerate}
disabled={isLoading || !generation}
>
{isRegenerating ? (
<ActivityIndicator size="small" color="#F5F5F5" />
) : (
<>
<ChangeIcon />
<Text style={styles.actionButtonText}></Text>
</>
)}
</Pressable>
<Pressable
style={[styles.deleteButton, isLoading && styles.buttonDisabled]}
onPress={handleDelete}
disabled={isLoading || !generation}
>
{isDeleting ? (
<ActivityIndicator size="small" color="#F5F5F5" />
) : (
<DeleteIcon />
)}
</Pressable>
</View>
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollView: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
paddingBottom: 100,
backgroundColor: '#090A0B',
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 16,
paddingBottom: 20,
},
backButton: {
width: 22,
height: 22,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '600',
flex: 1,
textAlign: 'center',
},
headerSpacer: {
width: 22,
},
categorySection: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
gap:4,
},
categoryIcon: {
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
},
categoryText: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '600',
},
originalImageSection: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
},
originalImageLabel: {
color: '#8A8A8A',
fontSize: 13,
marginRight: 6,
},
originalImageDivider: {
width: 1,
height: 12,
backgroundColor: '#FFFFFF33',
},
imageContainer: {
width: screenWidth - 24,
height: (screenWidth - 24) * 1.32, // 根据设计稿比例计算
marginHorizontal: 12,
marginBottom: 14,
borderRadius: 16,
overflow: 'hidden',
backgroundColor: '#1C1E22',
position: 'relative',
},
mainImage: {
width: '100%',
height: '100%',
},
durationText: {
paddingLeft: 28,
color: '#F5F5F5',
fontSize: 13,
marginBottom: 22,
fontWeight: '500',
},
actionButtons: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
gap: 8,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 8,
paddingVertical: 6,
borderRadius: 8,
backgroundColor: '#1C1E22',
height: 32,
},
actionButtonText: {
color: '#F5F5F5',
fontSize: 11,
fontWeight: '500',
},
deleteButton: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#1C1E22',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 'auto',
},
buttonDisabled: {
opacity: 0.5,
},
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
placeholderContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
placeholderText: {
color: '#8A8A8A',
fontSize: 14,
},
})