diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index eb6ac28..a6d5472 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -5,8 +5,10 @@ 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 { Ionicons } from '@expo/vector-icons'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { Alert, Animated, StyleSheet, Text } from 'react-native'; +import { Alert, Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { LoginModal } from '@/components/auth/login-modal'; export default function HomeScreen() { const { isAuthenticated } = useAuth(); @@ -17,6 +19,7 @@ export default function HomeScreen() { const [currentPage, setCurrentPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(null); + const [showLoginModal, setShowLoginModal] = useState(false); const pulseAnim = useRef(new Animated.Value(0)).current; const bounceAnim = useRef(new Animated.Value(0)).current; @@ -40,16 +43,13 @@ export default function HomeScreen() { }); 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); @@ -70,6 +70,7 @@ export default function HomeScreen() { useEffect(() => { if (isAuthenticated) { fetchTemplates(); + setShowLoginModal(false); } Animated.loop( @@ -126,12 +127,55 @@ export default function HomeScreen() { }; if (!isAuthenticated) { + const scaleAnim = pulseAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0.95, 1.05], + }); + return ( - - 欢迎使用 - 请先登录以查看模板列表 - + + + + + + + 欢迎使用模板中心 + + + 登录后即可浏览和使用海量精美模板 + + + setShowLoginModal(true)} + activeOpacity={0.8} + > + + + 立即登录 + + + + + 游客浏览 + + + + setShowLoginModal(false)} + /> ); } @@ -176,6 +220,11 @@ export default function HomeScreen() { onVideoChange={handleVideoChange} error={error} /> + + setShowLoginModal(false)} + /> ); } @@ -184,6 +233,74 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + welcomeContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 40, + }, + iconContainer: { + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: 'rgba(0, 122, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 24, + }, + welcomeTitle: { + fontSize: 28, + fontWeight: '700', + marginBottom: 12, + textAlign: 'center', + }, + welcomeSubtitle: { + fontSize: 16, + textAlign: 'center', + opacity: 0.7, + marginBottom: 48, + lineHeight: 24, + }, + loginButton: { + width: '100%', + height: 56, + borderRadius: 16, + overflow: 'hidden', + elevation: 8, + shadowColor: '#007AFF', + shadowOffset: { width: 0, height: 4 }, + shadowRadius: 12, + shadowOpacity: 0.3, + }, + loginButtonGradient: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + }, + loginButtonIcon: { + marginRight: 8, + }, + loginButtonText: { + color: '#fff', + fontSize: 17, + fontWeight: '700', + }, + guestButton: { + width: '100%', + height: 56, + borderRadius: 16, + borderWidth: 1.5, + borderColor: 'rgba(0, 122, 255, 0.3)', + alignItems: 'center', + justifyContent: 'center', + marginTop: 12, + }, + guestButtonText: { + color: '#007AFF', + fontSize: 16, + fontWeight: '600', + }, headerBanner: { position: 'absolute', top: 16, @@ -229,18 +346,4 @@ const styles = StyleSheet.create({ 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/components/auth/login-modal.tsx b/components/auth/login-modal.tsx new file mode 100644 index 0000000..0ce184d --- /dev/null +++ b/components/auth/login-modal.tsx @@ -0,0 +1,412 @@ +import { Feather, Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; +import React, { useEffect, useRef, useState } from "react"; +import { + ActivityIndicator, + Alert, + Animated, + Dimensions, + KeyboardAvoidingView, + Modal, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { useAuth } from "@/hooks/use-auth"; +import { authClient } from "@/lib/auth/client"; +import { storage } from "@/lib/storage"; + +interface LoginModalProps { + visible: boolean; + onClose: () => void; +} + +export function LoginModal({ visible, onClose }: LoginModalProps) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isPasswordVisible, setPasswordVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { isAuthenticated } = useAuth(); + const slideAnim = useRef(new Animated.Value(Dimensions.get("window").height)).current; + const fadeAnim = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0.5, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: Dimensions.get("window").height, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible]); + + useEffect(() => { + if (isAuthenticated && visible) { + onClose(); + setEmail(""); + setPassword(""); + } + }, [isAuthenticated, visible, onClose]); + + const handleClose = () => { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: Dimensions.get("window").height, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + ]).start(() => onClose()); + }; + + const handleLogin = async () => { + if (!email.trim() || !password.trim()) { + Alert.alert("错误", "请输入邮箱和密码"); + return; + } + + setIsLoading(true); + + try { + await new Promise(async (resolve, reject) => { + await authClient.signIn.username( + { + username: email.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(); + } + }, + } + ); + }); + Alert.alert("成功", "登录成功!"); + } catch (error: any) { + Alert.alert("登录失败", error?.message || "邮箱或密码错误"); + } finally { + setIsLoading(false); + } + }; + + const handleBackdropPress = () => { + handleClose(); + }; + + return ( + + + + + + + + + + + + + + + + + + + + Email Login + + + + + + + + + + + + + + + setPasswordVisible((visible) => !visible)} + style={styles.visibilityToggle} + > + + + + + + {isLoading ? ( + + ) : ( + Log in + )} + + + + + + 已阅读并同意 + 用户协议 + 和 + 隐私政策 + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + backdrop: { + flex: 1, + }, + modalContainer: { + position: "absolute", + left: 0, + right: 0, + bottom: 0, + }, + modalContent: { + backgroundColor: "#14161c", + borderTopLeftRadius: 28, + borderTopRightRadius: 28, + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.08)", + shadowColor: "#000", + shadowOffset: { width: 0, height: -8 }, + shadowRadius: 18, + shadowOpacity: 0.25, + elevation: 24, + maxHeight: Dimensions.get("window").height * 0.75, + }, + sheetIndicator: { + width: 40, + height: 4, + backgroundColor: "rgba(255, 255, 255, 0.3)", + borderRadius: 2, + alignSelf: "center", + marginTop: 12, + marginBottom: 8, + }, + safeArea: { + flex: 1, + }, + flex: { + flex: 1, + }, + content: { + flex: 1, + paddingHorizontal: 24, + paddingBottom: 34, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginTop: 16, + marginBottom: 28, + }, + titleGroup: { + flexDirection: "row", + alignItems: "center", + }, + iconBadge: { + width: 40, + height: 40, + borderRadius: 16, + alignItems: "center", + justifyContent: "center", + backgroundColor: "#f6474d", + marginRight: 12, + }, + title: { + color: "#f5f6f8", + fontSize: 22, + fontWeight: "700", + letterSpacing: 0.2, + }, + closeButton: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255, 255, 255, 0.05)", + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.06)", + }, + form: { + marginTop: 4, + }, + inputWrapper: { + position: "relative", + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 18, + backgroundColor: "#1a1d23", + borderRadius: 18, + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.05)", + marginBottom: 18, + }, + input: { + flex: 1, + paddingVertical: 16, + fontSize: 16, + color: "#f5f6f8", + letterSpacing: 0.3, + }, + visibilityToggle: { + marginLeft: 12, + }, + loginButton: { + height: 56, + borderRadius: 18, + alignItems: "center", + justifyContent: "center", + backgroundColor: "#d7ff1f", + shadowColor: "#d7ff1f", + shadowOpacity: 0.35, + shadowRadius: 16, + shadowOffset: { width: 0, height: 10 }, + elevation: 10, + marginTop: 4, + }, + loginButtonDisabled: { + opacity: 0.7, + }, + loginButtonText: { + color: "#10120d", + fontSize: 17, + fontWeight: "700", + letterSpacing: 0.5, + }, + termsRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + marginTop: 18, + }, + termsText: { + color: "#a7abb5", + fontSize: 13, + lineHeight: 18, + marginLeft: 8, + }, + linkText: { + color: "#f5f6f8", + fontWeight: "600", + }, +}); diff --git a/design/login.png b/design/login.png new file mode 100644 index 0000000..1e09914 Binary files /dev/null and b/design/login.png differ diff --git a/lib/api/client.ts b/lib/api/client.ts index 51493db..1493451 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -1,7 +1,7 @@ import { fetch, FetchRequestInit } from 'expo/fetch'; import { storage } from '../storage'; -const BASE_URL = 'https://api.mixvideo.bowong.cc'; +const BASE_URL = 'https://api-test.mixvideo.bowong.cc'; export interface ApiRequestOptions extends FetchRequestInit { params?: Record; diff --git a/package.json b/package.json index 2b4cd57..1a0d4df 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "lint": "expo lint" + "lint": "expo lint", + "claude": "claude --dangerously-skip-permissions" }, "dependencies": { "@better-auth/expo": "^1.3.27",