import { Ionicons } from '@expo/vector-icons'; import * as AppleAuthentication from 'expo-apple-authentication'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Animated, Linking, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { login } from '@/store/userSlice'; import Toast from 'react-native-toast-message'; export default function LoginScreen() { const router = useRouter(); const searchParams = useLocalSearchParams<{ redirectTo?: string; redirectParams?: string }>(); const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const color = Colors[scheme]; const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background; const dispatch = useAppDispatch(); const AnimatedLinear = useMemo(() => Animated.createAnimatedComponent(LinearGradient), []); // 背景动效:轻微平移/旋转与呼吸动画 const translateAnim = useRef(new Animated.Value(0)).current; const rotateAnim = useRef(new Animated.Value(0)).current; const pulseAnimA = useRef(new Animated.Value(0)).current; const pulseAnimB = useRef(new Animated.Value(0)).current; useEffect(() => { const loopTranslate = Animated.loop( Animated.sequence([ Animated.timing(translateAnim, { toValue: 1, duration: 6000, useNativeDriver: true }), Animated.timing(translateAnim, { toValue: 0, duration: 6000, useNativeDriver: true }), ]) ); const loopRotate = Animated.loop( Animated.sequence([ Animated.timing(rotateAnim, { toValue: 1, duration: 10000, useNativeDriver: true }), Animated.timing(rotateAnim, { toValue: 0, duration: 10000, useNativeDriver: true }), ]) ); const loopPulseA = Animated.loop( Animated.sequence([ Animated.timing(pulseAnimA, { toValue: 1, duration: 3500, useNativeDriver: true }), Animated.timing(pulseAnimA, { toValue: 0, duration: 3500, useNativeDriver: true }), ]) ); const loopPulseB = Animated.loop( Animated.sequence([ Animated.timing(pulseAnimB, { toValue: 1, duration: 4200, useNativeDriver: true }), Animated.timing(pulseAnimB, { toValue: 0, duration: 4200, useNativeDriver: true }), ]) ); loopTranslate.start(); loopRotate.start(); loopPulseA.start(); loopPulseB.start(); return () => { loopTranslate.stop(); loopRotate.stop(); loopPulseA.stop(); loopPulseB.stop(); }; }, [pulseAnimA, pulseAnimB, rotateAnim, translateAnim]); const [hasAgreed, setHasAgreed] = useState(false); const [appleAvailable, setAppleAvailable] = useState(false); const [loading, setLoading] = useState(false); useEffect(() => { AppleAuthentication.isAvailableAsync().then(setAppleAvailable).catch(() => setAppleAvailable(false)); }, []); const guardAgreement = useCallback((action: () => void) => { if (!hasAgreed) { Alert.alert( '请先阅读并同意', '继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。', [ { text: '取消', style: 'cancel' }, { text: '同意并继续', onPress: () => { setHasAgreed(true); setTimeout(() => action(), 0); }, }, ], { cancelable: true } ); return; } action(); }, [hasAgreed]); const onAppleLogin = useCallback(async () => { if (!appleAvailable) return; try { setLoading(true); const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], }); const identityToken = (credential as any)?.identityToken; if (!identityToken || typeof identityToken !== 'string') { throw new Error('未获取到 Apple 身份令牌'); } await dispatch(login({ appleIdentityToken: identityToken })).unwrap(); Toast.show({ text1: '登录成功', type: 'success', }); // 登录成功后处理重定向 const to = searchParams?.redirectTo as string | undefined; const paramsJson = searchParams?.redirectParams as string | undefined; let parsedParams: Record | undefined; if (paramsJson) { try { parsedParams = JSON.parse(paramsJson); } catch { } } if (to) { router.replace({ pathname: to, params: parsedParams } as any); } else { router.back(); } } catch (err: any) { if (err?.code === 'ERR_CANCELED') return; const message = err?.message || '登录失败,请稍后再试'; Alert.alert('登录失败', message); } finally { setLoading(false); } }, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo]); // 登录按钮不再因未勾选协议而禁用,仅在加载中禁用 return ( {/* 动态背景层(置于内容之下) */} {/* 基础全屏渐变:保证覆盖全屏 */} {/* 次级大面积渐变:对角线方向形成层次 */} {/* 动感色块 A(主色呼吸,置于左下) */} {/* 动感色块 B(辅色漂移,置于右上) */} {/* 自定义头部,与其它页面风格一致 */} router.back()} style={styles.backButton}> 登录 Out Live 欢迎登录Out Live {/* Apple 登录 */} {appleAvailable && ( guardAgreement(onAppleLogin)} disabled={loading} style={({ pressed }) => [ styles.appleButton, { backgroundColor: '#000000' }, loading && { opacity: 0.7 }, pressed && { transform: [{ scale: 0.98 }] }, ]} > 使用 Apple 登录 )} {/* 协议勾选 */} setHasAgreed((v) => !v)} style={styles.checkboxWrap} accessibilityRole="checkbox" accessibilityState={{ checked: hasAgreed }}> {hasAgreed && } 我已阅读并同意 Linking.openURL(PRIVACY_POLICY_URL)}> 《隐私政策》 Linking.openURL(USER_AGREEMENT_URL)}> 《用户协议》 {/* 占位底部间距 */} ); } const styles = StyleSheet.create({ safeArea: { flex: 1 }, container: { flex: 1 }, content: { flexGrow: 1, paddingHorizontal: 24, justifyContent: 'center', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingTop: 4, paddingBottom: 8, }, backButton: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' }, headerTitle: { fontSize: 18, fontWeight: '700' }, headerWrap: { marginBottom: 36, }, title: { fontSize: 32, fontWeight: '500', letterSpacing: 0.5, lineHeight: 38, }, subtitle: { marginTop: 8, fontSize: 14, fontWeight: '500', }, appleButton: { height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.15, shadowRadius: 12, elevation: 2, }, appleText: { fontSize: 16, color: '#FFFFFF', fontWeight: '600', }, guestButton: { height: 52, borderRadius: 26, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', borderWidth: 1, marginTop: 6, }, guestText: { fontSize: 15, fontWeight: '500', }, agreementRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', marginTop: 24, }, checkboxWrap: { marginRight: 8 }, checkbox: { width: 18, height: 18, borderRadius: 5, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, agreementText: { fontSize: 12 }, link: { fontSize: 12, fontWeight: '600' }, footerHint: { marginTop: 24 }, hintText: { fontSize: 12 }, // 背景样式 bgWrap: { ...StyleSheet.absoluteFillObject, zIndex: 0, }, bgGradientFull: { position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, }, bgGradientCover: { position: 'absolute', left: '-10%', top: '-15%', width: '130%', height: '70%', borderBottomLeftRadius: 36, borderBottomRightRadius: 36, }, accentBlob: { position: 'absolute', width: 180, height: 180, borderRadius: 90, }, accentBlobLarge: { position: 'absolute', width: 260, height: 260, borderRadius: 130, }, accentBlobMedium: { position: 'absolute', width: 180, height: 180, borderRadius: 90, }, });