Initial commit: Expo app with Better Auth integration

- Complete Expo React Native app setup with TypeScript
- Better Auth authentication system integration
- Secure storage implementation for session tokens
- Authentication flow with login/logout functionality
- API client configuration for backend communication
- Responsive UI components with themed styling
- Expo Router navigation setup
- Development configuration and scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2025-10-14 10:32:52 +08:00
commit 7e3f94bae3
71 changed files with 20800 additions and 0 deletions

531
app/template/[id]/run.tsx Normal file
View File

@@ -0,0 +1,531 @@
import { DynamicForm } from '@/components/forms/dynamic-form';
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 { 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 { 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';
type RunStep = 'form' | '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'
};
} else if (fieldName.startsWith('text_')) {
// 文本字段转换为对象格式
transformed[fieldName] = {
content: value,
type: 'text'
};
} else if (fieldName.startsWith('video_')) {
// 视频字段转换为对象格式
transformed[fieldName] = {
url: value,
type: 'video'
};
} else if (fieldName.startsWith('color_')) {
// 颜色字段转换为对象格式
transformed[fieldName] = {
value: value,
type: 'color'
};
} else {
// 其他类型保持原样
transformed[fieldName] = value;
}
});
return transformed;
}
export default function TemplateRunScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
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 {
state: runState,
progress,
result,
error,
isLoading,
isCompleted,
isError,
executeTemplate,
cancelRun,
reset,
cleanup,
} = useTemplateRun({
onSuccess: (result) => {
setCurrentStep('result');
},
onError: (error) => {
Alert.alert('运行失败', error.message);
},
});
const {
formData,
errors,
updateField,
validateForm,
isValid,
} = useTemplateFormData();
// 获取模板信息
useEffect(() => {
const loadTemplate = async () => {
if (!id) return;
try {
setLoading(true);
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('错误', '无法加载模板,请稍后重试');
router.back();
} finally {
setLoading(false);
}
};
loadTemplate();
}, [id, router]);
// 处理返回键
useEffect(() => {
const handleBackPress = () => {
if (currentStep === 'progress' && isLoading) {
Alert.alert(
'确认退出',
'任务正在运行中,确定要退出吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '退出',
style: 'destructive',
onPress: () => {
cancelRun();
router.back();
},
},
]
);
return true;
}
return false;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => subscription.remove();
}, [currentStep, isLoading, cancelRun, router]);
// 组件卸载时清理
useEffect(() => {
return cleanup;
}, [cleanup]);
// 验证表单数据
const validateFormData = useCallback((): { isValid: boolean; message?: string } => {
if (!template) {
return { isValid: false, message: '模板信息未加载完成' };
}
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}` };
}
}
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 {
Alert.alert(
'确认提交',
'确定要开始生成任务吗?',
[
{ text: '取消', style: 'cancel' },
{ text: '确定', onPress: onConfirm }
]
);
}
}, []);
// 提交表单
const handleSubmit = useCallback(() => {
const validation = validateFormData();
if (!validation.isValid) {
Alert.alert('表单错误', validation.message);
return;
}
const handleConfirm = async () => {
// 转换表单数据为 API 期望的格式
const transformedData = transformFormData(formData);
console.log('转换后的数据:', transformedData);
const identify = await subscription.meterEvent({
meter_id: `mtr_test_61TR3BkyDcen9OrBm412fzUa9k2QB732`,
event_name: `token_usage`,
payload: {
value: `100`
}
})
console.log({ identify })
// error
const orid = identify.data?.identifier
console.log({ orid })
if (orid) {
try {
setCurrentStep('progress');
executeTemplate(template!.id, transformedData);
} catch (e) {
// 退款orid
}
}
};
showConfirmDialog(handleConfirm);
}, [validateFormData, showConfirmDialog, executeTemplate, template, formData]);
// 重新运行
const handleRerun = useCallback(() => {
setCurrentStep('form');
reset();
}, [reset]);
// 渲染表单步骤
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>
)}
{/* 表单内容 */}
<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);
});
}}
/>
) : (
<View style={styles.noConfigContainer}>
<ThemedText style={styles.noConfigText}>
使
</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>
);
}
// 错误状态
if (error && !template) {
return (
<ThemedView style={styles.errorContainer}>
<ThemedText style={styles.errorText}>
{error.message}
</ThemedText>
</ThemedView>
);
}
// 渲染当前步骤
return (
<ThemedView style={styles.container}>
<Stack.Screen
options={{
title: currentStep === 'form' ? '配置模板' :
currentStep === 'progress' ? '生成中' : '生成结果',
headerBackVisible: currentStep !== 'progress' || !isLoading,
}}
/>
{currentStep === 'form' && renderFormStep()}
{currentStep === 'progress' && renderProgressStep()}
{currentStep === 'result' && renderResultStep()}
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: 16,
textAlign: 'center',
opacity: 0.7,
},
templateHeader: {
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
headerGradient: {
paddingTop: 20,
paddingBottom: 24,
paddingHorizontal: 20,
},
templateInfo: {
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: {
fontSize: 16,
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',
},
});