feat: 验证码组件

This commit is contained in:
jinyaqiu 2025-08-01 15:17:44 +08:00
parent 1f7a101fc5
commit f7146adf13

View File

@ -1,10 +1,11 @@
import Error from "@/assets/icons/svg/error.svg";
import { fetchApi } from "@/lib/server-api-util";
import { User } from "@/types/user";
import OTPInputView from '@twotalltotems/react-native-otp-input';
import { router } from "expo-router";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Animated, TextInput as RNTextInput, TextInput, TouchableOpacity, View } from "react-native";
import { Animated, TextInput as RNTextInput, StyleSheet, TouchableOpacity, View } from "react-native";
import { useAuth } from "../../contexts/auth-context";
import { ThemedText } from "../ThemedText";
@ -18,7 +19,7 @@ const Code = ({ phone }: CodeProps) => {
const [isLoading, setIsLoading] = useState(false);
const refs = useRef<Array<RNTextInput | null>>(Array(6).fill(null));
const shakeAnim = useRef(new Animated.Value(0)).current;
const [code, setCode] = useState<string[]>(['', '', '', '', '', '']);
const [code, setCode] = useState<string[]>([]);
const [error, setError] = useState<string>('');
const focusNext = (index: number, value: string) => {
@ -111,7 +112,7 @@ const Code = ({ phone }: CodeProps) => {
}, [countdown]);
return (
<View className="flex-1 bg-white px-6">
<View className="flex-1 bg-white px-2">
<View className="flex-1 justify-center">
<View className="items-center mb-8">
<ThemedText className="text-2xl font-semibold mb-2 text-gray-900">
@ -125,60 +126,28 @@ const Code = ({ phone }: CodeProps) => {
</ThemedText>
</View>
<Animated.View
style={{
transform: [{ translateX: shakeAnim }],
display: 'flex',
flexDirection: 'row',
gap: 24,
marginBottom: 16,
alignItems: 'center',
justifyContent: 'center',
<OTPInputView
pinCount={6} // 验证码长度
onCodeChanged={(code) => {
setCode([code]);
}}
>
{code.map((digit, index) => (
<TextInput
key={index}
ref={(ref) => {
if (ref) {
refs.current[index] = ref;
}
}}
style={{ width: 40, height: 40 }}
className="bg-[#FFF8DE] rounded-xl text-textTertiary text-3xl text-center"
keyboardType="number-pad"
maxLength={6}
value={digit}
onChangeText={text => handleCodeChange(text, index)}
onKeyPress={({ nativeEvent }) => focusPrevious(index, nativeEvent.key)}
selectTextOnFocus
caretHidden={true}
autoFocus={index === 0}
textContentType="oneTimeCode"
autoComplete="one-time-code"
importantForAutofill="yes"
/>
))}
</Animated.View>
onCodeFilled={(code) => {
handleTelLogin()
}}
code={code.join('')}
autoFocusOnLoad={false}
codeInputFieldStyle={styles.underlineStyleBase}
codeInputHighlightStyle={styles.underlineStyleHighLighted}
style={styles.otpContainer}
placeholderCharacter="-"
placeholderTextColor="#AC7E35"
/>
<View className={`w-full flex-row justify-end mb-[1rem] items-center ${error ? 'opacity-100' : 'opacity-0'}`}>
<Error />
<ThemedText className="text-base font-medium !text-buttonFill ml-2">
{error}
</ThemedText>
</View>
<TouchableOpacity
className="bg-buttonFill py-3 rounded-full items-center justify-center"
onPress={handleTelLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#ffffff" />
) : (
<ThemedText className="!text-white font-medium text-base">
{t("auth.telLogin.continue", { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
<View className="flex-row justify-center mt-4">
<ThemedText className="!text-textPrimary">
@ -201,5 +170,32 @@ const Code = ({ phone }: CodeProps) => {
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
otpContainer: {
width: '100%',
height: 100
},
underlineStyleBase: {
width: 50,
height: 50,
borderWidth: 0,
borderRadius: 16,
fontSize: 18,
color: '#000',
textAlign: 'center',
backgroundColor: '#FFF8DE'
},
underlineStyleHighLighted: {
borderColor: '#E2793F',
backgroundColor: '#FFF8DE',
borderWidth: 2,
},
});
export default Code