This commit is contained in:
imeepos
2026-01-20 17:21:09 +08:00
parent d2189ed971
commit 8c43b9daf0
6 changed files with 322 additions and 297 deletions

View File

@@ -55,6 +55,14 @@ const ErrorState = () => (
</Layout> </Layout>
) )
const EmptyState = () => (
<Layout>
<View style={styles.centerContainer}>
<Text style={styles.messageText}></Text>
</View>
</Layout>
)
const FooterLoading = () => ( const FooterLoading = () => (
<View style={styles.footerLoading}> <View style={styles.footerLoading}>
<ActivityIndicator size="small" color="#FFE500" /> <ActivityIndicator size="small" color="#FFE500" />
@@ -171,19 +179,37 @@ export default function VideoScreen() {
index, index,
}), [videoHeight]) }), [videoHeight])
// 过滤掉视频类型的 item // 过滤掉没有可用预览的 item
const filteredTemplates = templates.filter((item: TemplateDetail) => { const filteredTemplates = templates.filter((item: TemplateDetail) => {
// 检查所有可能的预览 URL如果任何一个包含视频格式则过滤掉 // 优先使用 WebP 格式(支持动画),回退到普通预览图
const hasVideoUrl = [ const displayUrl = item.webpHighPreviewUrl || item.webpPreviewUrl || item.previewUrl
item.webpHighPreviewUrl,
item.webpPreviewUrl, // 只检查最终要显示的 URL 是否为视频格式
item.previewUrl, const isDisplayVideo = displayUrl && isVideoUrl(displayUrl)
].some(url => url && isVideoUrl(url))
return !hasVideoUrl // 有显示URL且不是视频格式才保留
const shouldKeep = !!displayUrl && !isDisplayVideo
console.log(`Filtering ${item.title}:`, {
displayUrl,
isDisplayVideo,
shouldKeep,
})
return shouldKeep
})
console.log('Video page state:', {
templatesLength: templates.length,
filteredTemplatesLength: filteredTemplates.length,
loading,
error,
hasMore,
}) })
if (loading && templates.length === 0) return <LoadingState /> if (loading && templates.length === 0) return <LoadingState />
if (error && templates.length === 0) return <ErrorState /> if (error && templates.length === 0) return <ErrorState />
if (!loading && filteredTemplates.length === 0) return <EmptyState />
return ( return (
<Layout> <Layout>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react' import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { import {
View, View,
Text, Text,
@@ -6,7 +6,6 @@ import {
ScrollView, ScrollView,
Dimensions, Dimensions,
Pressable, Pressable,
TextInput,
StatusBar as RNStatusBar, StatusBar as RNStatusBar,
Platform, Platform,
KeyboardAvoidingView, KeyboardAvoidingView,
@@ -16,14 +15,13 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image' import { Image } from 'expo-image'
import { useRouter, useLocalSearchParams } from 'expo-router' import { useRouter, useLocalSearchParams } from 'expo-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { LinearGradient } from 'expo-linear-gradient'
import { LeftArrowIcon, UploadIcon, WhitePointsIcon } from '@/components/icon' import { LeftArrowIcon, WhitePointsIcon } from '@/components/icon'
import UploadReferenceImageDrawer from '@/components/drawer/UploadReferenceImageDrawer' import UploadReferenceImageDrawer from '@/components/drawer/UploadReferenceImageDrawer'
import { StartGeneratingNotification } from '@/components/ui' import { StartGeneratingNotification } from '@/components/ui'
import { DynamicForm, type DynamicFormRef, type FormSchema } from '@/components/DynamicForm'
import { useTemplateActions } from '@/hooks/use-template-actions' import { useTemplateActions } from '@/hooks/use-template-actions'
import { useTemplateDetail } from '@/hooks/use-template-detail' import { useTemplateDetail } from '@/hooks/use-template-detail'
import { uploadFile } from '@/lib/uploadFile'
import Toast from '@/components/ui/Toast' import Toast from '@/components/ui/Toast'
const { height: screenHeight } = Dimensions.get('window') const { height: screenHeight } = Dimensions.get('window')
@@ -35,11 +33,10 @@ export default function GenerateVideoScreen() {
const { runTemplate, loading } = useTemplateActions() const { runTemplate, loading } = useTemplateActions()
const { data: templateDetail, loading: templateLoading, execute: fetchTemplate } = useTemplateDetail() const { data: templateDetail, loading: templateLoading, execute: fetchTemplate } = useTemplateDetail()
const [description, setDescription] = useState('')
const [uploadedImageUrl, setUploadedImageUrl] = useState('')
const [previewImageUri, setPreviewImageUri] = useState('')
const [drawerVisible, setDrawerVisible] = useState(false) const [drawerVisible, setDrawerVisible] = useState(false)
const [showNotification, setShowNotification] = useState(false) const [showNotification, setShowNotification] = useState(false)
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null)
const dynamicFormRef = useRef<DynamicFormRef>(null)
useEffect(() => { useEffect(() => {
if (params.templateId && typeof params.templateId === 'string') { if (params.templateId && typeof params.templateId === 'string') {
@@ -47,68 +44,43 @@ export default function GenerateVideoScreen() {
} }
}, [params.templateId, fetchTemplate]) }, [params.templateId, fetchTemplate])
useEffect(() => { const formSchema = useMemo<FormSchema>(() => ({
if (templateDetail) { startNodes: templateDetail?.formSchema?.startNodes || []
if (templateDetail.thumbnailUrl) { }), [templateDetail])
setPreviewImageUri(templateDetail.thumbnailUrl)
}
}
}, [templateDetail])
const startNodes = useMemo(() => templateDetail?.formSchema?.startNodes || [], [templateDetail]) const handleOpenDrawer = useCallback((nodeId: string) => {
const hasImageNode = useMemo(() => startNodes.some((node) => node.type === 'image'), [startNodes]) setCurrentNodeId(nodeId)
setDrawerVisible(true)
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 () => { const handleSelectImage = useCallback(async (imageUri: string, mimeType?: string, fileName?: string) => {
if (!templateDetail) return if (!currentNodeId) return
if (hasImageNode && !uploadedImageUrl) { if (dynamicFormRef.current) {
Toast.show({ title: t('generateVideo.pleaseUploadImage') || '请上传参考图片' }) dynamicFormRef.current.updateFieldValue(currentNodeId, imageUri, imageUri)
return
} }
setDrawerVisible(false)
setCurrentNodeId(null)
}, [currentNodeId])
Toast.showLoading() const handleFormSubmit = useCallback(async (data: Record<string, string>) => {
try { if (!templateDetail) return { error: { message: 'Template not found' } }
const data: Record<string, string> = {}
startNodes.forEach((node) => {
data[node.id] = node.type === 'text' ? description : uploadedImageUrl
})
const { generationId, error } = await runTemplate({ const result = await runTemplate({
templateId: templateDetail.id, templateId: templateDetail.id,
data, data,
}) })
Toast.hideLoading() if (result.generationId) {
if (error) {
Toast.show({ title: error.message || t('generateVideo.generateFailed') || '生成失败' })
return
}
if (generationId) {
setShowNotification(true) setShowNotification(true)
setTimeout(() => { setTimeout(() => {
setShowNotification(false) setShowNotification(false)
router.back() router.back()
}, 3000) }, 3000)
} }
} catch (error) {
Toast.hideLoading() return result
Toast.show({ title: t('generateVideo.generateFailed') || '生成失败' }) }, [templateDetail, runTemplate, router])
}
}, [templateDetail, uploadedImageUrl, description, runTemplate, router, t, hasImageNode, startNodes])
return ( return (
<SafeAreaView <SafeAreaView
@@ -143,85 +115,66 @@ export default function GenerateVideoScreen() {
<LeftArrowIcon /> <LeftArrowIcon />
</Pressable> </Pressable>
</View> </View>
{/* 主要内容区域 */}
<View style={styles.content}> <View style={styles.content}>
{/* 标题区域 */} {/* 模板预览卡片 */}
<View style={styles.titleSection}> <View style={styles.templatePreviewCard}>
<Text style={styles.title}> {templateDetail?.title}</Text> <View style={styles.templateThumbnailContainer}>
<Text style={styles.subtitle}> <Image
{templateDetail?.title } source={templateDetail?.coverImageUrl || ''}
</Text> style={styles.templateThumbnail}
contentFit="cover"
/>
<View style={styles.templateInfo}>
<Text style={styles.templateTitle}>{templateDetail?.title}</Text>
<Text style={styles.templateDescription}>{templateDetail?.description}</Text>
</View>
</View>
{/* 价格标签 */}
<View style={styles.priceTag}>
<WhitePointsIcon />
<Text style={styles.priceText}>{templateDetail?.price || 10}</Text>
</View>
</View> </View>
<View style={styles.uploadContainer}> {/* 动态表单 */}
{previewImageUri ? ( <View style={styles.formContainer}>
<Image {templateLoading ? (
source={previewImageUri} <View style={styles.loadingContainer}>
style={styles.uploadedImage} <Text style={styles.loadingText}>{t('generateVideo.loading') || '加载中...'}</Text>
contentFit="cover" </View>
) : formSchema.startNodes && formSchema.startNodes.length > 0 ? (
<DynamicForm
ref={dynamicFormRef}
formSchema={formSchema}
onSubmit={handleFormSubmit}
loading={loading}
onOpenDrawer={handleOpenDrawer}
points={templateDetail?.price}
/> />
) : ( ) : (
<View style={styles.avatarPlaceholder}> <View style={styles.emptyContainer}>
<View style={styles.avatarCircle} /> <Text style={styles.emptyText}>
{t('generateVideo.noFormFields') || '暂无可填写的表单项'}
</Text>
</View> </View>
)} )}
<View style={styles.thumbnailContainer}>
<Image
source={templateDetail?.thumbnailUrl}
style={styles.thumbnail}
contentFit="cover"
/>
</View>
</View>
{/* 上传区域 */}
<Pressable
style={styles.uploadReferenceButton}
onPress={() => {
setDrawerVisible(true)
}}
>
<UploadIcon />
<Text style={styles.uploadReferenceText}>{t('generateVideo.uploadReference')}</Text>
</Pressable>
{/* 描述输入区域 */}
<TextInput
style={styles.descriptionInput}
value={description}
onChangeText={setDescription}
placeholder={t('generateVideo.descriptionPlaceholder')}
placeholderTextColor="#8A8A8A"
multiline
numberOfLines={4}
textAlignVertical="top"
/>
{/* 底部生成按钮 */}
<View style={styles.generateButtonContainer}>
<Pressable
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, loading && styles.generateButtonDisabled]}
>
<Text style={styles.generateButtonText}>{loading ? (t('generateVideo.generating') || '生成中...') : (t('generateVideo.generate') || '生成')}</Text>
<View style={styles.pointsBadge}>
<WhitePointsIcon />
<Text style={styles.pointsText}>{templateDetail?.price || 10}</Text>
</View>
</LinearGradient>
</Pressable>
</View> </View>
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
{/* 图片上传抽屉 */}
<UploadReferenceImageDrawer <UploadReferenceImageDrawer
visible={drawerVisible} visible={drawerVisible}
onClose={() => setDrawerVisible(false)} onClose={() => {
setDrawerVisible(false)
setCurrentNodeId(null)
}}
onSelectImage={handleSelectImage} onSelectImage={handleSelectImage}
/> />
{/* 通知组件 */} {/* 通知组件 */}
{showNotification && ( {showNotification && (
<View style={styles.notificationContainer}> <View style={styles.notificationContainer}>
@@ -255,28 +208,6 @@ const styles = StyleSheet.create({
flexGrow: 1, flexGrow: 1,
backgroundColor: '#090A0B', backgroundColor: '#090A0B',
}, },
generateButtonContainer: {
marginTop: 'auto',
paddingTop: 12,
paddingBottom: Platform.select({
ios: 10,
android: 10,
default: 10,
}),
},
content: {
paddingHorizontal: 12,
paddingTop: 16,
backgroundColor: '#1C1E20',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
gap: 8,
minHeight: Platform.select({
ios: screenHeight - 100,
android: screenHeight - 100,
default: screenHeight - 60,
}),
},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -294,116 +225,94 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
titleSection: { content: {
paddingHorizontal:4, paddingHorizontal: 12,
marginBottom: 11, paddingTop: 16,
backgroundColor: '#1C1E20',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
gap: 16,
minHeight: Platform.select({
ios: screenHeight - 100,
android: screenHeight - 100,
default: screenHeight - 60,
}),
}, },
title: { // 模板预览卡片样式
templatePreviewCard: {
backgroundColor: '#262A31',
borderRadius: 16,
padding: 12,
borderWidth: 1,
borderColor: '#2F3134',
},
templateThumbnailContainer: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
},
templateThumbnail: {
width: 80,
height: 80,
borderRadius: 12,
backgroundColor: '#1C1E20',
},
templateInfo: {
flex: 1,
gap: 4,
},
templateTitle: {
color: '#F5F5F5', color: '#F5F5F5',
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
marginBottom: 5, lineHeight: 22,
marginLeft: -4,
}, },
subtitle: { templateDescription: {
color: '#CCCCCC', color: '#ABABAB',
fontSize: 12, fontSize: 13,
lineHeight: 18,
}, },
priceTag: {
uploadContainer: { flexDirection: 'row',
height: 140,
borderRadius: 12,
backgroundColor: '#262A31',
overflow: 'hidden',
position: 'relative',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'flex-end',
},
thumbnailContainer: {
position: 'absolute',
left: 8,
bottom: 5,
width: 56,
height: 56,
borderRadius: 8,
overflow: 'hidden',
borderWidth: 2,
borderColor: '#FFFFFF',
backgroundColor: '#090A0B',
},
thumbnail: {
width: '100%',
height: '100%',
},
avatarPlaceholder: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: '#2F3134',
alignItems: 'center',
justifyContent: 'center',
},
avatarCircle: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#4A4C4F',
},
uploadedImage: {
width: '100%',
height: '100%',
},
uploadReferenceButton: {
height: 110,
backgroundColor: '#262A31',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
gap: 6, gap: 6,
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: '#2F3134',
}, },
uploadReferenceText: { priceText: {
color: '#F5F5F5', color: '#FFCF00',
fontSize: 12, fontSize: 16,
fontWeight: '500', fontWeight: '600',
}, },
descriptionInput: { // 表单容器样式
minHeight: 150, formContainer: {
backgroundColor: '#262A31', gap: 12,
borderRadius: 12,
padding: 12,
color: '#F5F5F5',
fontSize: 14,
lineHeight: 20,
...Platform.select({
android: {
textAlignVertical: 'top',
}, },
}), loadingContainer: {
},
generateButton: {
width: '100%',
height: 48,
backgroundColor: 'red',
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, padding: 40,
}, },
generateButtonText: { loadingText: {
color: '#F5F5F5', color: '#8A8A8A',
fontSize: 16,
fontWeight: '500',
},
pointsBadge: {
flexDirection: 'row',
alignItems: 'center',
},
pointsText: {
color: '#f5f5f5',
fontSize: 14, fontSize: 14,
fontWeight: '500',
}, },
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 40,
backgroundColor: '#262A31',
borderRadius: 12,
},
emptyText: {
color: '#8A8A8A',
fontSize: 14,
textAlign: 'center',
},
// 通知容器样式
notificationContainer: { notificationContainer: {
position: 'absolute', position: 'absolute',
top: Platform.select({ top: Platform.select({
@@ -419,8 +328,5 @@ const styles = StyleSheet.create({
notification: { notification: {
width: '100%', width: '100%',
}, },
generateButtonDisabled: {
opacity: 0.6,
},
}) })

View File

@@ -195,6 +195,7 @@ export default function TemplateDetailScreen() {
formSchema={formSchema} formSchema={formSchema}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
onOpenDrawer={handleOpenDrawer} onOpenDrawer={handleOpenDrawer}
points={templateDetail?.price}
/> />
</View> </View>
) : ( ) : (

View File

@@ -31,17 +31,37 @@ export interface StartNode {
text?: string text?: string
label?: string label?: string
description?: string description?: string
usage?: {
unit: string
price: number
price_desc: string
}
output?: { output?: {
texts?: string[] texts?: string[]
images?: Array<{ url: string }> images?: Array<{ url: string }>
selections?: string selections?: string
videos?: Array<{ url: string }>
usage?: {
unit: string
price: number
price_desc: string
}
} }
actionData?: { actionData?: {
options?: SelectOption[] options?: SelectOption[]
required?: boolean required?: boolean
placeholder?: string placeholder?: string
allowMultiple?: boolean allowMultiple?: boolean
prompt?: string
duration?: string
resolution?: string
aspectRatio?: string
selectedModel?: any
advancedParams?: any
} }
autoPlay?: boolean
controls?: boolean
flowType?: string
} }
} }
@@ -58,10 +78,11 @@ interface DynamicFormProps {
onSubmit: (data: Record<string, string>) => Promise<{ generationId?: string; error?: any }> onSubmit: (data: Record<string, string>) => Promise<{ generationId?: string; error?: any }>
loading?: boolean loading?: boolean
onOpenDrawer?: (nodeId: string) => void onOpenDrawer?: (nodeId: string) => void
points?: number
} }
export const DynamicForm = forwardRef<DynamicFormRef, DynamicFormProps>( export const DynamicForm = forwardRef<DynamicFormRef, DynamicFormProps>(
function DynamicForm({ formSchema, onSubmit, loading = false, onOpenDrawer }, ref) { function DynamicForm({ formSchema, onSubmit, loading = false, onOpenDrawer, points }, ref) {
const { t } = useTranslation() const { t } = useTranslation()
const startNodes = formSchema.startNodes || [] const startNodes = formSchema.startNodes || []
@@ -70,10 +91,20 @@ export const DynamicForm = forwardRef<DynamicFormRef, DynamicFormProps>(
const initialData: Record<string, string> = {} const initialData: Record<string, string> = {}
startNodes.forEach((node) => { startNodes.forEach((node) => {
if (node.type === 'text') { if (node.type === 'text') {
initialData[node.id] = node.data?.text || '' // Get default value from data.text or output.texts[0]
const texts = node.data?.output?.texts
initialData[node.id] = texts?.[0] || node.data?.text || ''
} else if (node.type === 'select') { } else if (node.type === 'select') {
// Get default value from output.selections // Get default value from output.selections
initialData[node.id] = node.data?.output?.selections || '' initialData[node.id] = node.data?.output?.selections || ''
} else if (node.type === 'image') {
// Get default value from output.images
const images = node.data?.output?.images
initialData[node.id] = images?.[0]?.url || ''
} else if (node.type === 'video') {
// Get default value from output.videos
const videos = node.data?.output?.videos
initialData[node.id] = videos?.[0]?.url || ''
} else { } else {
initialData[node.id] = '' initialData[node.id] = ''
} }
@@ -83,7 +114,23 @@ export const DynamicForm = forwardRef<DynamicFormRef, DynamicFormProps>(
const [drawerVisible, setDrawerVisible] = useState(false) const [drawerVisible, setDrawerVisible] = useState(false)
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null) const [currentNodeId, setCurrentNodeId] = useState<string | null>(null)
const [previewImages, setPreviewImages] = useState<Record<string, string>>({}) const [previewImages, setPreviewImages] = useState<Record<string, string>>(() => {
const initialPreviews: Record<string, string> = {}
startNodes.forEach((node) => {
if (node.type === 'image') {
const images = node.data?.output?.images
if (images?.[0]?.url) {
initialPreviews[node.id] = images[0].url
}
} else if (node.type === 'video') {
const videos = node.data?.output?.videos
if (videos?.[0]?.url) {
initialPreviews[node.id] = videos[0].url
}
}
})
return initialPreviews
})
const [errors, setErrors] = useState<Record<string, string>>({}) const [errors, setErrors] = useState<Record<string, string>>({})
const updateFormData = useCallback((nodeId: string, value: string) => { const updateFormData = useCallback((nodeId: string, value: string) => {
@@ -410,9 +457,16 @@ export const DynamicForm = forwardRef<DynamicFormRef, DynamicFormProps>(
{loading ? ( {loading ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color="#fff" />
) : ( ) : (
<View style={styles.submitButtonContent}>
<Text style={styles.submitButtonText}> <Text style={styles.submitButtonText}>
{t('dynamicForm.submit') || '提交'} {t('dynamicForm.submit') || '提交'}
</Text> </Text>
{points !== undefined && points > 0 && (
<View style={styles.pointsContainer}>
<Text style={styles.pointsText}>{points}</Text>
</View>
)}
</View>
)} )}
</Button> </Button>
</View> </View>
@@ -499,11 +553,28 @@ const styles = StyleSheet.create({
height: 56, height: 56,
minHeight: 56, minHeight: 56,
}, },
submitButtonContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
submitButtonText: { submitButtonText: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 16, fontSize: 16,
fontWeight: '500', fontWeight: '500',
}, },
pointsContainer: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
},
pointsText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
},
emptyContainer: { emptyContainer: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',

View File

@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TextInput } from "react-native"; import { ActivityIndicator, TextInput } from "react-native";
import { authClient, setAuthToken, useSession } from "../../lib/auth"; import { authClient, setAuthToken, useSession } from "../../lib/auth";
import type { ApiError } from "../../lib/types";
import { Block } from "../ui"; import { Block } from "../ui";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import Text from "../ui/Text"; import Text from "../ui/Text";
@@ -29,15 +30,17 @@ export function AuthForm({ mode = "login", onSuccess, onModeChange }: AuthFormPr
const isLogin = currentMode === "login"; const isLogin = currentMode === "login";
// 根据错误代码获取翻译后的错误信息 // 根据错误代码获取翻译后的错误信息
const getErrorMessage = (error: { message: string; code?: string }): string => { const getErrorMessage = (error: ApiError): string => {
if (error.code) { if (error.code) {
const translated = t(`authForm.errors.${error.code}`); const key = `authForm.errors.${error.code}`;
// 如果翻译不存在,返回原始 message const translated = t(key);
if (!translated.includes('authForm.errors')) { // 如果翻译存在,返回翻译结果
if (translated !== key) {
return translated; return translated;
} }
} }
return error.message; // 返回 message 或默认错误信息
return error.message || t("authForm.errors.UNKNOWN_ERROR");
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -56,33 +59,49 @@ export function AuthForm({ mode = "login", onSuccess, onModeChange }: AuthFormPr
try { try {
if (isLogin) { if (isLogin) {
await signIn.username({ username, password }, { const result = await signIn.username({ username, password }, {
onSuccess: async (ctx) => { onSuccess: async (ctx) => {
const authToken = ctx.response.headers.get('set-auth-token') const authToken = ctx.response.headers.get('set-auth-token')
if (authToken) { if (authToken) {
setAuthToken(authToken) await setAuthToken(authToken)
} }
}, },
onError: (ctx) => { onError: (ctx) => {
setError(getErrorMessage(ctx.error)); setError(getErrorMessage(ctx.error));
console.error(`[LOGIN] username login error`, ctx)
},
});
} else {
await signUp.email({ email, password, name: username }, {
onSuccess: async (ctx) => {
const authToken = ctx.response.headers.get('set-auth-token')
if (authToken) {
setAuthToken(authToken)
}
},
onError: (ctx) => {
setError(getErrorMessage(ctx.error));
console.error(`[LOGIN] username login error`, ctx)
}, },
}); });
// 检查返回结果中是否有错误
if (result.error) {
setError(getErrorMessage(result.error));
return;
} }
// 登录成功
onSuccess?.(); onSuccess?.();
} else {
const result = await signUp.email({ email, password, name: username }, {
onSuccess: async (ctx) => {
const authToken = ctx.response.headers.get('set-auth-token')
if (authToken) {
await setAuthToken(authToken)
}
},
onError: (ctx) => {
setError(getErrorMessage(ctx.error));
console.error(`[REGISTER] email register error`, ctx)
},
});
// 检查返回结果中是否有错误
if (result.error) {
setError(getErrorMessage(result.error));
return;
}
// 注册成功
onSuccess?.();
}
} catch (e: unknown) { } catch (e: unknown) {
const msg = e instanceof Error ? e.message : (isLogin ? t("authForm.loginFailed") : t("authForm.registerFailed")); const msg = e instanceof Error ? e.message : (isLogin ? t("authForm.loginFailed") : t("authForm.registerFailed"));
setError(msg); setError(msg);
@@ -139,7 +158,9 @@ export function AuthForm({ mode = "login", onSuccess, onModeChange }: AuthFormPr
/> />
{error ? ( {error ? (
<Text className="text-red-500 text-sm mb-4 text-center">{error}</Text> <Text style={{ color: '#ef4444' }} className="text-sm mb-4 text-center">
{error}
</Text>
) : null} ) : null}
<Button <Button

View File

@@ -151,7 +151,7 @@
"select": "选择", "select": "选择",
"selectPlaceholder": "请选择", "selectPlaceholder": "请选择",
"noFields": "暂无可填写的表单项", "noFields": "暂无可填写的表单项",
"submit": "提交" "submit": "生成视频"
}, },
"pointsDrawer": { "pointsDrawer": {
"title": "我的积分", "title": "我的积分",