import type { CaptchaRes } from '@/components/captcha-box'; import CaptchaBox from '@/components/captcha-box'; import { site } from '@/config.json'; import { useInterval } from '@/hooks/hooks'; import api, { ApiError } from '@/utils/api'; import { signIn, smsSignIn, useAuth } from '@/utils/auth'; import { Button, Toast } from '@ant-design/react-native'; import { Ionicons } from '@expo/vector-icons'; import { Link, router, useLocalSearchParams } from 'expo-router'; import { openBrowserAsync } from 'expo-web-browser'; import React, { useEffect, useRef, useState } from 'react'; import { KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import Animated, { interpolateColor, useAnimatedStyle, useSharedValue, withSequence, withTiming, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; function FieldLabel({ children }: { children: React.ReactNode }) { return ( {children} ); } export default function SignInScreen() { const [authMode, setAuthMode] = useState<'sms' | 'password'>('sms'); const [agreed, setAgreed] = useState(false); const [agreementHighlight, setFlushAgree] = useState(false); const [loading, setLoading] = useState(false); const [mobile, setMobile] = useState(''); const [code, setCode] = useState(''); const [password, setPassword] = useState(''); const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>(); const { setToken } = useAuth(); const scrollViewRef = useRef(null); const agreeHighlight = useSharedValue(0); useEffect(() => { if (agreementHighlight) { agreeHighlight.value = withSequence( withTiming(1, { duration: 140 }), withTiming(0, { duration: 140 }), withTiming(1, { duration: 140 }), withTiming(0, { duration: 140 }), withTiming(1, { duration: 200 }), ); } else { agreeHighlight.value = withTiming(0, { duration: 200 }); } }, [agreementHighlight, agreeHighlight]); const agreeBoxStyle = useAnimatedStyle(() => ({ borderColor: interpolateColor( agreeHighlight.value, [0, 1], ['rgba(37, 99, 235, 0)', 'rgba(37, 99, 235, 0.5)'], ), })); const [captchaVisible, setCaptchaVisible] = useState(false); const [smsTtl, setSmsTtl] = useState(0); const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null); const needsCaptchaRef = useRef(false); const handleCaptcha = (res: CaptchaRes) => { const resolver = waitCaptcha.current; waitCaptcha.current = null; setCaptchaVisible(false); resolver?.(res); }; const handleSendCode = async () => { if (mobile.trim().length !== 11) { Toast.fail('请先输入 11 位手机号'); return; } let captcha: CaptchaRes = null!; while (true) { if (needsCaptchaRef.current) { setCaptchaVisible(true); captcha = await new Promise((resolve) => { waitCaptcha.current = resolve; }); if (!captcha || !captcha.ok) { return; } } try { const res = await api.post<{ needsCaptcha?: boolean; timerout: number; }>(`sms/send?__session_id=${captcha?.sid ?? ''}`, { mobile: mobile, event: 'login', captcha_code: captcha?.code, }); if (res.needsCaptcha) { needsCaptchaRef.current = true; continue; } setSmsTtl(res.timerout); // 后端字段拼写为 timerout,保留以兼容接口 break; } catch (e) { if (ApiError.isApiError(e)) { if (e.code === -1) { Toast.fail("该手机已被注册"); return; } if (e.code === -99) { Toast.fail("图形验证码无效"); return; } } Toast.fail("发送验证码失败"); return; } } }; useInterval(() => { setSmsTtl((prev) => prev - 1) }, smsTtl > 0 ? 1000 : null); const handleLogin = async () => { setFlushAgree(false); scrollViewRef.current?.scrollTo({ y: 0, animated: true }); if (mobile.trim().length !== 11) { Toast.fail('请输入正确的手机号'); return; } if (authMode === 'sms' && code.trim().length !== 6) { Toast.fail('请输入 6 位验证码'); return; } if (authMode === 'password' && password.length < 6) { Toast.fail('请输入不少于 6 位的密码'); return; } if (!agreed) { scrollViewRef.current?.scrollToEnd({ animated: true }); setFlushAgree(true); Toast.fail('请先阅读并同意协议'); return; } setLoading(true); try { const token = authMode === 'sms' ? await smsSignIn(mobile, code) : await signIn(mobile, password); setToken(token); Toast.success('登录成功'); if (redirectTo) { router.replace(redirectTo as never); } else { router.canGoBack() ? router.back() : router.replace('/'); } } catch (error) { console.error('登录失败:', error); Toast.fail('登录失败,请稍后重试'); } finally { setLoading(false); } }; const insets = useSafeAreaInsets(); return ( 欢迎回来 登录贷款助手,开启您的 {[ ['sms', '验证码登录'], ['password', '密码登录'], ].map(([mode, label]) => { const active = authMode === mode; return ( setAuthMode(mode as 'sms' | 'password')} className={`flex-1 rounded-xl px-4 py-3 ${active ? 'bg-surface-container-lowest' : '' }`} style={({ pressed }) => ({ opacity: loading ? 0.5 : pressed ? 0.86 : 1, transform: [{ scale: pressed ? 0.99 : 1 }], })} > {label} ); })} 手机号码 +86 {authMode === 'sms' ? '验证码' : '密码'} {authMode === 'sms' ? ( 0} hitSlop={8} onPress={handleSendCode} > 0 ? 'text-on-secondary-fixed/50' : 'text-primary'}`}> {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'} ) : ( )} router.push({ pathname: '/sign-up', params: { signIn: 1, redirectTo } })}> 注册账号 遇到问题? 其他方式登录 {[ ['chatbubble-ellipses-outline', '#6b7280'], ['logo-apple', '#6b7280'], ].map(([icon, color]) => ( ({ opacity: pressed ? 0.8 : 1, transform: [{ scale: pressed ? 0.94 : 1 }], })} > ))} setAgreed((value) => !value)} hitSlop={8} className="pt-1" > {agreed ? ( ) : null} 登录即代表您已阅读并同意 《用户服务协议》 openBrowserAsync(`${site}`)}>《隐私政策》 ,以及授权该应用获取您的公开信息。 {loading && } ); }