feat: 对接 generateVideo 页面后端接口
- 新增 uploadFile 工具函数用于图片上传 - 更新 useTemplateActions hook 使用 handleError 统一错误处理 - 实现 generateVideo 页面视频生成功能 - 根据 formSchema.startNodes 动态构建请求数据 - 支持图片和文本输入 - 添加 loading 状态和错误提示 - 生成成功后显示通知并返回 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4d1e901032
commit
755a374b67
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -21,35 +21,44 @@ import { LinearGradient } from 'expo-linear-gradient'
|
|||
import { LeftArrowIcon, UploadIcon, WhitePointsIcon } from '@/components/icon'
|
||||
import UploadReferenceImageDrawer from '@/components/drawer/UploadReferenceImageDrawer'
|
||||
import { StartGeneratingNotification } from '@/components/ui'
|
||||
import { useTemplateActions } from '@/hooks/use-template-actions'
|
||||
import { uploadFile } from '@/lib/uploadFile'
|
||||
import Toast from '@/components/ui/Toast'
|
||||
|
||||
const { height: screenHeight } = Dimensions.get('window')
|
||||
|
||||
interface TemplateData {
|
||||
id: number
|
||||
id: string
|
||||
videoUrl: any
|
||||
thumbnailUrl: any
|
||||
title: string
|
||||
duration: string
|
||||
price?: number
|
||||
formSchema?: {
|
||||
startNodes?: Array<{ id: string; type: string }>
|
||||
}
|
||||
}
|
||||
|
||||
export default function GenerateVideoScreen() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const params = useLocalSearchParams()
|
||||
const { runTemplate, loading } = useTemplateActions()
|
||||
|
||||
const [description, setDescription] = useState('')
|
||||
const [uploadedImage, setUploadedImage] = useState<any>(null)
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState('')
|
||||
const [previewImageUri, setPreviewImageUri] = useState('')
|
||||
const [templateData, setTemplateData] = useState<TemplateData | null>(null)
|
||||
const [drawerVisible, setDrawerVisible] = useState(false)
|
||||
const [showNotification, setShowNotification] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (params.template && typeof params.template === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(params.template) as TemplateData
|
||||
setTemplateData(parsed)
|
||||
// 使用缩略图作为上传的图片
|
||||
if (parsed.thumbnailUrl) {
|
||||
setUploadedImage(parsed.thumbnailUrl)
|
||||
setPreviewImageUri(parsed.thumbnailUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse template data:', error)
|
||||
|
|
@ -57,6 +66,61 @@ export default function GenerateVideoScreen() {
|
|||
}
|
||||
}, [params.template])
|
||||
|
||||
const startNodes = useMemo(() => templateData?.formSchema?.startNodes || [], [templateData])
|
||||
const hasImageNode = useMemo(() => startNodes.some((node) => node.type === 'image'), [startNodes])
|
||||
|
||||
const handleSelectImage = useCallback(async (imageUri: string, mimeType?: string, fileName?: string) => {
|
||||
try {
|
||||
setPreviewImageUri(imageUri)
|
||||
const url = await uploadFile({ uri: imageUri, mimeType, fileName })
|
||||
setUploadedImageUrl(url)
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
} finally {
|
||||
setDrawerVisible(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (!templateData) return
|
||||
|
||||
if (hasImageNode && !uploadedImageUrl) {
|
||||
Toast.show({ title: t('generateVideo.pleaseUploadImage') || '请上传参考图片' })
|
||||
return
|
||||
}
|
||||
|
||||
Toast.showLoading()
|
||||
try {
|
||||
const data: Record<string, string> = {}
|
||||
startNodes.forEach((node) => {
|
||||
data[node.id] = node.type === 'text' ? description : uploadedImageUrl
|
||||
})
|
||||
|
||||
const { generationId, error } = await runTemplate({
|
||||
templateId: templateData.id,
|
||||
data,
|
||||
})
|
||||
|
||||
Toast.hideLoading()
|
||||
|
||||
if (error) {
|
||||
Toast.show({ title: error.message || t('generateVideo.generateFailed') || '生成失败' })
|
||||
return
|
||||
}
|
||||
|
||||
if (generationId) {
|
||||
setShowNotification(true)
|
||||
setTimeout(() => {
|
||||
setShowNotification(false)
|
||||
router.back()
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.hideLoading()
|
||||
Toast.show({ title: t('generateVideo.generateFailed') || '生成失败' })
|
||||
}
|
||||
}, [templateData, uploadedImageUrl, description, runTemplate, router, t, hasImageNode, startNodes])
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={styles.container}
|
||||
|
|
@ -100,9 +164,9 @@ export default function GenerateVideoScreen() {
|
|||
</View>
|
||||
|
||||
<View style={styles.uploadContainer}>
|
||||
{uploadedImage ? (
|
||||
{previewImageUri ? (
|
||||
<Image
|
||||
source={uploadedImage}
|
||||
source={previewImageUri}
|
||||
style={styles.uploadedImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
|
|
@ -143,25 +207,20 @@ export default function GenerateVideoScreen() {
|
|||
{/* 底部生成按钮 */}
|
||||
<View style={styles.generateButtonContainer}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowNotification(true)
|
||||
// 3秒后自动隐藏通知
|
||||
setTimeout(() => {
|
||||
setShowNotification(false)
|
||||
}, 3000)
|
||||
}}
|
||||
onPress={handleGenerate}
|
||||
disabled={loading}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={['#9966FF', '#FF6699', '#FF9966']}
|
||||
locations={[0.0015, 0.4985, 0.9956]}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0, y: 0 }}
|
||||
style={styles.generateButton}
|
||||
style={[styles.generateButton, loading && styles.generateButtonDisabled]}
|
||||
>
|
||||
<Text style={styles.generateButtonText}>{t('generateVideo.generate')}</Text>
|
||||
<Text style={styles.generateButtonText}>{loading ? (t('generateVideo.generating') || '生成中...') : (t('generateVideo.generate') || '生成')}</Text>
|
||||
<View style={styles.pointsBadge}>
|
||||
<WhitePointsIcon />
|
||||
<Text style={styles.pointsText}>10</Text>
|
||||
<Text style={styles.pointsText}>{templateData?.price || 10}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
|
|
@ -172,9 +231,7 @@ export default function GenerateVideoScreen() {
|
|||
<UploadReferenceImageDrawer
|
||||
visible={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
onSelectImage={(imageUri) => {
|
||||
setUploadedImage(imageUri)
|
||||
}}
|
||||
onSelectImage={handleSelectImage}
|
||||
/>
|
||||
{/* 通知组件 */}
|
||||
{showNotification && (
|
||||
|
|
@ -373,5 +430,8 @@ const styles = StyleSheet.create({
|
|||
notification: {
|
||||
width: '100%',
|
||||
},
|
||||
generateButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
import { OWNER_ID } from "@/lib/auth"
|
||||
import { ApiError } from "@/lib/types"
|
||||
import { root } from '@repo/core'
|
||||
import { TemplateController, type RunTemplateInput } from "@repo/sdk"
|
||||
import { useState } from "react"
|
||||
import { type RunTemplateInput, TemplateController } from '@repo/sdk'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
import { handleError } from './use-error'
|
||||
|
||||
export const useTemplateActions = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
|
||||
const runTemplate = async (params: RunTemplateInput): Promise<{ generationId?: string; error?: ApiError }> => {
|
||||
const runTemplate = useCallback(async (params: RunTemplateInput): Promise<{ generationId?: string; error?: ApiError }> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const template = root.get(TemplateController)
|
||||
const result = await template.run({
|
||||
templateId: params.templateId,
|
||||
data: params.data || {},
|
||||
})
|
||||
setLoading(false)
|
||||
return { generationId: result.generationId }
|
||||
} catch (e) {
|
||||
setError(e as ApiError)
|
||||
setLoading(false)
|
||||
return { error: e as ApiError }
|
||||
const template = root.get(TemplateController)
|
||||
const { data, error } = await handleError(async () => await template.run({
|
||||
templateId: params.templateId,
|
||||
data: params.data || {},
|
||||
}))
|
||||
|
||||
setLoading(false)
|
||||
|
||||
if (error) {
|
||||
setError(error)
|
||||
return { error }
|
||||
}
|
||||
}
|
||||
|
||||
return { generationId: data?.generationId, error: null }
|
||||
}, [])
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { root } from '@repo/core'
|
||||
import { FileController } from '@repo/sdk'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
import { handleError } from '@/hooks/use-error'
|
||||
|
||||
export async function uploadFile(params: { uri: string; mimeType?: string; fileName?: string }): Promise<string> {
|
||||
const { uri, mimeType, fileName } = params
|
||||
const file = {
|
||||
name: fileName || 'uploaded_file.jpg',
|
||||
type: mimeType || 'image/jpeg',
|
||||
uri: Platform.OS === 'android' ? uri : uri.replace('file://', ''),
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', file as any)
|
||||
|
||||
const fileController = root.get(FileController)
|
||||
const { data, error } = await handleError(async () => await fileController.uploadS3(formData))
|
||||
|
||||
if (error || !data?.data) {
|
||||
throw error || new Error('上传失败')
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
Loading…
Reference in New Issue