/** * 注册页 RegisterScreen(响应式适配 - 分栏布局) * 胡萝卜BBS - 用户注册 * * 布局规则: * - 屏幕宽度 < 768px:单栏布局,橙色渐变背景 * - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰 */ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, TextInput, KeyboardAvoidingView, Platform, ScrollView, ActivityIndicator, Animated, StatusBar, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { useAuthStore } from '../../stores'; import * as hrefs from '../../navigation/hrefs'; import { useResponsive, useResponsiveValue } from '../../hooks'; import { showPrompt } from '../../services/promptService'; // 分栏布局断点 const SPLIT_BREAKPOINT = 768; 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); // 响应式布局 const { isLandscape, width } = useResponsive(); // 是否使用分栏布局 const isSplitLayout = width >= SPLIT_BREAKPOINT; 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(50)).current; const scaleAnim = useRef(new Animated.Value(0.95)).current; const inputFadeAnim = useRef(new Animated.Value(0)).current; // 响应式值 - 大屏模式单独放大图标尺寸,更显大气 const iconSize = useResponsiveValue({ xs: 48, sm: 52, md: 56, lg: isSplitLayout ? 120 : 64, // 大屏模式图标进一步放大 xl: isSplitLayout ? 140 : 72 }); const iconContainerSize = useResponsiveValue({ xs: 80, sm: 88, md: 96, lg: isSplitLayout ? 180 : 108, // 大屏模式图标容器进一步放大 xl: isSplitLayout ? 200 : 120 }); const titleSize = useResponsiveValue({ xs: 24, sm: 26, md: 28, lg: isSplitLayout ? 60 : 30, // 大屏模式标题文字大幅放大 xl: isSplitLayout ? 72 : 32 }); const formMaxWidth = isSplitLayout ? 480 : undefined; // 启动入场动画 useEffect(() => { Animated.sequence([ Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true, }), Animated.timing(scaleAnim, { toValue: 1, duration: 500, useNativeDriver: true, }), ]), Animated.timing(slideAnim, { toValue: 0, duration: 400, useNativeDriver: true, }), Animated.timing(inputFadeAnim, { toValue: 1, duration: 300, 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()); }; // 渲染左侧面板(Logo和品牌信息)- 优化大屏布局 const renderLeftPanel = () => ( {/* 用户图标 - 靠上显示 */} {/* 品牌名称 - 优化大屏排版 */} 加入萝卜社区 加入萝卜社区,发现有趣的内容 {/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */} {isSplitLayout ? ( // 大屏模式:水平排列 + 大幅放大图标 ) : ( // 小屏模式:保持原有横向排列 )} ); // 渲染表单内容 const renderFormContent = () => ( 注册账号 {/* 用户名输入框 */} {username.length > 0 && ( setUsername('')} style={styles.clearButton} > )} {/* 昵称输入框 */} {nickname.length > 0 && ( setNickname('')} style={styles.clearButton} > )} {/* 邮箱输入框 */} {email.length > 0 && ( setEmail('')} style={styles.clearButton} > )} {/* 邮箱验证码 */} 0) && styles.sendCodeButtonDisabled, ]} onPress={handleSendCode} disabled={sendingCode || countdown > 0} activeOpacity={0.8} > {sendingCode ? '发送中...' : countdown > 0 ? `${countdown}s` : '发送验证码'} {/* 手机号输入框 */} {phone.length > 0 && ( setPhone('')} style={styles.clearButton} > )} {/* 密码输入框 */} setShowPassword(!showPassword)} style={styles.clearButton} > {/* 确认密码输入框 */} setShowConfirmPassword(!showConfirmPassword)} style={styles.clearButton} > {/* 服务条款 */} setAgreedToTerms(!agreedToTerms)} activeOpacity={0.8} > 我已阅读并同意 《用户协议》《隐私政策》 {/* 注册按钮 */} {loading ? ( ) : ( 注 册 )} {/* 底部登录提示 */} 已有账号? 立即登录 ); // 渲染分栏布局 const renderSplitLayout = () => ( {/* 左侧橙色面板 */} {/* 装饰性背景元素 */} {renderLeftPanel()} {/* 右侧中性灰面板 */} {/* 核心修复:用View包裹,确保垂直+水平居中 */} {renderFormContent()} ); // 渲染单栏布局 const renderSingleLayout = () => ( {/* 装饰性背景元素 */} {/* 头部 */} 加入萝卜社区 加入萝卜社区,发现有趣的内容 {/* 表单 */} {renderFormContent()} ); return isSplitLayout ? renderSplitLayout() : renderSingleLayout(); }; function createRegisterStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, }, gradient: { flex: 1, }, keyboardView: { flex: 1, }, scrollContent: { flexGrow: 1, paddingHorizontal: spacing.xl, paddingVertical: spacing.xl, }, scrollContentLandscape: { paddingVertical: spacing.md, }, // 装饰性背景元素 decorCircle1: { position: 'absolute', top: -80, right: -80, width: 250, height: 250, borderRadius: 125, backgroundColor: 'rgba(255,255,255,0.1)', }, decorCircle2: { position: 'absolute', bottom: 150, left: -120, width: 200, height: 200, borderRadius: 100, backgroundColor: 'rgba(255,255,255,0.08)', }, // 头部区域 headerSection: { alignItems: 'center', marginTop: spacing.xl, marginBottom: spacing.xl, }, headerSectionLandscape: { marginTop: spacing.md, marginBottom: spacing.md, }, iconContainer: { backgroundColor: 'rgba(255,255,255,0.25)', alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md, borderWidth: 2, borderColor: 'rgba(255,255,255,0.4)', ...shadows.md, }, title: { fontWeight: '800', color: '#FFFFFF', marginBottom: spacing.xs, textShadowColor: 'rgba(0,0,0,0.1)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, }, subtitle: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', }, // 表单卡片 formCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius['2xl'], padding: spacing.xl, marginBottom: spacing.lg, ...shadows.md, }, inputContainer: { marginBottom: spacing.md, }, codeRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, }, inputWrapper: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.default, borderRadius: borderRadius.lg, borderWidth: 1.5, borderColor: colors.divider, paddingHorizontal: spacing.md, height: 52, }, inputWrapperWide: { height: 56, }, codeInputWrapper: { flex: 1, }, inputIcon: { marginRight: spacing.sm, }, input: { flex: 1, fontSize: fontSizes.md, color: colors.text.primary, }, inputWide: { fontSize: fontSizes.lg, }, clearButton: { padding: spacing.xs, }, sendCodeButton: { height: 52, minWidth: 110, paddingHorizontal: spacing.md, borderRadius: borderRadius.lg, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, sendCodeButtonDisabled: { opacity: 0.6, }, sendCodeButtonText: { color: '#fff', fontSize: fontSizes.sm, fontWeight: '600', }, // 服务条款 termsContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.lg, marginTop: spacing.sm, }, checkbox: { marginRight: spacing.sm, }, checkboxChecked: { // 选中状态的额外样式 }, termsText: { fontSize: fontSizes.sm, color: colors.text.secondary, flex: 1, lineHeight: 20, }, termsLink: { color: colors.primary.main, fontWeight: '500', }, // 注册按钮 registerButton: { borderRadius: borderRadius.lg, overflow: 'hidden', ...shadows.md, }, registerButtonGradient: { height: 50, alignItems: 'center', justifyContent: 'center', }, registerButtonGradientWide: { height: 54, }, registerButtonDisabled: { opacity: 0.7, }, registerButtonText: { fontSize: fontSizes.lg, fontWeight: '700', color: '#FFFFFF', }, // 底部区域 footerSection: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 'auto', paddingTop: spacing.md, paddingBottom: spacing.md, }, splitFooterSection: { paddingTop: spacing.xl, }, footerText: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', }, splitFooterText: { color: colors.text.primary, }, loginLink: { fontSize: fontSizes.md, color: '#FFFFFF', fontWeight: '700', marginLeft: spacing.xs, textDecorationLine: 'underline', }, splitLoginLink: { color: colors.primary.main, }, // ========== 分栏布局样式 ========== splitContainer: { flex: 1, flexDirection: 'row', }, leftPanelContainer: { flex: 1, // 50% justifyContent: 'center', alignItems: 'center', overflow: 'hidden', }, leftPanelSafeArea: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: spacing['4xl'], // 增加内边距,不贴边 }, leftPanel: { alignItems: 'center', paddingHorizontal: spacing['2xl'], }, // 大屏模式左侧面板布局 splitLeftPanel: { width: '100%', justifyContent: 'center', paddingVertical: 120, }, // 大屏模式图标容器样式 - 增加阴影和内边距,更立体 splitIconContainer: { borderWidth: 3, borderColor: 'rgba(255,255,255,0.5)', ...shadows.lg, }, iconContainerTop: { marginTop: spacing['3xl'], }, // 大屏模式标题样式 - 大幅放大,增加间距和文字效果 splitTitle: { fontSize: 72, // 大幅放大主标题 marginBottom: 48, textShadowOffset: { width: 0, height: 4 }, textShadowRadius: 8, letterSpacing: 4, // 增加字间距,更显大气 fontWeight: '900', }, // 大屏模式副标题样式 - 大幅放大 splitSubtitle: { fontSize: 24, // 放大副标题 marginBottom: 120, // 增加底部间距 lineHeight: 36, paddingHorizontal: spacing['4xl'], letterSpacing: 1.5, fontWeight: '500', }, leftPanelDecorBottom: { marginTop: spacing['2xl'], }, // 大屏模式装饰元素布局 splitLeftPanelDecorBottom: { marginTop: 120, width: '100%', alignItems: 'center', }, // 大屏模式装饰元素水平排列 splitDecorRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', width: '100%', gap: spacing['2xl'], // 增加图标间距 }, // 大屏模式装饰图标样式 splitDecorIcon: { transform: [{ scale: 1.1 }], opacity: 0.9, }, decorRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', width: '80%', }, rightPanelContainer: { flex: 1, // 50% backgroundColor: colors.background.paper, }, rightPanelSafeArea: { flex: 1, // 移除左右内边距,交给子容器控制 }, // 分栏布局右侧内容包裹器(核心修复) splitRightContentWrapper: { flex: 1, justifyContent: 'center', alignItems: 'center', // 新增:左右内边距,保证表单和边缘有间距 paddingHorizontal: spacing['2xl'], }, // 表单内容包裹器(区分大屏/小屏) splitFormWrapper: { // 关键修改:设置最大宽度 + 自适应宽度,实现水平居中 maxWidth: 480, width: '100%', }, singleFormWrapper: { flexGrow: 1, width: '100%', }, // 小屏模式:原有默认阴影 normalFormCardShadow: { ...shadows.md, }, // 大屏模式:专用全向阴影 splitFormCardShadow: { // iOS 全向柔和阴影 shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.12, shadowRadius: 12, // Android 高程 + 轻微边框(模拟顶部阴影) elevation: 8, // 新增:极细微的顶部边框,强化区分度 borderTopWidth: 1, borderTopColor: 'rgba(0,0,0,0.05)', }, formTitle: { fontSize: fontSizes['2xl'], fontWeight: '700', color: colors.text.primary, marginBottom: spacing.xl, textAlign: 'center', }, }); } export default RegisterScreen;