Files
frontend/src/screens/auth/RegisterScreen.tsx

618 lines
19 KiB
TypeScript
Raw Normal View History

/**
* RegisterScreen
* BBS -
*
*
* -
* -
* -
* -
*/
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
KeyboardAvoidingView,
Platform,
ScrollView,
ActivityIndicator,
Animated,
2026-03-16 17:47:10 +08:00
StatusBar,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import { authService, resolveAuthApiError } from '../../services/authService';
import { useAuthStore } from '../../stores';
import * as hrefs from '../../navigation/hrefs';
import { showPrompt } from '../../services/promptService';
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
2026-03-16 17:47:10 +08:00
export const RegisterScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
const router = useRouter();
const register = useAuthStore((state) => state.register);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
useEffect(() => {
if (isAuthenticated) {
router.replace(hrefs.hrefHome());
}
}, [isAuthenticated, router]);
const [username, setUsername] = useState('');
const [nickname, setNickname] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [verificationCode, setVerificationCode] = useState('');
const [loading, setLoading] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(true);
// 动画值
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(20)).current;
// 启动入场动画
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]).start();
}, []);
useEffect(() => {
if (countdown <= 0) {
return;
}
const timer = setInterval(() => {
setCountdown((prev) => (prev <= 1 ? 0 : prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [countdown]);
// 表单验证
const validateForm = (): boolean => {
if (!username.trim()) {
showPrompt({ title: '提示', message: '请输入用户名', type: 'warning' });
return false;
}
if (username.length < 3 || username.length > 20) {
showPrompt({ title: '提示', message: '用户名长度需在3-20个字符之间', type: 'warning' });
return false;
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
showPrompt({ title: '提示', message: '用户名只能包含字母、数字和下划线', type: 'warning' });
return false;
}
if (!nickname.trim()) {
showPrompt({ title: '提示', message: '请输入昵称', type: 'warning' });
return false;
}
if (nickname.length < 2 || nickname.length > 20) {
showPrompt({ title: '提示', message: '昵称长度需在2-20个字符之间', type: 'warning' });
return false;
}
if (!email.trim()) {
showPrompt({ title: '提示', message: '请输入邮箱', type: 'warning' });
return false;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
return false;
}
if (!verificationCode.trim()) {
showPrompt({ title: '提示', message: '请输入邮箱验证码', type: 'warning' });
return false;
}
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
showPrompt({ title: '提示', message: '请输入正确的手机号', type: 'warning' });
return false;
}
if (!password) {
showPrompt({ title: '提示', message: '请输入密码', type: 'warning' });
return false;
}
if (password.length < 6) {
showPrompt({ title: '提示', message: '密码长度不能少于6位', type: 'warning' });
return false;
}
if (password !== confirmPassword) {
showPrompt({ title: '提示', message: '两次输入的密码不一致', type: 'warning' });
return false;
}
if (!agreedToTerms) {
showPrompt({ title: '提示', message: '请同意用户协议和隐私政策', type: 'warning' });
return false;
}
return true;
};
const handleSendCode = async () => {
if (!email.trim()) {
showPrompt({ title: '提示', message: '请先输入邮箱', type: 'warning' });
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
return;
}
if (countdown > 0) {
return;
}
setSendingCode(true);
try {
const ok = await authService.sendRegisterCode(email.trim());
if (ok) {
setCountdown(60);
showPrompt({ title: '发送成功', message: '验证码已发送,请查收邮箱', type: 'success' });
}
} catch (error: any) {
showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'), type: 'error' });
} finally {
setSendingCode(false);
}
};
// 处理注册
const handleRegister = async () => {
if (!validateForm()) return;
setLoading(true);
try {
const success = await register({
username,
password,
nickname,
email: email.trim(),
verification_code: verificationCode.trim(),
phone: phone || undefined,
});
if (success) {
// 注册成功,导航会自动切换到主页
} else {
const msg = useAuthStore.getState().error || '注册失败,请稍后重试';
showPrompt({ title: '注册失败', message: msg, type: 'error' });
}
} catch (error: any) {
showPrompt({ title: '注册失败', message: resolveAuthApiError(error, '请稍后重试'), type: 'error' });
} finally {
setLoading(false);
}
};
// 跳转到登录页
const handleGoToLogin = () => {
router.push(hrefs.hrefAuthLogin());
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<Animated.View
style={[
styles.content,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
{/* 标题区域 - 增大间距 */}
<View style={styles.titleSection}>
<Text style={styles.title}></Text>
<View style={styles.underline} />
<Text style={styles.subtitle}></Text>
</View>
{/* 表单区域 */}
<View style={styles.formSection}>
{/* 用户名输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="3-20个字符字母数字下划线"
placeholderTextColor={colors.text.hint}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
maxLength={20}
/>
{username.length > 0 && /^[a-zA-Z0-9_]{3,20}$/.test(username) && (
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
)}
</View>
</View>
{/* 昵称输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="2-20个字符"
placeholderTextColor={colors.text.hint}
value={nickname}
onChangeText={setNickname}
maxLength={20}
/>
{nickname.length > 0 && nickname.length >= 2 && (
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
)}
</View>
</View>
{/* 邮箱输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="请输入邮箱地址"
placeholderTextColor={colors.text.hint}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
/>
{email.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && (
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
)}
</View>
</View>
{/* 邮箱验证码 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.codeRow}>
<View style={[styles.inputWrapper, styles.codeInputWrapper]}>
<TextInput
style={styles.input}
placeholder="6位验证码"
placeholderTextColor={colors.text.hint}
value={verificationCode}
onChangeText={setVerificationCode}
keyboardType="number-pad"
maxLength={6}
/>
</View>
<TouchableOpacity
style={[
styles.sendCodeButton,
(sendingCode || countdown > 0) && styles.sendCodeButtonDisabled,
]}
onPress={handleSendCode}
disabled={sendingCode || countdown > 0}
activeOpacity={0.8}
>
<Text style={styles.sendCodeButtonText}>
{sendingCode ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码'}
</Text>
</TouchableOpacity>
</View>
</View>
{/* 手机号输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="请输入手机号"
placeholderTextColor={colors.text.hint}
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
maxLength={11}
/>
</View>
</View>
{/* 密码输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="至少6位"
placeholderTextColor={colors.text.hint}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeButton}
>
<MaterialCommunityIcons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={20}
color={colors.text.hint}
/>
</TouchableOpacity>
</View>
</View>
{/* 确认密码输入框 */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="再次输入密码"
placeholderTextColor={colors.text.hint}
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry={!showConfirmPassword}
autoCapitalize="none"
returnKeyType="done"
onSubmitEditing={handleRegister}
/>
<TouchableOpacity
onPress={() => setShowConfirmPassword(!showConfirmPassword)}
style={styles.eyeButton}
>
<MaterialCommunityIcons
name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'}
size={20}
color={colors.text.hint}
/>
</TouchableOpacity>
</View>
</View>
{/* 服务条款 */}
<TouchableOpacity
style={styles.termsContainer}
onPress={() => setAgreedToTerms(!agreedToTerms)}
activeOpacity={0.8}
>
<MaterialCommunityIcons
name={agreedToTerms ? "checkbox-marked" : "checkbox-blank-outline"}
size={22}
color={agreedToTerms ? THEME_COLORS.primary : colors.text.hint}
/>
<Text style={styles.termsText}>
<Text style={styles.termsLink}></Text>
<Text style={styles.termsLink}></Text>
</Text>
</TouchableOpacity>
{/* 注册按钮 */}
<TouchableOpacity
style={[styles.registerButton, loading && styles.registerButtonDisabled]}
onPress={handleRegister}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
<Text style={styles.registerButtonText}> </Text>
)}
</TouchableOpacity>
</View>
{/* 底部登录提示 */}
<View style={styles.footer}>
<Text style={styles.footerText}></Text>
<TouchableOpacity onPress={handleGoToLogin}>
<Text style={styles.loginLink}></Text>
</TouchableOpacity>
</View>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
function createRegisterStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
keyboardView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 28,
paddingTop: 50,
paddingBottom: 50,
},
content: {
flex: 1,
maxWidth: 400,
width: '100%',
alignSelf: 'center',
},
// 标题区域 - 增大间距
titleSection: {
marginBottom: 40,
marginTop: 10,
},
title: {
fontSize: 32,
fontWeight: '700',
color: colors.text.primary,
lineHeight: 40,
},
underline: {
width: 40,
height: 4,
backgroundColor: THEME_COLORS.primary,
borderRadius: 2,
marginTop: 12,
marginBottom: 16,
},
subtitle: {
fontSize: 16,
color: colors.text.secondary,
},
// 表单区域
formSection: {
marginBottom: 24,
},
// 输入框
inputContainer: {
marginBottom: 20,
},
inputLabel: {
fontSize: 14,
fontWeight: '500',
color: colors.text.secondary,
marginBottom: 10,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F5F7',
borderRadius: 14,
paddingHorizontal: 18,
height: 56,
},
input: {
flex: 1,
fontSize: 16,
color: colors.text.primary,
height: 56,
},
checkIcon: {
marginLeft: 8,
},
eyeButton: {
padding: 4,
marginLeft: 4,
},
// 验证码行
codeRow: {
flexDirection: 'row',
gap: 12,
},
codeInputWrapper: {
flex: 1,
},
sendCodeButton: {
height: 56,
paddingHorizontal: 16,
backgroundColor: 'rgba(255, 107, 53, 0.1)',
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
minWidth: 100,
},
sendCodeButtonDisabled: {
opacity: 0.5,
},
sendCodeButtonText: {
fontSize: 14,
fontWeight: '600',
color: THEME_COLORS.primary,
},
// 服务条款
termsContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 28,
marginTop: 4,
},
termsText: {
fontSize: 14,
color: colors.text.secondary,
marginLeft: 10,
flex: 1,
lineHeight: 22,
},
termsLink: {
color: THEME_COLORS.primary,
fontWeight: '500',
},
// 注册按钮
registerButton: {
height: 56,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
alignItems: 'center',
justifyContent: 'center',
},
registerButtonDisabled: {
opacity: 0.7,
},
registerButtonText: {
fontSize: 17,
fontWeight: '600',
color: '#FFFFFF',
},
// 底部
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 32,
},
footerText: {
fontSize: 15,
color: colors.text.secondary,
},
loginLink: {
fontSize: 15,
color: THEME_COLORS.primary,
fontWeight: '600',
marginLeft: 6,
},
});
}
export default RegisterScreen;