| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- import { signIn, useAuthContext } from '@/utils/auth';
- import { Toast } from '@ant-design/react-native';
- import { Ionicons } from '@expo/vector-icons';
- import { router, useLocalSearchParams } from 'expo-router';
- import React, { useState } from 'react';
- import {
- KeyboardAvoidingView,
- Platform,
- Pressable,
- ScrollView,
- StyleSheet,
- Text,
- TextInput,
- View,
- } from 'react-native';
- import { SafeAreaView } from 'react-native-safe-area-context';
- function FieldLabel({ children }: { children: React.ReactNode }) {
- return (
- <Text className="mb-2 ml-1 text-xs font-bold uppercase tracking-widest text-outline">
- {children}
- </Text>
- );
- }
- export default function SignInScreen({
- callback,
- }: {
- callback?: (signin: boolean) => void;
- }) {
- const [authMode, setAuthMode] = useState<'sms' | 'password'>('sms');
- const [agreed, setAgreed] = useState(false);
- const [loading, setLoading] = useState(false);
- const [phone, setPhone] = useState('');
- const [code, setCode] = useState('');
- const [password, setPassword] = useState('');
- const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>();
- const { setToken } = useAuthContext();
- const handleSendCode = () => {
- if (phone.trim().length !== 11) {
- Toast.fail('请先输入 11 位手机号');
- return;
- }
- Toast.success('验证码已发送');
- };
- const handleLogin = async () => {
- if (!agreed) {
- Toast.fail('请先阅读并同意协议');
- callback?.(false);
- return;
- }
- if (phone.trim().length !== 11) {
- Toast.fail('请输入正确的手机号');
- callback?.(false);
- return;
- }
- if (authMode === 'sms' && code.trim().length !== 6) {
- Toast.fail('请输入 6 位验证码');
- callback?.(false);
- return;
- }
- if (authMode === 'password' && password.trim().length < 6) {
- Toast.fail('请输入不少于 6 位的密码');
- callback?.(false);
- return;
- }
- setLoading(true);
- const toastKey = Toast.loading('正在登录...');
- try {
- const token = await signIn(1);
- setToken(token);
- Toast.success('登录成功');
- callback?.(true);
- const nextRoute =
- typeof redirectTo === 'string' && redirectTo.startsWith('/')
- ? redirectTo
- : '/';
- router.replace(nextRoute as never);
- } catch (error) {
- console.error('登录失败:', error);
- Toast.fail('登录失败,请稍后重试');
- callback?.(false);
- } finally {
- setLoading(false);
- Toast.remove(toastKey);
- }
- };
- return (
- <SafeAreaView className="flex-1 bg-surface">
- <KeyboardAvoidingView
- className="flex-1"
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
- >
- <ScrollView
- className="flex-1"
- contentContainerClassName="px-8 pt-12 pb-10"
- contentContainerStyle={{ flexGrow: 1 }}
- keyboardShouldPersistTaps="handled"
- showsVerticalScrollIndicator={false}
- >
- <View className="absolute -top-24 -right-24 h-96 w-96 rounded-full bg-primary-container/10" />
- <View className="absolute top-32 -left-20 h-64 w-64 rounded-full bg-secondary-container/20" />
- <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
- <View className="flex-1 justify-between">
- <View>
- <View className="mb-12">
- <View className="mb-6 h-16 w-16 items-center justify-center rounded-2xl bg-primary-container shadow-lg">
- <Ionicons name="business" size={30} color="#ffffff" />
- </View>
- <Text className="mb-2 text-4xl font-extrabold tracking-tight text-on-surface">
- 欢迎回来
- </Text>
- <Text className="text-base font-medium leading-7 text-on-surface-variant">
- 登录贷款助手,开启您的财务管理之旅
- </Text>
- </View>
- <View className="mb-8 rounded-2xl bg-surface-container-lowest p-3 shadow-sm">
- <View className="mb-6 flex-row rounded-2xl bg-surface-container-low p-1">
- {[
- ['sms', '验证码登录'],
- ['password', '密码登录'],
- ].map(([mode, label]) => {
- const active = authMode === mode;
- return (
- <Pressable
- key={mode}
- disabled={loading}
- onPress={() => 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 }],
- })}
- >
- <Text
- className={`text-center text-base font-bold ${
- active ? 'text-primary' : 'text-on-surface-variant'
- }`}
- >
- {label}
- </Text>
- </Pressable>
- );
- })}
- </View>
- <View className="px-2 pb-2">
- <FieldLabel>手机号码</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <Text className="text-2xl font-bold text-on-surface">+86</Text>
- <View
- className="mx-4 h-5 bg-outline-variant/40"
- style={{ width: StyleSheet.hairlineWidth }}
- />
- <TextInput
- value={phone}
- onChangeText={setPhone}
- editable={!loading}
- keyboardType="phone-pad"
- maxLength={11}
- placeholder="请输入手机号"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons
- name="phone-portrait-outline"
- size={22}
- color="#c3c6d7"
- />
- </View>
- <FieldLabel>{authMode === 'sms' ? '验证码' : '密码'}</FieldLabel>
- <View className="flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={authMode === 'sms' ? code : password}
- onChangeText={authMode === 'sms' ? setCode : setPassword}
- editable={!loading}
- keyboardType={authMode === 'sms' ? 'number-pad' : 'default'}
- maxLength={authMode === 'sms' ? 6 : 20}
- secureTextEntry={authMode === 'password'}
- placeholder={
- authMode === 'sms'
- ? '请输入 6 位验证码'
- : '请输入登录密码'
- }
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- {authMode === 'sms' ? (
- <Pressable
- disabled={loading}
- hitSlop={8}
- onPress={handleSendCode}
- style={({ pressed }) => ({
- opacity: loading ? 0.5 : pressed ? 0.72 : 1,
- })}
- >
- <Text className="text-lg font-bold leading-6 text-primary">
- 获取验证码
- </Text>
- </Pressable>
- ) : (
- <Ionicons name="lock-closed-outline" size={22} color="#c3c6d7" />
- )}
- </View>
- </View>
- </View>
- <View className="mb-8 gap-4">
- <Pressable
- disabled={loading}
- onPress={handleLogin}
- className="items-center justify-center rounded-2xl bg-primary-container py-5 shadow-lg"
- style={({ pressed }) => ({
- opacity: loading ? 0.6 : pressed ? 0.88 : 1,
- transform: [{ scale: pressed ? 0.985 : 1 }],
- })}
- >
- <Text className="text-2xl font-extrabold text-on-primary">
- {loading ? '登录中...' : '立即登录'}
- </Text>
- </Pressable>
- <View className="flex-row items-center justify-between px-3">
- <Pressable hitSlop={8}>
- <Text className="text-lg font-medium text-on-surface-variant">
- 注册账号
- </Text>
- </Pressable>
- <Pressable hitSlop={8}>
- <Text className="text-lg font-medium text-on-surface-variant">
- 遇到问题?
- </Text>
- </Pressable>
- </View>
- </View>
- </View>
- <View className="pt-4">
- <View className="mb-8 flex-row items-center gap-4">
- <View className="h-px flex-1 bg-surface-container-highest" />
- <Text className="text-sm font-bold tracking-widest text-outline">
- 其他方式登录
- </Text>
- <View className="h-px flex-1 bg-surface-container-highest" />
- </View>
- <View className="mb-10 flex-row justify-center gap-10">
- {[
- ['chatbubble-ellipses-outline', '#6b7280'],
- ['logo-apple', '#6b7280'],
- ].map(([icon, color]) => (
- <Pressable
- key={icon}
- className="h-14 w-14 items-center justify-center rounded-full bg-surface-container-low"
- style={({ pressed }) => ({
- opacity: pressed ? 0.8 : 1,
- transform: [{ scale: pressed ? 0.94 : 1 }],
- })}
- >
- <Ionicons
- name={icon as keyof typeof Ionicons.glyphMap}
- size={24}
- color={color}
- />
- </Pressable>
- ))}
- </View>
- <View className="flex-row items-start gap-3 px-2">
- <Pressable
- onPress={() => setAgreed((value) => !value)}
- hitSlop={8}
- className="pt-1"
- >
- <View
- className={`h-6 w-6 items-center justify-center rounded-full border ${
- agreed
- ? 'border-primary bg-primary'
- : 'border-outline-variant bg-surface-container-low'
- }`}
- >
- {agreed ? (
- <Ionicons name="checkmark" size={14} color="#ffffff" />
- ) : null}
- </View>
- </Pressable>
- <Text className="flex-1 text-sm leading-7 text-on-surface-variant">
- 登录即代表您已阅读并同意
- <Text className="font-semibold text-primary">《用户服务协议》</Text>
- 、
- <Text className="font-semibold text-primary">《隐私政策》</Text>
- ,以及授权该应用获取您的公开信息。
- </Text>
- </View>
- </View>
- </View>
- </ScrollView>
- </KeyboardAvoidingView>
- </SafeAreaView>
- );
- }
|