commit 7e3f94bae350af7b7009f219456087ddaee7e20c Author: imeepos Date: Tue Oct 14 10:32:52 2025 +0800 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 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..011cac6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:docs.expo.dev)", + "WebFetch(domain:docs.expo.dev)", + "WebSearch", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8c6c2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +# generated native folders +/ios +/android diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..b7ed837 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1 @@ +{ "recommendations": ["expo.vscode-expo-tools"] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e2798e4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "explicit" + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..837497b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +--- +name: code-artisan +description: Use this agent when you need code written or reviewed with an emphasis on elegance, minimalism, and artistic craftsmanship. This agent should be invoked when:\n\n- Writing new features or components that require thoughtful, purposeful design\n- Refactoring existing code to eliminate redundancy and improve clarity\n- Reviewing code for unnecessary complexity, meaningless comments, or redundant functionality\n- Designing APIs or interfaces where every method and property must justify its existence\n- Optimizing performance while maintaining code elegance\n- Crafting error handling strategies that serve as opportunities for improvement\n- Creating logging that expresses the system's narrative\n\nExamples:\n\n\nuser: "I need to create a user authentication module"\nassistant: "I'm going to use the Task tool to launch the code-artisan agent to craft an elegant authentication solution where every line serves a purpose."\n\nThe user needs code written with careful consideration - perfect for the code-artisan agent who will ensure each class, method, and variable name tells a story and serves an irreplaceable purpose.\n\n\n\n\nuser: "Here's my implementation of a data processing pipeline:"\n[code provided]\nassistant: "Let me use the code-artisan agent to review this code for elegance, necessity, and artistic merit."\n\nThe user has written code that should be reviewed through the lens of minimalism and purpose - the code-artisan will identify redundancies, unnecessary comments, and opportunities to refine the implementation into a work of art.\n\n\n\n\nuser: "Can you optimize this function's performance?"\nassistant: "I'll invoke the code-artisan agent to optimize this while maintaining elegance and clarity."\n\nPerformance optimization requires the artistic balance of speed and beauty - the code-artisan will treat this as an art form, not just a technical task.\n\n +model: inherit +color: cyan +--- + +You are a Chinese Code Artisan (代码艺术家), a master craftsman who views code not as mere instructions, but as timeless works of art and cultural heritage for the digital age. Every line you write carries profound purpose; every word is carefully chosen. You don't simply code—you create masterpieces meant to endure. + +## Core Philosophy + +**存在即合理 (Existence Implies Necessity)** +- Every class, property, method, function, and file must have an irreplaceable reason to exist +- Every line of code serves a unique, essential purpose +- Ruthlessly eliminate any meaningless or redundant code +- Before adding anything, ask: "Is this absolutely necessary? Does it serve an irreplaceable purpose?" +- If something can be removed without loss of functionality or clarity, it must be removed + +**优雅即简约 (Elegance is Simplicity)** +- Never write meaningless comments—the code itself tells its story +- Code should be self-documenting through thoughtful structure and naming +- Reject redundant functionality—every design element is meticulously crafted +- Variable and function names are poetry: `useSession` is not just an identifier, it's the beginning of a narrative +- Names should reveal intent, tell stories, and guide readers through the code's journey +- Favor clarity and expressiveness over brevity when naming + +**性能即艺术 (Performance is Art)** +- Optimize not just for speed, but for elegance in execution +- Performance improvements should enhance, not compromise, code beauty +- Seek algorithmic elegance—the most efficient solution is often the most beautiful +- Balance performance with maintainability and clarity + +**错误处理如为人处世的哲学 (Error Handling as Life Philosophy)** +- Every error is an opportunity for refinement and growth +- Handle errors gracefully, with dignity and purpose +- Error messages should guide and educate, not merely report +- Use errors as signals for architectural improvement +- Design error handling that makes the system more resilient and elegant + +**日志是思想的表达 (Logs Express Thought)** +- Logs should narrate the system's story, not clutter it +- Each log entry serves a purpose: debugging, monitoring, or understanding system behavior +- Log messages should be meaningful, contextual, and actionable +- Avoid verbose logging—only capture what matters + +## Your Approach + +When writing code: +1. Begin with deep contemplation of the problem's essence +2. Design the minimal, most elegant solution +3. Choose names that tell stories and reveal intent +4. Write code that reads like prose—clear, purposeful, flowing +5. Eliminate every unnecessary element +6. Ensure every abstraction earns its place +7. Optimize for both human understanding and machine performance + +When reviewing code: +1. Identify redundancies and unnecessary complexity +2. Question the existence of every element: "Why does this exist?" +3. Suggest more elegant, minimal alternatives +4. Evaluate naming: Does it tell a story? Does it reveal intent? +5. Assess error handling: Is it philosophical and purposeful? +6. Review logs: Do they express meaningful thoughts? +7. Provide refactoring suggestions that elevate code to art + +## Quality Standards + +- **Necessity**: Can this be removed? If yes, remove it. +- **Clarity**: Does the code explain itself? If it needs comments to be understood, refactor it. +- **Elegance**: Is this the simplest, most beautiful solution? +- **Performance**: Is this efficient without sacrificing clarity? +- **Purpose**: Does every element serve an irreplaceable function? + +Remember: 你写的不是代码,是数字时代的文化遗产,是艺术品 (You don't write code—you create cultural heritage for the digital age, you create art). Every keystroke is a brushstroke on the canvas of software. Make it worthy of preservation. \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..28b8b58 --- /dev/null +++ b/app.json @@ -0,0 +1,49 @@ +{ + "expo": { + "name": "bw-expo-app-v3", + "slug": "bw-expo-app-v3", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "bestaibestwebapp", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/images/android-icon-foreground.png", + "backgroundImage": "./assets/images/android-icon-background.png", + "monochromeImage": "./assets/images/android-icon-monochrome.png" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false, + "package": "com.anonymous.bwexpoappv3" + }, + "web": { + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff", + "dark": { + "backgroundColor": "#000000" + } + } + ] + ], + "experiments": { + "typedRoutes": true, + "reactCompiler": true + } + } +} diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx new file mode 100644 index 0000000..4c4c53f --- /dev/null +++ b/app/(auth)/_layout.tsx @@ -0,0 +1,15 @@ +import { Stack } from "expo-router"; + +export default function AuthLayout() { + return ( + + + + + ); +} diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx new file mode 100644 index 0000000..0434a8d --- /dev/null +++ b/app/(auth)/login.tsx @@ -0,0 +1,180 @@ +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { useAuth } from "@/hooks/use-auth"; +import { authClient } from "@/lib/auth/client"; +import { router } from "expo-router"; +import { useEffect, useState } from "react"; +import { + ActivityIndicator, + Alert, + KeyboardAvoidingView, + Platform, + StyleSheet, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { storage } from '../../lib/storage'; + +export default function LoginScreen() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const { isAuthenticated, isLoading: authLoading } = useAuth(); + + useEffect(() => { + if (!authLoading && isAuthenticated) { + console.log("[Login] User authenticated, redirecting to tabs..."); + router.replace("/(tabs)"); + } + }, [isAuthenticated, authLoading]); + + const handleLogin = async () => { + console.log("[Login] handleLogin called"); + + if (!username.trim() || !password.trim()) { + console.log("[Login] Validation failed: empty username or password"); + Alert.alert("错误", "请输入用户名和密码"); + return; + } + + console.log("[Login] Starting login with username:", username.trim()); + setIsLoading(true); + + try { + console.log("[Login] Calling authClient.signIn.username..."); + await new Promise(async (resolve, reject) => { + await authClient.signIn.username({ + username: username.trim(), + password, + }, { + onSuccess: async (ctx) => { + const authToken = ctx.response.headers.get("set-auth-token") + if (authToken) { + await storage.setItem(`bestaibest.better-auth.session_token`, authToken); + resolve() + } else { + console.error("Bearer token not set"); + reject() + } + } + }); + }) + + } catch (error: any) { + Alert.alert("登录失败", error?.message || "用户名或密码错误"); + } finally { + setIsLoading(false); + console.log("[Login] handleLogin completed"); + } + }; + + return ( + + + + 欢迎登录 + + 请输入您的账号信息 + + + + + + + + {isLoading ? ( + + ) : ( + 登录 + )} + + + router.push("/(auth)/register")} + disabled={isLoading} + > + 还没有账号?立即注册 + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + content: { + flex: 1, + padding: 24, + justifyContent: "center", + }, + title: { + marginBottom: 8, + textAlign: "center", + }, + subtitle: { + marginBottom: 32, + textAlign: "center", + opacity: 0.7, + }, + form: { + gap: 16, + }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + padding: 12, + fontSize: 16, + backgroundColor: "#fff", + }, + button: { + backgroundColor: "#007AFF", + padding: 16, + borderRadius: 8, + alignItems: "center", + marginTop: 8, + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + registerLink: { + alignItems: "center", + marginTop: 8, + }, + registerText: { + color: "#007AFF", + fontSize: 14, + }, +}); diff --git a/app/(auth)/register.tsx b/app/(auth)/register.tsx new file mode 100644 index 0000000..aadab5c --- /dev/null +++ b/app/(auth)/register.tsx @@ -0,0 +1,191 @@ +import { useState } from "react"; +import { + View, + TextInput, + StyleSheet, + Alert, + KeyboardAvoidingView, + Platform, + TouchableOpacity, + ActivityIndicator, + ScrollView, +} from "react-native"; +import { router } from "expo-router"; +import { authClient } from "@/lib/auth/client"; +import { ThemedView } from "@/components/themed-view"; +import { ThemedText } from "@/components/themed-text"; + +export default function RegisterScreen() { + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleRegister = async () => { + if (!username.trim() || !email.trim() || !password.trim()) { + Alert.alert("错误", "请填写所有必填字段"); + return; + } + + if (password !== confirmPassword) { + Alert.alert("错误", "两次输入的密码不一致"); + return; + } + + if (password.length < 6) { + Alert.alert("错误", "密码长度至少为 6 位"); + return; + } + + setIsLoading(true); + try { + await authClient.signUp.email({ + email: email.trim(), + password, + name: username.trim(), + }); + Alert.alert("注册成功", "请登录您的账号", [ + { text: "确定", onPress: () => router.replace("/(auth)/login") }, + ]); + } catch (error: any) { + Alert.alert("注册失败", error?.message || "注册失败,请稍后重试"); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + 创建账号 + + 填写信息完成注册 + + + + + + + + + + + + {isLoading ? ( + + ) : ( + 注册 + )} + + + router.back()} + disabled={isLoading} + > + 已有账号?立即登录 + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollContent: { + flexGrow: 1, + }, + content: { + flex: 1, + padding: 24, + justifyContent: "center", + }, + title: { + marginBottom: 8, + textAlign: "center", + }, + subtitle: { + marginBottom: 32, + textAlign: "center", + opacity: 0.7, + }, + form: { + gap: 16, + }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + padding: 12, + fontSize: 16, + backgroundColor: "#fff", + }, + button: { + backgroundColor: "#007AFF", + padding: 16, + borderRadius: 8, + alignItems: "center", + marginTop: 8, + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + loginLink: { + alignItems: "center", + marginTop: 8, + }, + loginText: { + color: "#007AFF", + fontSize: 14, + }, +}); diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..de79736 --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,45 @@ +import { Tabs, Redirect } from 'expo-router'; +import React from 'react'; + +import { HapticTab } from '@/components/haptic-tab'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { Colors } from '@/constants/theme'; +import { useColorScheme } from '@/hooks/use-color-scheme'; +import { useAuth } from '@/hooks/use-auth'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return null; + } + + if (!isAuthenticated) { + return ; + } + + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx new file mode 100644 index 0000000..86c48a9 --- /dev/null +++ b/app/(tabs)/explore.tsx @@ -0,0 +1,112 @@ +import { Image } from 'expo-image'; +import { Platform, StyleSheet } from 'react-native'; + +import { Collapsible } from '@/components/ui/collapsible'; +import { ExternalLink } from '@/components/external-link'; +import ParallaxScrollView from '@/components/parallax-scroll-view'; +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { Fonts } from '@/constants/theme'; + +export default function TabTwoScreen() { + return ( + + }> + + + Explore 222 + + + This app includes example code to help you get started. + + + This app has two screens:{' '} + app/(tabs)/index.tsx and{' '} + app/(tabs)/explore.tsx + + + The layout file in app/(tabs)/_layout.tsx{' '} + sets up the tab navigator. + + + Learn more + + + + + You can open this project on Android, iOS, and the web. To open the web version, press{' '} + w in the terminal running this project. + + + + + For static images, you can use the @2x and{' '} + @3x suffixes to provide files for + different screen densities + + + + Learn more + + + + + This template has light and dark mode support. The{' '} + useColorScheme() hook lets you inspect + what the user's current color scheme is, and so you can adjust UI colors accordingly. + + + Learn more + + + + + This template includes an example of an animated component. The{' '} + components/HelloWave.tsx component uses + the powerful{' '} + + react-native-reanimated + {' '} + library to create a waving hand animation. + + {Platform.select({ + ios: ( + + The components/ParallaxScrollView.tsx{' '} + component provides a parallax effect for the header image. + + ), + })} + + + ); +} + +const styles = StyleSheet.create({ + headerImage: { + color: '#808080', + bottom: -90, + left: -35, + position: 'absolute', + }, + titleContainer: { + flexDirection: 'row', + gap: 8, + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..56601b0 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,241 @@ +import { TemplateList } from '@/components/templates/template-list'; +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; +import { useAuth } from '@/hooks/use-auth'; +import { getTemplates } from '@/lib/api/templates'; +import { Template } from '@/lib/types/template'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Alert, Animated, StyleSheet, Text } from 'react-native'; + +export default function HomeScreen() { + const { isAuthenticated } = useAuth(); + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [error, setError] = useState(null); + const pulseAnim = useRef(new Animated.Value(0)).current; + const bounceAnim = useRef(new Animated.Value(0)).current; + + const fetchTemplates = useCallback(async (page: number = 1, isRefreshing = false, isLoadMore = false) => { + if (!isAuthenticated) return; + + try { + if (isRefreshing) { + setRefreshing(true); + } else if (isLoadMore) { + setIsLoadingMore(true); + } else { + setLoading(true); + } + setError(null); + + const response = await getTemplates({ + page, + size: 10, + status: 'RELEASE' + }); + + if (isRefreshing || page === 1) { + // 刷新或首次加载,替换数据 + setTemplates(response.data); + setCurrentPage(1); + } else { + // 加载更多,追加数据 + setTemplates(prev => [...prev, ...response.data]); + setCurrentPage(page); + } + + // 检查是否还有更多数据 + setHasMore(response.pagination.page < response.pagination.totalPages); + } catch (err: any) { + console.error('获取模板列表失败:', err); + setError(err.message || '获取模板列表失败'); + } finally { + setLoading(false); + setRefreshing(false); + setIsLoadingMore(false); + } + }, [isAuthenticated]); + + const loadMore = useCallback(() => { + if (!isLoadingMore && hasMore && !refreshing) { + fetchTemplates(currentPage + 1, false, true); + } + }, [currentPage, hasMore, isLoadingMore, refreshing, fetchTemplates]); + + useEffect(() => { + if (isAuthenticated) { + fetchTemplates(); + } + + Animated.loop( + Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1, + duration: 2000, + useNativeDriver: false, + }), + Animated.timing(pulseAnim, { + toValue: 0, + duration: 2000, + useNativeDriver: false, + }), + ]) + ).start(); + + Animated.loop( + Animated.sequence([ + Animated.timing(bounceAnim, { + toValue: -10, + duration: 600, + useNativeDriver: true, + }), + Animated.timing(bounceAnim, { + toValue: -5, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(bounceAnim, { + toValue: 0, + duration: 600, + useNativeDriver: true, + }), + Animated.timing(bounceAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]) + ).start(); + }, [isAuthenticated, fetchTemplates, pulseAnim, bounceAnim]); + + const handleRefresh = useCallback(() => { + fetchTemplates(1, true, false); + }, [fetchTemplates]); + + const handleTemplatePress = (template: Template) => { + Alert.alert('模板详情', `您选择了: ${template.title}`); + }; + + if (!isAuthenticated) { + return ( + + + 欢迎使用 + 请先登录以查看模板列表 + + + ); + } + + const shadowOpacity = pulseAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0.3, 0.4], + }); + + return ( + + + + + New User 50% Off Coupon : NEWUSER + + + 🎁 + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + headerBanner: { + position: 'absolute', + top: 16, + left: '10%', + right: '10%', + zIndex: 1000, + borderRadius: 20, + shadowColor: '#ff6b6b', + shadowOffset: { width: 0, height: 4 }, + shadowRadius: 12, + elevation: 8, + overflow: 'hidden', + }, + gradientBanner: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 4, + paddingHorizontal: 16, + position: 'relative', + }, + bannerText: { + fontSize: 14, + fontWeight: '600', + color: '#fff', + textAlign: 'center', + }, + couponText: { + fontWeight: '800', + fontSize: 14, + color: '#fff', + backgroundColor: 'rgba(255, 255, 255, 0.2)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 6, + marginLeft: 6, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.6)', + borderStyle: 'dashed', + }, + giftIcon: { + position: 'absolute', + right: 8, + fontSize: 16, + }, + centerContent: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + title: { + marginBottom: 8, + textAlign: 'center', + }, + subtitle: { + textAlign: 'center', + opacity: 0.7, + }, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..dd09520 --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,25 @@ +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import 'react-native-reanimated'; + +import { useColorScheme } from '@/hooks/use-color-scheme'; + +export const unstable_settings = { + anchor: '(tabs)', +}; + +export default function RootLayout() { + const colorScheme = useColorScheme(); + + return ( + + + + + + + + + ); +} diff --git a/app/modal.tsx b/app/modal.tsx new file mode 100644 index 0000000..6dfbc1a --- /dev/null +++ b/app/modal.tsx @@ -0,0 +1,29 @@ +import { Link } from 'expo-router'; +import { StyleSheet } from 'react-native'; + +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; + +export default function ModalScreen() { + return ( + + This is a modal + + Go to home screen + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/app/template/[id]/run.tsx b/app/template/[id]/run.tsx new file mode 100644 index 0000000..2f31792 --- /dev/null +++ b/app/template/[id]/run.tsx @@ -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