| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import CaptchaBox, { CaptchaRes } from '@/components/captcha-box';
- import { site } from '@/config.json';
- import { useInterval } from '@/hooks/hooks';
- import api, { ApiError } from '@/utils/api';
- import { signUp, 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, { useRef, useState } from 'react';
- import {
- KeyboardAvoidingView,
- Platform,
- Pressable,
- ScrollView,
- StyleSheet,
- Text,
- TextInput,
- View,
- } from 'react-native';
- import { useSafeAreaInsets } 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 SignUpScreen() {
- const [mobile, setMobile] = useState('');
- const [code, setCode] = useState('');
- const [password, setPassword] = useState('');
- const [rePass, setRepass] = useState('');
- const [email, setEmail] = useState('');
- const [name, setName] = useState('');
- const [organization, setOrganization] = useState('');
- const [agreed, setAgreed] = useState(false);
- const [flushAgree, setFlushAgree] = useState(false);
- const [loading, setLoading] = useState(false);
- const scrollView = useRef<ScrollView>(null);
- const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
- const [smsTtl, setSmsTtl] = useState(0);
- const { setToken } = useAuth();
- const { signIn, } = useLocalSearchParams<{ signIn: string; redirectTo?: string }>();
- const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
- const needsCaptchaRef = useRef(false);
- const handleCaptcha = (res: CaptchaRes) => {
- let fun = waitCaptcha.current;
- waitCaptcha.current = null;
- alert(1)
- setCaptchaVisible(false);
- fun?.(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<CaptchaRes>((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: 'register',
- captcha_code: captcha?.code,
- });
- if (res.needsCaptcha) {
- needsCaptchaRef.current = true;
- setCaptchaVisible(true);
- captcha = await new Promise<CaptchaRes>((resolve) => {
- waitCaptcha.current = resolve;
- });
- alert(JSON.stringify(captcha));
- if (!captcha || !captcha.ok) {
- return;
- }
- continue;
- }
- setSmsTtl(res.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;
- }
- }
- };
- // useEffect(() => {
- // let isMount = true;
- // setTimeout(()=> {
- // if (!isMount) {
- // return;
- // }
- // setSmsTtl((pre)=> pre --);
- // })
- // return ()=> {
- // isMount = false;
- // }
- // }, [])
- useInterval(() => {
- setSmsTtl((pre) => pre - 1)
- }, smsTtl > 0 ? 1000 : null);
- const handleRegister = async () => {
- setFlushAgree(false);
- if (mobile.trim().length !== 11) {
- Toast.fail('请输入正确的手机号');
- return;
- }
- if (code.trim().length !== 6) {
- Toast.fail('请输入 6 位验证码');
- return;
- }
- if (password.length < 6) {
- Toast.fail('请输入不少于 6 位的登录密码');
- return;
- }
- if (rePass !== password) {
- Toast.fail("两次密码输入不一至");
- return;
- }
- if (!email.includes('@')) {
- Toast.fail('请输入正确的邮箱');
- return;
- }
- if (!name.trim()) {
- Toast.fail('请输入姓名');
- return;
- }
- if (!agreed) {
- Toast.fail('请先阅读并同意协议');
- setFlushAgree(true);
- scrollView.current?.scrollToEnd({ animated: true });
- return;
- }
- setLoading(true);
- const toastKey = Toast.loading('正在创建账号...');
- try {
- const token = await signUp({
- mobile, password, captcha: code, name, email,
- });
- setToken(token);
- Toast.success('登录成功');
- if (signIn) {
- router.dismissTo("/");
- } else {
- router.dismiss();
- }
- } catch (error) {
- console.error('登录失败:', error);
- Toast.fail('登录失败,请稍后重试');
- } finally {
- setLoading(false);
- Toast.remove(toastKey);
- }
- };
- const insets = useSafeAreaInsets();
- return (
- <View className="flex-1 bg-surface">
- <KeyboardAvoidingView
- className="flex-1"
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
- >
- <ScrollView
- ref={scrollView}
- className="flex-1"
- contentContainerClassName="px-8"
- contentInset={{ top: (insets.top ?? 10) + 2, bottom: (insets.bottom ?? 8) + 4 }}
- 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="sparkles" 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="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={mobile}
- onChangeText={setMobile}
- 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>验证码</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={code}
- onChangeText={setCode}
- editable={!loading}
- keyboardType="number-pad"
- maxLength={6}
- placeholder="请输入 6 位验证码"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Pressable
- disabled={loading || smsTtl > 0}
- hitSlop={8}
- onPress={handleSendCode}
- >
- <Text className={`text-lg font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed' : 'text-primary'}`}>
- {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
- </Text>
- </Pressable>
- </View>
- <FieldLabel>登录密码</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={password}
- onChangeText={setPassword}
- editable={!loading}
- secureTextEntry
- maxLength={20}
- placeholder="请输入不少于 6 位的密码"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons name="lock-closed-outline" size={22} color="#c3c6d7" />
- </View>
- <FieldLabel>重复密码</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={rePass}
- onChangeText={setRepass}
- editable={!loading}
- secureTextEntry
- maxLength={20}
- placeholder="请再输入一次密码"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons name="lock-closed-outline" size={22} color="#c3c6d7" />
- </View>
- <FieldLabel>邮箱</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={email}
- onChangeText={setEmail}
- editable={!loading}
- keyboardType="email-address"
- autoCapitalize="none"
- placeholder="请输入常用邮箱"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons name="mail-outline" size={22} color="#c3c6d7" />
- </View>
- <FieldLabel>姓名</FieldLabel>
- <View className="mb-5 flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={name}
- onChangeText={setName}
- editable={!loading}
- placeholder="请输入姓名"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons name="person-outline" size={22} color="#c3c6d7" />
- </View>
- <FieldLabel>所属机构</FieldLabel>
- <View className="flex-row items-center rounded-2xl bg-surface-container-low px-5 py-4">
- <TextInput
- value={organization}
- onChangeText={setOrganization}
- editable={!loading}
- placeholder="请输入所属机构"
- placeholderTextColor="#9ca3af"
- className="flex-1 p-0 text-xl font-medium text-on-surface"
- />
- <Ionicons name="business-outline" size={22} color="#c3c6d7" />
- </View>
- </View>
- </View>
- <View className="mb-8 gap-4">
- <Button type="primary" loading={loading} onPress={handleRegister}>
- 注册账号
- </Button>
- <View className="flex-row items-center justify-between px-3">
- <Pressable hitSlop={8} onPress={() => router.back()}>
- <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={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${flushAgree ? ' border-primary/50' : 'border-transparent'}`}>
- <Pressable
- onPress={() => setAgreed((value) => !value)}
- hitSlop={8}
- className="pt-1"
- >
- <View
- className={`h-6 w-6 mt-2 items-center justify-center border-2 border-primary/50 rounded-full cursor-pointer ${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-6 text-on-surface-variant">
- 注册即代表您已阅读并同意
- <Link href={{
- pathname: '/web',
- params: { uri: site }
- }}>
- <Text className="font-semibold text-primary">《用户服务协议》</Text>
- </Link>
- 、
- <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《隐私政策》</Text>
- ,以及授权该应用获取您的公开信息。
- </Text>
- </View>
- </View>
- </View>
- </ScrollView>
- </KeyboardAvoidingView>
- <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
- </View>
- );
- }
|