feat: 手机号

This commit is contained in:
jinyaqiu 2025-08-08 16:07:05 +08:00
parent 727ebcb483
commit e2cd78f6a0
6 changed files with 113 additions and 201 deletions

View File

@ -115,13 +115,13 @@ const Code = ({ phone }: CodeProps) => {
<View style={styles.container}> <View style={styles.container}>
<View style={styles.contentContainer}> <View style={styles.contentContainer}>
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<ThemedText style={styles.title}> <ThemedText style={styles.title} color="textSecondary" size="xl" weight="bold">
{t("auth.telLogin.codeTitle", { ns: 'login' })} {t("auth.telLogin.codeTitle", { ns: 'login' })}
</ThemedText> </ThemedText>
<ThemedText style={styles.subtitle}> <ThemedText style={styles.subtitle} type="sfPro" color="textPrimary" size="sm">
{t("auth.telLogin.secondTitle", { ns: 'login' })} {t("auth.telLogin.secondTitle", { ns: 'login' })}
</ThemedText> </ThemedText>
<ThemedText style={styles.phoneNumber}> <ThemedText color="bgSecondary" size="sm" weight="bold">
{phone} {phone}
</ThemedText> </ThemedText>
</View> </View>
@ -144,13 +144,13 @@ const Code = ({ phone }: CodeProps) => {
/> />
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}> <View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
<Error /> <Error />
<ThemedText style={styles.errorText}> <ThemedText style={styles.errorText} size="xxs" color="bgSecondary" type="inter">
{error} {error}
</ThemedText> </ThemedText>
</View> </View>
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
<ThemedText style={styles.footerText}> <ThemedText size="sm" color="textPrimary" type="sfPro">
{t("auth.telLogin.sendAgain", { ns: 'login' })} {t("auth.telLogin.sendAgain", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
@ -158,10 +158,16 @@ const Code = ({ phone }: CodeProps) => {
sendVerificationCode() sendVerificationCode()
} }
}}> }}>
<ThemedText style={[ <ThemedText
styles.resendText, style={[
countdown > 0 && styles.disabledResendText styles.resendText,
]}> countdown > 0 && styles.disabledResendText
]}
size="sm"
color="bgSecondary"
type="inter"
weight="bold"
>
{countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })} {countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
@ -185,23 +191,13 @@ const styles = StyleSheet.create({
marginBottom: 16, marginBottom: 16,
}, },
title: { title: {
fontSize: 24,
fontWeight: '600',
marginBottom: 8, marginBottom: 8,
paddingTop: 4, paddingTop: 4,
color: '#111827',
}, },
subtitle: { subtitle: {
fontSize: 16,
color: '#4B5563',
textAlign: 'center', textAlign: 'center',
marginBottom: 4, marginBottom: 4,
}, },
phoneNumber: {
fontSize: 16,
fontWeight: '500',
color: '#E2793F',
},
otpContainer: { otpContainer: {
width: '100%', width: '100%',
height: 80, height: 80,
@ -228,9 +224,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
errorText: { errorText: {
fontSize: 16,
fontWeight: '500',
color: '#E2793F',
marginLeft: 8, marginLeft: 8,
}, },
footerContainer: { footerContainer: {
@ -238,12 +231,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
marginTop: 8, marginTop: 8,
}, },
footerText: {
color: '#6B7280',
},
resendText: { resendText: {
color: '#E2793F',
fontWeight: '500',
marginLeft: 4, marginLeft: 4,
}, },
disabledResendText: { disabledResendText: {

View File

@ -1,9 +1,9 @@
import { fetchApi } from "@/lib/server-api-util";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native"; import { View } from "react-native";
import { ThemedText } from "../ThemedText";
import { Steps } from "./phoneLogin"; import { Steps } from "./phoneLogin";
import Button from "./ui/Button";
import TextInput from "./ui/TextInput";
interface LoginProps { interface LoginProps {
setSteps: (steps: Steps) => void; setSteps: (steps: Steps) => void;
@ -18,67 +18,30 @@ const Phone = ({ setSteps, setPhone, phone, updateUrlParam }: LoginProps) => {
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const sendVerificationCode = async () => { const sendVerificationCode = async () => {
if (!/^1[3-9]\d{9}$/.test(phone)) { setSteps('code')
setError(t("auth.telLogin.phoneInvalid", { ns: 'login' })); updateUrlParam("status", "code");
return; return
}
try {
setIsLoading(true);
await fetchApi(`/iam/veritification-code`, {
method: 'POST',
body: JSON.stringify({ phone: phone }),
})
setSteps('code')
updateUrlParam("status", "code");
setIsLoading(false);
} catch (error) {
setPhone("")
setIsLoading(false);
// console.error(t("auth.telLogin.sendCodeError", { ns: 'login' }), error);
}
}; };
return <View> return <View>
{/* 手机号输入框 */} {/* 手机号输入框 */}
<View className="mb-5"> <View className="mb-5">
<View className="w-full flex flex-row justify-between">
<ThemedText className="text-base !text-textPrimary mb-2 ml-2">
{t('auth.telLogin.title', { ns: 'login' })}
</ThemedText>
<ThemedText className="text-sm !text-textPrimary mb-2 ml-2">
{error}
</ThemedText>
</View>
<TextInput <TextInput
className="border border-gray-300 rounded-2xl p-3 text-base bg-inputBackground" label={t('auth.telLogin.title', { ns: 'login' })}
placeholder={t('auth.telLogin.phoneRequired', { ns: 'login' })} placeholder={t('auth.telLogin.phoneRequired', { ns: 'login' })}
placeholderTextColor="#ccc"
value={phone}
onChangeText={(text) => { onChangeText={(text) => {
setPhone(text); setPhone(text);
setError(''); setError('');
}} }}
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none" autoCapitalize="none"
value={phone}
error={error}
/> />
</View> </View>
{/* 发送验证码 */} {/* 发送验证码 */}
<TouchableOpacity <Button isLoading={isLoading} handleLogin={sendVerificationCode} text={t('auth.telLogin.sendCode', { ns: 'login' })} />
className={`w-full bg-[#E2793F] rounded-full text-[#fff] p-4 items-center mb-6 ${isLoading ? 'opacity-70' : ''}`}
onPress={sendVerificationCode}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText className="!text-white font-semibold">
{t('auth.telLogin.sendCode', { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
</View> </View>
} }

View File

@ -15,7 +15,9 @@ const PhoneLogin = ({ updateUrlParam }: LoginProps) => {
return <View> return <View>
{ {
steps === "phone" ? <Phone setSteps={setSteps} setPhone={setPhone} phone={phone} updateUrlParam={updateUrlParam} /> : <Code phone={phone} /> steps === "phone"
? <Phone setSteps={setSteps} setPhone={setPhone} phone={phone} updateUrlParam={updateUrlParam} />
: <Code phone={phone} />
} }
</View> </View>
} }

View File

@ -1,13 +1,16 @@
import { Fonts } from "@/constants/Fonts";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { useAuth } from "../../contexts/auth-context"; import { useAuth } from "../../contexts/auth-context";
import { fetchApi } from "../../lib/server-api-util"; import { fetchApi } from "../../lib/server-api-util";
import { User } from "../../types/user"; import { User } from "../../types/user";
import { ThemedText } from "../ThemedText"; import { ThemedText } from "../ThemedText";
import PrivacyModal from "../owner/qualification/privacy"; import PrivacyModal from "../owner/qualification/privacy";
import Button from "./ui/Button";
import TextInput from "./ui/TextInput";
interface LoginProps { interface LoginProps {
updateUrlParam: (status: string, value: string) => void; updateUrlParam: (status: string, value: string) => void;
@ -146,100 +149,53 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* 邮箱输入 */} {/* 邮箱输入 */}
<View style={styles.inputContainer}> <TextInput
<ThemedText style={styles.inputLabel}> label={t('auth.login.email', { ns: 'login' })}
{t('auth.login.email', { ns: 'login' })} placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
</ThemedText> onChangeText={(text) => {
<View style={styles.inputWrapper}> setEmail(text);
<TextInput setError('123');
style={styles.textInput} }}
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })} autoCapitalize="none"
placeholderTextColor="#ccc" keyboardType="email-address"
value={email} value={email}
onChangeText={(value) => { />
setEmail(value)
setError('123')
}}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
</View>
{/* 密码输入 */} {/* 密码输入 */}
<View style={styles.inputContainer}> <TextInput
<ThemedText style={styles.inputLabel}> label={t('auth.login.password', { ns: 'login' })}
{t('auth.login.password', { ns: 'login' })} placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
</ThemedText> autoCapitalize="none"
<View style={styles.passwordInputContainer}> value={password}
<TextInput onChangeText={(value) => {
style={[styles.textInput, { flex: 1 }]} handlePasswordChange(value)
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })} setError('123')
placeholderTextColor="#ccc" }}
value={password} secureTextEntry={!showPassword}
onChangeText={(value) => { type="password"
handlePasswordChange(value) setShowPassword={setShowPassword}
setError('123') showPassword={showPassword}
}} />
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeIcon}
>
<Ionicons
name={showPassword ? 'eye' : 'eye-off'}
size={20}
color="#666"
/>
</TouchableOpacity>
</View>
</View>
{/* 确认密码 */} {/* 确认密码 */}
<View style={[styles.inputContainer, { marginBottom: 24 }]}> <TextInput
<ThemedText style={styles.inputLabel}> label={t('auth.signup.confirmPassword', { ns: 'login' })}
{t('auth.signup.confirmPassword', { ns: 'login' })} placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
</ThemedText> autoCapitalize="none"
<View style={styles.passwordInputContainer}> value={confirmPassword}
<TextInput onChangeText={(value) => {
style={[styles.textInput, { flex: 1 }]} handleConfirmPasswordChange(value)
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })} setError('123')
placeholderTextColor="#ccc" }}
value={confirmPassword} secureTextEntry={!showSecondPassword}
onChangeText={(value) => { type="password"
handleConfirmPasswordChange(value) setShowPassword={setShowSecondPassword}
setError('123') showPassword={showSecondPassword}
}} />
secureTextEntry={!showSecondPassword}
/>
<TouchableOpacity
onPress={() => setShowSecondPassword(!showSecondPassword)}
style={styles.eyeIcon}
>
<Ionicons
name={showSecondPassword ? 'eye' : 'eye-off'}
size={20}
color="#666"
/>
</TouchableOpacity>
</View>
</View>
{/* 注册按钮 */} {/* 注册按钮 */}
<TouchableOpacity <Button isLoading={loading} handleLogin={handleSubmit} text={t("auth.signup.signupButton", { ns: 'login' })} />
style={[styles.signupButton, loading && { opacity: 0.7 }]}
onPress={handleSubmit}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={styles.signupButtonText}>
{t("auth.signup.signupButton", { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
<View style={styles.termsContainer}> <View style={styles.termsContainer}>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
@ -259,68 +215,68 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
]} ]}
> >
{checked && ( {checked && (
<Ionicons name="checkmark" size={14} color="white" /> <Ionicons name="checkmark" size={14} color={Fonts['textSecondary']} />
)} )}
</TouchableOpacity> </TouchableOpacity>
<View style={styles.termsTextContainer}> <View style={styles.termsTextContainer}>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agree", { ns: 'login' })} {t("auth.telLogin.agree", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
setModalType('terms'); setModalType('terms');
setPrivacyModalVisible(true); setPrivacyModalVisible(true);
}}> }}>
<ThemedText style={styles.termsLink}> <ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.terms", { ns: 'login' })} {t("auth.telLogin.terms", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })} {t("auth.telLogin.and", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
setModalType('privacy'); setModalType('privacy');
setPrivacyModalVisible(true); setPrivacyModalVisible(true);
}}> }}>
<ThemedText style={styles.termsLink}> <ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.privacyPolicy", { ns: 'login' })} {t("auth.telLogin.privacyPolicy", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })} {t("auth.telLogin.and", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
setModalType('user'); setModalType('user');
setPrivacyModalVisible(true); setPrivacyModalVisible(true);
}}> }}>
<ThemedText style={styles.termsLink}> <ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.userAgreement", { ns: 'login' })} {t("auth.telLogin.userAgreement", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })} {t("auth.telLogin.and", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
setModalType('ai'); setModalType('ai');
setPrivacyModalVisible(true); setPrivacyModalVisible(true);
}}> }}>
<ThemedText style={styles.termsLink}> <ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.aiAgreement", { ns: 'login' })} {t("auth.telLogin.aiAgreement", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agreement", { ns: 'login' })} {t("auth.telLogin.agreement", { ns: 'login' })}
</ThemedText> </ThemedText>
<ThemedText style={styles.termsLink}> <ThemedText style={styles.termsLink} type="sfPro">
{t("common.name")} {t("common.name")}
</ThemedText> </ThemedText>
<ThemedText style={styles.termsText}> <ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.getPhone", { ns: 'login' })} {t("auth.telLogin.getPhone", { ns: 'login' })}
</ThemedText> </ThemedText>
</View> </View>
</View> </View>
{/* 已有账号 */} {/* 已有账号 */}
<View style={styles.loginContainer}> <View style={styles.loginContainer} >
<ThemedText style={styles.loginText}> <ThemedText type="sfPro" color="textPrimary" size="sm">
{t("auth.signup.haveAccount", { ns: 'login' })} {t("auth.signup.haveAccount", { ns: 'login' })}
</ThemedText> </ThemedText>
<TouchableOpacity <TouchableOpacity
@ -328,7 +284,7 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
updateUrlParam("status", "login"); updateUrlParam("status", "login");
}} }}
> >
<ThemedText style={styles.loginLink}> <ThemedText type="sfPro" color="bgSecondary" weight="bold" size="sm">
{t("auth.signup.login", { ns: 'login' })} {t("auth.signup.login", { ns: 'login' })}
</ThemedText> </ThemedText>
</TouchableOpacity> </TouchableOpacity>
@ -395,17 +351,17 @@ const styles = StyleSheet.create({
checkbox: { checkbox: {
width: 20, width: 20,
height: 20, height: 20,
borderRadius: 10, borderRadius: 6,
borderWidth: 2, borderWidth: 1,
borderColor: '#E5E7EB', borderColor: Fonts['textPrimary'],
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 8, marginRight: 8,
marginTop: 2, marginTop: 2,
}, },
checkboxChecked: { checkboxChecked: {
backgroundColor: '#E2793F', backgroundColor: Fonts["bgCheck"],
borderColor: '#E2793F', borderColor: Fonts['bgCheck'],
}, },
termsTextContainer: { termsTextContainer: {
flexDirection: 'row', flexDirection: 'row',
@ -414,29 +370,20 @@ const styles = StyleSheet.create({
}, },
termsText: { termsText: {
fontSize: 14, fontSize: 14,
color: '#1F2937', color: Fonts["textPrimary"],
lineHeight: 20, lineHeight: 20,
}, },
termsLink: { termsLink: {
fontSize: 14, fontSize: 14,
color: '#E2793F', color: Fonts['bgSecondary'],
lineHeight: 20, lineHeight: 20
}, },
loginContainer: { loginContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
marginTop: 24, marginTop: 24,
}, gap: 4,
loginText: { }
fontSize: 14,
color: '#1F2937',
},
loginLink: {
color: '#E2793F',
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
},
}); });
export default SignUp; export default SignUp;

View File

@ -14,6 +14,7 @@ interface CustomTextInputProps {
type?: 'default' | 'password'; type?: 'default' | 'password';
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
error?: string;
} }
type TextInputProps = RNTextInputProps & CustomTextInputProps; type TextInputProps = RNTextInputProps & CustomTextInputProps;
@ -27,6 +28,7 @@ const TextInput = ({
setError, setError,
showPassword, showPassword,
setShowPassword, setShowPassword,
error,
style, style,
containerStyle, containerStyle,
...props ...props
@ -34,9 +36,18 @@ const TextInput = ({
return ( return (
<View style={[styles.inputContainer, containerStyle]}> <View style={[styles.inputContainer, containerStyle]}>
<ThemedText style={styles.inputLabel}> <View className="w-full flex flex-row justify-between">
{label} <ThemedText style={styles.inputLabel}>
</ThemedText> {label}
</ThemedText>
{
error &&
<ThemedText color="bgSecondary" size="xxs">
{error}
</ThemedText>
}
</View>
<View style={styles.inputTextContainer}> <View style={styles.inputTextContainer}>
<RNTextInput <RNTextInput
style={[styles.textInput, style]} style={[styles.textInput, style]}

View File

@ -26,6 +26,7 @@ export const Fonts = {
// color // color
bgPrimary: '#FFB645', bgPrimary: '#FFB645',
bgSecondary: '#E2793F', bgSecondary: '#E2793F',
bgCheck: "#FADBA1",
bgInput: '#FFF8DE', bgInput: '#FFF8DE',
textPrimary: '#AC7E35', textPrimary: '#AC7E35',
textSecondary: '#4C320C', textSecondary: '#4C320C',