✨ feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面 - 更新底部导航:将explore改为content,index改为home - 新增多个页面:积分、兑换、历史记录、结果展示、设置等 - 优化UI组件:新增通用按钮、分类标签、加载状态等组件 - 清理冗余文件:删除旧的认证文档和积分详情页面 - 更新主题配置:调整配色方案和视觉风格 - 修复导入路径:优化组件导入结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,54 +1,46 @@
|
||||
import { DynamicForm } from '@/components/forms/dynamic-form';
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import { ResultDisplay } from '@/components/template-run/result-display';
|
||||
import { RunProgressView } from '@/components/template-run/run-progress';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { useTemplateFormData, useTemplateRun } from '@/hooks/use-template-run';
|
||||
import { useTemplateRun } from '@/hooks/use-template-run';
|
||||
import { getTemplateById } from '@/lib/api/templates';
|
||||
import { subscription } from '@/lib/auth/client';
|
||||
import { Template } from '@/lib/types/template';
|
||||
import { RunFormSchema, RunTemplateData } from '@/lib/types/template-run';
|
||||
import { transformWorkflowToFormSchema, validateFormSchema } from '@/lib/utils/form-schema-transformer';
|
||||
import { RunTemplateData } from '@/lib/types/template-run';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, BackHandler, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { Stack, useGlobalSearchParams, useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, BackHandler, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type RunStep = 'form' | 'progress' | 'result';
|
||||
type RunStep = 'progress' | 'result';
|
||||
|
||||
// 转换表单数据为 API 期望的格式
|
||||
function transformFormData(formData: RunTemplateData): RunTemplateData {
|
||||
const transformed: RunTemplateData = {};
|
||||
|
||||
Object.entries(formData).forEach(([fieldName, value]) => {
|
||||
// 根据字段名称判断类型并转换
|
||||
if (fieldName.startsWith('image_')) {
|
||||
// 图片字段转换为对象格式
|
||||
transformed[fieldName] = {
|
||||
url: value,
|
||||
type: 'image'
|
||||
type: 'image',
|
||||
};
|
||||
} else if (fieldName.startsWith('text_')) {
|
||||
// 文本字段转换为对象格式
|
||||
transformed[fieldName] = {
|
||||
content: value,
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
};
|
||||
} else if (fieldName.startsWith('video_')) {
|
||||
// 视频字段转换为对象格式
|
||||
transformed[fieldName] = {
|
||||
url: value,
|
||||
type: 'video'
|
||||
type: 'video',
|
||||
};
|
||||
} else if (fieldName.startsWith('color_')) {
|
||||
// 颜色字段转换为对象格式
|
||||
transformed[fieldName] = {
|
||||
value: value,
|
||||
type: 'color'
|
||||
value,
|
||||
type: 'color',
|
||||
};
|
||||
} else {
|
||||
// 其他类型保持原样
|
||||
transformed[fieldName] = value;
|
||||
}
|
||||
});
|
||||
@@ -59,10 +51,14 @@ function transformFormData(formData: RunTemplateData): RunTemplateData {
|
||||
export default function TemplateRunScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const globalParams = useGlobalSearchParams<{ formData?: string }>();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [template, setTemplate] = useState<Template | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentStep, setCurrentStep] = useState<RunStep>('form');
|
||||
const [formSchema, setFormSchema] = useState<RunFormSchema>({ fields: [] });
|
||||
const [currentStep, setCurrentStep] = useState<RunStep>('progress');
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const [formImageUrls, setFormImageUrls] = useState<string[]>([]);
|
||||
|
||||
const {
|
||||
progress,
|
||||
@@ -71,20 +67,16 @@ export default function TemplateRunScreen() {
|
||||
isLoading,
|
||||
executeTemplate,
|
||||
cancelRun,
|
||||
reset,
|
||||
cleanup,
|
||||
} = useTemplateRun({
|
||||
onSuccess: (result) => {
|
||||
onSuccess: () => {
|
||||
setCurrentStep('result');
|
||||
},
|
||||
onError: (error) => {
|
||||
Alert.alert('运行失败', error.message);
|
||||
onError: (runError) => {
|
||||
Alert.alert('运行失败', runError.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { formData, errors, updateField } = useTemplateFormData();
|
||||
|
||||
// 获取模板信息
|
||||
useEffect(() => {
|
||||
const loadTemplate = async () => {
|
||||
if (!id) return;
|
||||
@@ -94,43 +86,12 @@ export default function TemplateRunScreen() {
|
||||
const response = await getTemplateById(id);
|
||||
if (response.success && response.data) {
|
||||
setTemplate(response.data);
|
||||
|
||||
// 解析表单配置
|
||||
if (response.data.formSchema) {
|
||||
try {
|
||||
console.log('原始formSchema数据:', response.data.formSchema);
|
||||
|
||||
// 解析formSchema数据(可能是字符串或对象)
|
||||
const rawData = typeof response.data.formSchema === 'string'
|
||||
? JSON.parse(response.data.formSchema)
|
||||
: response.data.formSchema;
|
||||
|
||||
// 使用转换工具将工作流数据转换为表单配置
|
||||
const formConfig = transformWorkflowToFormSchema(rawData);
|
||||
|
||||
// 验证转换结果
|
||||
if (validateFormSchema(formConfig)) {
|
||||
console.log('转换后的表单配置:', formConfig);
|
||||
setFormSchema(formConfig);
|
||||
} else {
|
||||
console.warn('表单配置验证失败,使用空配置');
|
||||
setFormSchema({ fields: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析表单配置失败:', error);
|
||||
console.log('使用空表单配置作为后备方案');
|
||||
setFormSchema({ fields: [] });
|
||||
}
|
||||
} else {
|
||||
console.log('模板没有formSchema,使用空配置');
|
||||
setFormSchema({ fields: [] });
|
||||
}
|
||||
} else {
|
||||
throw new Error('模板不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载模板失败:', error);
|
||||
Alert.alert('错误', '无法加载模板,请稍后重试');
|
||||
} catch (templateError) {
|
||||
console.error('加载模板失败:', templateError);
|
||||
Alert.alert('错误', '无法加载模板,请稍后重试。');
|
||||
router.back();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -140,298 +101,324 @@ export default function TemplateRunScreen() {
|
||||
loadTemplate();
|
||||
}, [id, router]);
|
||||
|
||||
// 处理返回键
|
||||
useEffect(() => {
|
||||
const handleBackPress = () => {
|
||||
if (currentStep === 'progress' && isLoading) {
|
||||
Alert.alert(
|
||||
'确认退出',
|
||||
'任务正在运行中,确定要退出吗?',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '退出',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
cancelRun();
|
||||
router.back();
|
||||
if (template && !hasStarted && globalParams.formData) {
|
||||
try {
|
||||
const formData = JSON.parse(globalParams.formData) as RunTemplateData;
|
||||
const images = Object.entries(formData)
|
||||
.filter(([fieldName, value]) => fieldName.startsWith('image_') && typeof value === 'string' && value.length > 0)
|
||||
.map(([, value]) => value as string);
|
||||
setFormImageUrls(images);
|
||||
setHasStarted(true);
|
||||
|
||||
const executeWithData = async () => {
|
||||
try {
|
||||
const list = await subscription.list();
|
||||
const listData = list.data ?? [];
|
||||
if (listData.length === 0) {
|
||||
Alert.alert('未检测到订阅', '请先完成订阅后再尝试生成内容。');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionId = listData[0]?.stripeSubscriptionId;
|
||||
if (!subscriptionId) {
|
||||
Alert.alert('订阅信息缺失', '当前订阅缺少 Stripe 订阅标识,无法记录用量。');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
await subscription.credit.summary({
|
||||
subscriptionId,
|
||||
filter: {
|
||||
type: 'applicability_scope',
|
||||
applicability_scope: {
|
||||
price_type: 'metered',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
});
|
||||
|
||||
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||
return () => subscription.remove();
|
||||
}, [currentStep, isLoading, cancelRun, router]);
|
||||
const identify = await subscription.meterEvent({
|
||||
event_name: 'token_usage',
|
||||
payload: {
|
||||
value: '100',
|
||||
},
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
return cleanup;
|
||||
}, [cleanup]);
|
||||
const identifier = identify.data?.identifier;
|
||||
if (!identifier) {
|
||||
Alert.alert('用量记录失败', '无法生成用量凭证,请稍后重试。');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证表单数据
|
||||
const validateFormData = useCallback((): { isValid: boolean; message?: string } => {
|
||||
if (!template) {
|
||||
return { isValid: false, message: '模板信息未加载完成' };
|
||||
}
|
||||
const transformedData = transformFormData(formData);
|
||||
await executeTemplate(template.id, transformedData);
|
||||
} catch (executeError) {
|
||||
console.error('执行模板失败:', executeError);
|
||||
Alert.alert('执行失败', '运行模板时出现异常,请稍后重试。');
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return { isValid: false, message: `请修正 ${Object.keys(errors).length} 个错误后再提交` };
|
||||
}
|
||||
|
||||
if (formSchema.fields?.length > 0) {
|
||||
const requiredFields = formSchema.fields.filter(field => field.required);
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const value = formData[field.name];
|
||||
return value === undefined || value === null || value === '';
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
const fieldNames = missingFields.map(f => f.label || f.name).join('、');
|
||||
return { isValid: false, message: `请填写必填项:${fieldNames}` };
|
||||
executeWithData();
|
||||
} catch (parseError) {
|
||||
console.error('解析表单数据失败:', parseError);
|
||||
Alert.alert('数据错误', '表单数据格式错误,请重新提交。');
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
}, [template, hasStarted, globalParams.formData, executeTemplate, router]);
|
||||
|
||||
return { isValid: true };
|
||||
}, [template, errors, formSchema.fields, formData]);
|
||||
|
||||
// 确认对话框
|
||||
const showConfirmDialog = useCallback((onConfirm: () => void) => {
|
||||
if (Platform.OS === 'web') {
|
||||
const confirmed = window.confirm('确定要开始生成任务吗?');
|
||||
if (confirmed) onConfirm();
|
||||
} else {
|
||||
const handleRequestExit = useCallback(() => {
|
||||
if (currentStep === 'progress' && isLoading) {
|
||||
Alert.alert(
|
||||
'确认提交',
|
||||
'确定要开始生成任务吗?',
|
||||
'确认退出',
|
||||
'任务正在运行中,确定要退出吗?',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: '确定', onPress: onConfirm }
|
||||
{ text: '继续等待', style: 'cancel' },
|
||||
{
|
||||
text: '停止并返回',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
cancelRun();
|
||||
router.back();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = useCallback(() => {
|
||||
const validation = validateFormData();
|
||||
|
||||
if (!validation.isValid) {
|
||||
Alert.alert('表单错误', validation.message);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
const transformedData = transformFormData(formData);
|
||||
console.log('转换后的数据:', transformedData);
|
||||
router.back();
|
||||
return true;
|
||||
}, [cancelRun, currentStep, isLoading, router]);
|
||||
|
||||
const list = await subscription.list();
|
||||
const listData = list.data ?? [];
|
||||
if (listData.length === 0) {
|
||||
Alert.alert('未检测到订阅', '请先完成订阅后再尝试生成内容。');
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
const subscription = BackHandler.addEventListener('hardwareBackPress', handleRequestExit);
|
||||
return () => subscription.remove();
|
||||
}, [handleRequestExit]);
|
||||
|
||||
const subscriptionId = listData[0]?.stripeSubscriptionId;
|
||||
if (!subscriptionId) {
|
||||
Alert.alert('订阅信息缺失', '当前订阅缺少 Stripe 订阅标识,无法记录用量。');
|
||||
return;
|
||||
}
|
||||
useEffect(() => cleanup, [cleanup]);
|
||||
|
||||
await subscription.credit.summary({
|
||||
subscriptionId,
|
||||
filter: {
|
||||
type: 'applicability_scope',
|
||||
applicability_scope: {
|
||||
price_type: 'metered',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const identify = await subscription.meterEvent({
|
||||
event_name: 'token_usage',
|
||||
payload: {
|
||||
value: '100',
|
||||
},
|
||||
});
|
||||
|
||||
const identifier = identify.data?.identifier;
|
||||
if (!identifier) {
|
||||
Alert.alert('用量记录失败', '无法生成用量凭证,请稍后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setCurrentStep('progress');
|
||||
await executeTemplate(template!.id, transformedData);
|
||||
} catch (error) {
|
||||
console.error('执行模板失败:', error);
|
||||
Alert.alert('执行失败', '运行模板时出现异常,请稍后重试。');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交表单失败:', error);
|
||||
Alert.alert('提交失败', '提交生成请求时出现问题,请稍后重试。');
|
||||
}
|
||||
};
|
||||
|
||||
showConfirmDialog(handleConfirm);
|
||||
}, [validateFormData, showConfirmDialog, executeTemplate, template, formData]);
|
||||
|
||||
// 重新运行
|
||||
const handleRerun = useCallback(() => {
|
||||
setCurrentStep('form');
|
||||
reset();
|
||||
}, [reset]);
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
// 渲染表单步骤
|
||||
const renderFormStep = () => (
|
||||
<View style={styles.container}>
|
||||
{/* 模板信息头部 */}
|
||||
{template && (
|
||||
<View style={styles.templateHeader}>
|
||||
<LinearGradient
|
||||
colors={['#4ECDC4', '#44A3A0']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.headerGradient}
|
||||
>
|
||||
<View style={styles.templateInfo}>
|
||||
<Image
|
||||
source={{ uri: template.coverImageUrl }}
|
||||
style={styles.templateImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<View style={styles.templateDetails}>
|
||||
<ThemedText style={styles.templateTitle}>
|
||||
{template.title}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.templateDescription} numberOfLines={2}>
|
||||
{template.description}
|
||||
</ThemedText>
|
||||
{template.category && (
|
||||
<View style={styles.categoryBadge}>
|
||||
<ThemedText style={styles.categoryText}>
|
||||
{template.category.name}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
)}
|
||||
const progressMeta = useMemo(() => {
|
||||
const rawProgress = typeof progress.progress === 'number' ? progress.progress : 0;
|
||||
const normalized = Math.max(0, Math.min(rawProgress, 100));
|
||||
return {
|
||||
normalized,
|
||||
percentLabel: Math.round(normalized),
|
||||
fillWidth: normalized <= 0 ? 0 : Math.min(Math.max(normalized, 8), 100),
|
||||
};
|
||||
}, [progress.progress]);
|
||||
|
||||
{/* 表单内容 */}
|
||||
<View style={styles.formContainer}>
|
||||
<ThemedText style={styles.formTitle}>配置参数</ThemedText>
|
||||
{formSchema.fields && formSchema.fields.length > 0 ? (
|
||||
<DynamicForm
|
||||
schema={formSchema}
|
||||
onDataChange={(data, valid) => {
|
||||
// 实时更新表单数据到外部状态
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
updateField(key, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
const narrative = useMemo(() => {
|
||||
const fallbackTitle = 'Insert Your Product Into An ASMR Video';
|
||||
const fallbackDescription =
|
||||
'ASMR-Add-On keeps your original image the same while seamlessly adding our uploaded product into the ASMR scene.';
|
||||
|
||||
const rawTitle = template?.titleEn ?? template?.title ?? fallbackTitle;
|
||||
const rawDescription = template?.descriptionEn ?? template?.description ?? fallbackDescription;
|
||||
|
||||
return {
|
||||
title: rawTitle.toUpperCase(),
|
||||
description: rawDescription,
|
||||
};
|
||||
}, [template]);
|
||||
|
||||
const imagery = useMemo(() => {
|
||||
const fallback = template?.previewUrl ?? template?.coverImageUrl ?? undefined;
|
||||
const primary = formImageUrls[0] ?? fallback;
|
||||
const secondary = formImageUrls[1] ?? template?.previewUrl ?? primary;
|
||||
|
||||
return {
|
||||
hero: primary,
|
||||
overlay: secondary,
|
||||
preview: template?.previewUrl ?? primary ?? fallback,
|
||||
};
|
||||
}, [formImageUrls, template]);
|
||||
|
||||
const renderProgressStep = () => (
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={[
|
||||
styles.progressContent,
|
||||
{
|
||||
paddingTop: insets.top + 16,
|
||||
paddingBottom: insets.bottom + 40,
|
||||
},
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handleRequestExit}
|
||||
activeOpacity={0.8}
|
||||
style={[styles.backButton, styles.backButtonProgress]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.titleSection}>
|
||||
<ThemedText style={styles.title} lightColor="#FFFFFF" darkColor="#FFFFFF">
|
||||
{narrative.title}
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
style={styles.subtitle}
|
||||
lightColor="rgba(255,255,255,0.68)"
|
||||
darkColor="rgba(255,255,255,0.68)"
|
||||
>
|
||||
{narrative.description}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.heroContainer}>
|
||||
{imagery.hero ? (
|
||||
<Image source={{ uri: imagery.hero }} style={styles.heroImage} contentFit="cover" />
|
||||
) : (
|
||||
<View style={styles.noConfigContainer}>
|
||||
<ThemedText style={styles.noConfigText}>
|
||||
此模板无需配置,直接使用默认参数
|
||||
<View style={styles.heroPlaceholder} />
|
||||
)}
|
||||
|
||||
{imagery.overlay && (
|
||||
<View style={styles.overlayImageFrame}>
|
||||
<Image source={{ uri: imagery.overlay }} style={styles.overlayImage} contentFit="cover" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<LinearGradient colors={['#1E1E21', '#101015']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={styles.progressCard}>
|
||||
{imagery.preview ? (
|
||||
<View style={styles.previewFrame}>
|
||||
<Image source={{ uri: imagery.preview }} style={styles.previewImage} contentFit="cover" />
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.previewFrame, styles.previewPlaceholder]} />
|
||||
)}
|
||||
|
||||
<View style={styles.progressCopy}>
|
||||
<ThemedText style={styles.progressHeadline} lightColor="#FFFFFF" darkColor="#FFFFFF">
|
||||
✨ 正在加载生成中...
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
style={styles.progressMessage}
|
||||
lightColor="rgba(255,255,255,0.72)"
|
||||
darkColor="rgba(255,255,255,0.72)"
|
||||
>
|
||||
{progress.message || '正在为你织造 ASMR 场景,请稍候片刻。'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.progressBarArea}>
|
||||
<View style={styles.progressTrack}>
|
||||
{progressMeta.fillWidth > 0 && (
|
||||
<View style={[styles.progressFill, { width: `${progressMeta.fillWidth}%` }]}>
|
||||
<LinearGradient
|
||||
colors={['#78F8FF', '#49A7FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<ThemedText
|
||||
style={styles.progressPercent}
|
||||
lightColor="rgba(255,255,255,0.6)"
|
||||
darkColor="rgba(255,255,255,0.6)"
|
||||
>
|
||||
{progressMeta.percentLabel}%
|
||||
</ThemedText>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
style={styles.primaryButton}
|
||||
onPress={() => {
|
||||
Alert.alert('生成进行中', '系统正在为你制作视频,请耐心等待。');
|
||||
}}
|
||||
>
|
||||
<ThemedText style={styles.primaryButtonText} lightColor="#050505" darkColor="#050505">
|
||||
Generate Video
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const renderResultStep = () => (
|
||||
<View
|
||||
style={[
|
||||
styles.resultContainer,
|
||||
{
|
||||
paddingTop: insets.top + 16,
|
||||
paddingBottom: Math.max(insets.bottom, 24),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.resultHeader}>
|
||||
<TouchableOpacity
|
||||
onPress={handleRequestExit}
|
||||
activeOpacity={0.8}
|
||||
style={[styles.backButton, styles.backButtonResult]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.resultTitle} lightColor="#FFFFFF" darkColor="#FFFFFF">
|
||||
生成结果
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.resultBody}>
|
||||
{result ? (
|
||||
<ResultDisplay result={result} onRerun={handleRerun} />
|
||||
) : (
|
||||
<View style={styles.stateContainer}>
|
||||
<ThemedText style={styles.stateText} lightColor="rgba(255,255,255,0.7)" darkColor="rgba(255,255,255,0.7)">
|
||||
正在整理结果,请稍候...
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<View style={styles.submitContainer}>
|
||||
<TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
|
||||
<ThemedText style={styles.submitButtonText}>
|
||||
{isLoading ? '处理中...' : '开始生成'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染进度步骤
|
||||
const renderProgressStep = () => (
|
||||
<View style={styles.container}>
|
||||
<RunProgressView
|
||||
progress={progress}
|
||||
onCancel={() => {
|
||||
Alert.alert(
|
||||
'确认取消',
|
||||
'确定要取消当前任务吗?',
|
||||
[
|
||||
{ text: '继续执行', style: 'cancel' },
|
||||
{
|
||||
text: '取消任务',
|
||||
style: 'destructive',
|
||||
onPress: cancelRun,
|
||||
},
|
||||
]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染结果步骤
|
||||
const renderResultStep = () => (
|
||||
<View style={styles.container}>
|
||||
{result && (
|
||||
<ResultDisplay
|
||||
result={result}
|
||||
onShare={(url) => {
|
||||
// 可以添加自定义分享逻辑
|
||||
}}
|
||||
onDownload={(url) => {
|
||||
// 可以添加自定义下载逻辑
|
||||
}}
|
||||
onRerun={handleRerun}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<ThemedView style={styles.loadingContainer}>
|
||||
<ThemedText>加载中...</ThemedText>
|
||||
<ThemedView style={styles.screen} lightColor="#050505" darkColor="#050505">
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<View style={styles.stateContainer}>
|
||||
<ThemedText style={styles.stateText} lightColor="rgba(255,255,255,0.7)" darkColor="rgba(255,255,255,0.7)">
|
||||
加载中...
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (error && !template) {
|
||||
return (
|
||||
<ThemedView style={styles.errorContainer}>
|
||||
<ThemedText style={styles.errorText}>
|
||||
{error.message}
|
||||
</ThemedText>
|
||||
<ThemedView style={styles.screen} lightColor="#050505" darkColor="#050505">
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<View style={styles.stateContainer}>
|
||||
<ThemedText style={styles.stateText} lightColor="rgba(255,255,255,0.7)" darkColor="rgba(255,255,255,0.7)">
|
||||
{error.message}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染当前步骤
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedView style={styles.screen} lightColor="#050505" darkColor="#050505">
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: currentStep === 'form' ? '配置模板' :
|
||||
currentStep === 'progress' ? '生成中' : '生成结果',
|
||||
headerBackVisible: currentStep !== 'progress' || !isLoading,
|
||||
headerShown: false,
|
||||
gestureEnabled: currentStep !== 'progress' || !isLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
{currentStep === 'form' && renderFormStep()}
|
||||
{currentStep === 'progress' && renderProgressStep()}
|
||||
{currentStep === 'result' && renderResultStep()}
|
||||
</ThemedView>
|
||||
@@ -439,112 +426,182 @@ export default function TemplateRunScreen() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: '#050505',
|
||||
},
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
progressContent: {
|
||||
paddingHorizontal: 24,
|
||||
gap: 28,
|
||||
},
|
||||
backButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.08)',
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
opacity: 0.7,
|
||||
backButtonProgress: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
templateHeader: {
|
||||
elevation: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
backButtonResult: {
|
||||
marginRight: 12,
|
||||
},
|
||||
titleSection: {
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.8,
|
||||
lineHeight: 28,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
},
|
||||
heroContainer: {
|
||||
position: 'relative',
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#121212',
|
||||
},
|
||||
heroImage: {
|
||||
width: '100%',
|
||||
aspectRatio: 1.58,
|
||||
},
|
||||
heroPlaceholder: {
|
||||
width: '100%',
|
||||
aspectRatio: 1.58,
|
||||
backgroundColor: '#1F1F20',
|
||||
},
|
||||
overlayImageFrame: {
|
||||
position: 'absolute',
|
||||
left: 18,
|
||||
bottom: 18,
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 18,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 2,
|
||||
borderColor: '#050505',
|
||||
shadowColor: '#000000',
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
elevation: 6,
|
||||
},
|
||||
headerGradient: {
|
||||
paddingTop: 20,
|
||||
paddingBottom: 24,
|
||||
paddingHorizontal: 20,
|
||||
overlayImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
templateInfo: {
|
||||
progressCard: {
|
||||
borderRadius: 28,
|
||||
paddingVertical: 28,
|
||||
paddingHorizontal: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.06)',
|
||||
gap: 24,
|
||||
},
|
||||
previewFrame: {
|
||||
alignSelf: 'center',
|
||||
width: 96,
|
||||
height: 148,
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.12)',
|
||||
backgroundColor: '#0F0F12',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
previewPlaceholder: {
|
||||
backgroundColor: '#18181C',
|
||||
},
|
||||
progressCopy: {
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
progressHeadline: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
progressMessage: {
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
progressBarArea: {
|
||||
gap: 12,
|
||||
},
|
||||
progressTrack: {
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressPercent: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'right',
|
||||
},
|
||||
primaryButton: {
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#D7FF1F',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#D7FF1F',
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 16,
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
elevation: 4,
|
||||
},
|
||||
primaryButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.6,
|
||||
},
|
||||
resultContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 24,
|
||||
gap: 24,
|
||||
},
|
||||
resultHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
templateImage: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
marginRight: 16,
|
||||
},
|
||||
templateDetails: {
|
||||
flex: 1,
|
||||
},
|
||||
templateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginBottom: 4,
|
||||
},
|
||||
templateDescription: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginBottom: 8,
|
||||
},
|
||||
categoryBadge: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
fontWeight: '500',
|
||||
},
|
||||
formContainer: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
formTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 16,
|
||||
},
|
||||
noConfigContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
noConfigText: {
|
||||
resultTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
resultBody: {
|
||||
flex: 1,
|
||||
},
|
||||
stateContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
stateText: {
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
opacity: 0.7,
|
||||
},
|
||||
submitContainer: {
|
||||
marginTop: 'auto',
|
||||
paddingTop: 20,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#4ECDC4',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#4ECDC4',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
},
|
||||
submitButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user