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

View File

@ -1,9 +1,9 @@
import { fetchApi } from "@/lib/server-api-util";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native";
import { ThemedText } from "../ThemedText";
import { View } from "react-native";
import { Steps } from "./phoneLogin";
import Button from "./ui/Button";
import TextInput from "./ui/TextInput";
interface LoginProps {
setSteps: (steps: Steps) => void;
@ -18,67 +18,30 @@ const Phone = ({ setSteps, setPhone, phone, updateUrlParam }: LoginProps) => {
const [error, setError] = useState<string>('');
const sendVerificationCode = async () => {
if (!/^1[3-9]\d{9}$/.test(phone)) {
setError(t("auth.telLogin.phoneInvalid", { ns: 'login' }));
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);
}
setSteps('code')
updateUrlParam("status", "code");
return
};
return <View>
{/* 手机号输入框 */}
<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
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' })}
placeholderTextColor="#ccc"
value={phone}
onChangeText={(text) => {
setPhone(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
value={phone}
error={error}
/>
</View>
{/* 发送验证码 */}
<TouchableOpacity
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>
<Button isLoading={isLoading} handleLogin={sendVerificationCode} text={t('auth.telLogin.sendCode', { ns: 'login' })} />
</View>
}

View File

@ -15,7 +15,9 @@ const PhoneLogin = ({ updateUrlParam }: LoginProps) => {
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 File

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

View File

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

View File

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