expo-popcore-app/app/templateDetail.tsx

341 lines
12 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 React, { useEffect, useState, useCallback, useRef } from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
StatusBar as RNStatusBar,
ActivityIndicator,
ScrollView,
Platform,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useTranslation } from 'react-i18next'
import { LeftArrowIcon } from '@/components/icon'
import SearchResultsGrid from '@/components/SearchResultsGrid'
import { DynamicForm, type FormSchema, type DynamicFormRef } from '@/components/DynamicForm'
import { useTemplateActions, useTemplateDetail, useTemplateGenerations, type TemplateGeneration } from '@/hooks'
import Toast from '@/components/ui/Toast'
import UploadReferenceImageDrawer from '@/components/drawer/UploadReferenceImageDrawer'
import { uploadFile } from '@/lib/uploadFile'
const CARD_HEIGHTS = [214, 236, 200, 220, 210, 225]
export default function TemplateDetailScreen() {
const { t } = useTranslation()
const router = useRouter()
const params = useLocalSearchParams()
const templateId = typeof params.id === 'string' ? params.id : undefined
const { data: templateDetail, loading: templateLoading, error: templateError, execute: loadTemplateDetail } = useTemplateDetail()
const { generations, loading: generationsLoading, execute: loadGenerations } = useTemplateGenerations()
const { runTemplate } = useTemplateActions()
const [formSchema, setFormSchema] = useState<FormSchema | null>(null)
const [drawerVisible, setDrawerVisible] = useState(false)
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null)
const dynamicFormRef = useRef<DynamicFormRef>(null)
useEffect(() => {
if (templateId) {
loadTemplateDetail({ id: templateId })
loadGenerations({ templateId, page: 1, limit: 20 })
}
}, [templateId, loadTemplateDetail, loadGenerations])
// Set formSchema when templateDetail is loaded
useEffect(() => {
if (templateDetail?.formSchema?.startNodes && templateDetail.formSchema.startNodes.length > 0) {
setFormSchema(templateDetail.formSchema)
}
}, [templateDetail])
const handleStartCreating = useCallback(() => {
// Navigate to generateVideo page if no form schema
if (!templateDetail?.formSchema?.startNodes || templateDetail.formSchema.startNodes.length === 0) {
router.push({
pathname: '/generateVideo',
params: {
template: JSON.stringify(templateDetail),
},
} as any)
}
// If formSchema exists, the form is already visible inline
}, [templateDetail, router])
const handleFormSubmit = useCallback(async (data: Record<string, string>) => {
if (!templateId) return { error: 'Template ID is required' }
const result = await runTemplate({
templateId,
data,
})
if (result.generationId) {
// Show success message and reload generations
Toast.show({
title: t('templateDetail.generationStarted') || '生成已开始',
})
// Reload generations to show the new one
loadGenerations({ templateId, page: 1, limit: 20 })
}
return result
}, [templateId, runTemplate, t, loadGenerations])
const handleOpenDrawer = useCallback((nodeId: string) => {
setCurrentNodeId(nodeId)
setDrawerVisible(true)
}, [])
const handleCloseDrawer = useCallback(() => {
setDrawerVisible(false)
setCurrentNodeId(null)
}, [])
const handleSelectImage = useCallback(async (imageUri: string, mimeType?: string, fileName?: string) => {
if (!currentNodeId) return
try {
const url = await uploadFile({ uri: imageUri, mimeType, fileName })
// 通过 ref 更新 DynamicForm 中的字段值
dynamicFormRef.current?.updateFieldValue(currentNodeId, url, imageUri)
Toast.show({
title: t('templateDetail.uploadSuccess') || '上传成功',
})
} catch (error) {
console.error('Upload failed:', error)
Toast.show({
title: t('templateDetail.uploadFailed') || '上传失败,请重试',
})
} finally {
handleCloseDrawer()
}
}, [currentNodeId, t, handleCloseDrawer])
// 直接使用 TemplateGeneration 数据,只添加必要的 height 字段
const displayData = generations.map((generation, index) => ({
...generation,
height: CARD_HEIGHTS[index % CARD_HEIGHTS.length],
title: generation.template?.title || generation.template?.titleEn || '',
image: generation.resultUrl?.[0] || generation.originalUrl || '',
}))
if (templateLoading && !templateDetail) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#FFE500" />
</View>
</SafeAreaView>
)
}
if (templateError && !templateDetail) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.centerContainer}>
<Text style={styles.errorText}></Text>
<Pressable style={styles.retryButton} onPress={() => router.back()}>
<Text style={styles.retryButtonText}></Text>
</Pressable>
</View>
</SafeAreaView>
)
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
{/* 顶部导航栏 */}
<View style={styles.header}>
<Pressable
onPress={() => router.back()}
style={styles.backButton}
>
<LeftArrowIcon />
</Pressable>
<Text style={styles.headerTitle}>
{templateDetail?.title || templateDetail?.titleEn || t('templateDetail.title')}
</Text>
</View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 标题区域 */}
<View style={styles.titleSection}>
<Text style={styles.mainTitle}>
{templateDetail?.title || templateDetail?.titleEn || t('templateDetail.title')}
</Text>
<Text style={styles.subTitle}>
{t('templateDetail.subtitle')}
</Text>
</View>
{/* Dynamic Form - Show inline if formSchema exists */}
{formSchema && formSchema.startNodes && formSchema.startNodes.length > 0 ? (
<View style={styles.formSection}>
<Text style={styles.sectionTitle}>
{t('templateDetail.fillForm') || '填写表单'}
</Text>
<DynamicForm
ref={dynamicFormRef}
formSchema={formSchema}
onSubmit={handleFormSubmit}
onOpenDrawer={handleOpenDrawer}
/>
</View>
) : (
/* Start Creating Button - Only show if no formSchema */
<Pressable style={styles.startCreatingButton} onPress={handleStartCreating}>
<Text style={styles.startCreatingButtonText}>
{t('templateDetail.startCreating') || '开始创作'}
</Text>
</Pressable>
)}
{/* Generations Section */}
<View style={styles.generationsSection}>
<Text style={styles.sectionTitle}>
{t('templateDetail.generations') || '作品列表'}
</Text>
{/* 加载更多指示器 */}
{generationsLoading && generations.length > 0 && (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#FFE500" />
<Text style={styles.loadingMoreText}>...</Text>
</View>
)}
<SearchResultsGrid results={displayData} />
</View>
</ScrollView>
{/* Upload Drawer - 移到最外层以确保从屏幕底部弹出 */}
<UploadReferenceImageDrawer
visible={drawerVisible}
onClose={handleCloseDrawer}
onSelectImage={handleSelectImage}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
centerContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 12,
paddingHorizontal: 12,
paddingBottom: 24,
},
backButton: {
padding: 4,
},
headerTitle: {
flex: 1,
color: '#F5F5F5',
fontSize: 16,
fontWeight: '600',
marginLeft: 12,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: 20,
},
titleSection: {
paddingHorizontal: 12,
paddingBottom: 24,
},
mainTitle: {
color: '#F5F5F5',
fontSize: 20,
fontWeight: '500',
marginBottom: 4,
},
subTitle: {
color: '#ABABAB',
fontSize: 14,
fontWeight: '400',
},
formSection: {
paddingHorizontal: 12,
paddingBottom: 24,
},
startCreatingButton: {
backgroundColor: '#FF6699',
marginHorizontal: 12,
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
marginBottom: 24,
},
startCreatingButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
generationsSection: {
paddingHorizontal: 12,
},
sectionTitle: {
color: '#F5F5F5',
fontSize: 16,
fontWeight: '600',
marginBottom: 16,
},
errorText: {
color: '#FFFFFF',
fontSize: 14,
textAlign: 'center',
},
retryButton: {
backgroundColor: '#FF6699',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
retryButtonText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
loadingMoreContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
},
loadingMoreText: {
color: '#ABABAB',
fontSize: 12,
},
})