2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
2026-04-01 04:48:38 +08:00
|
|
|
|
* 注册页面(分步向导版)
|
|
|
|
|
|
* 胡萝卜BBS - 用户注册流程
|
2026-04-01 00:50:38 +08:00
|
|
|
|
*
|
2026-04-01 04:48:38 +08:00
|
|
|
|
* 步骤:
|
|
|
|
|
|
* 1. 输入邮箱
|
|
|
|
|
|
* 2. 填写验证码
|
|
|
|
|
|
* 3. 设置用户信息
|
|
|
|
|
|
*
|
|
|
|
|
|
* 使用 registerStore 持久化存储步骤数据
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
import React, { useEffect, useMemo, useRef } from 'react';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
Text,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
KeyboardAvoidingView,
|
|
|
|
|
|
Platform,
|
|
|
|
|
|
ScrollView,
|
|
|
|
|
|
Animated,
|
2026-03-16 17:47:10 +08:00
|
|
|
|
StatusBar,
|
2026-04-01 04:48:38 +08:00
|
|
|
|
TouchableOpacity,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} from 'react-native';
|
|
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import { useRouter } from 'expo-router';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
2026-04-01 00:50:38 +08:00
|
|
|
|
import { useAppColors, type AppColors } from '../../theme';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { authService, resolveAuthApiError } from '../../services/authService';
|
|
|
|
|
|
import { useAuthStore } from '../../stores';
|
2026-04-01 04:48:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
useRegisterStore,
|
|
|
|
|
|
useRegisterStep,
|
|
|
|
|
|
useRegisterFormData,
|
|
|
|
|
|
useRegisterCountdown,
|
|
|
|
|
|
} from '../../stores/registerStore';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import * as hrefs from '../../navigation/hrefs';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { showPrompt } from '../../services/promptService';
|
2026-04-01 04:48:38 +08:00
|
|
|
|
import { RegisterStepIndicator } from '../../components/common';
|
|
|
|
|
|
import { RegisterStepProps, RegisterStep } from './types';
|
|
|
|
|
|
import { RegisterStep1Email } from './RegisterStep1Email';
|
|
|
|
|
|
import { RegisterStep2Verification } from './RegisterStep2Verification';
|
|
|
|
|
|
import { RegisterStep3Profile } from './RegisterStep3Profile';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-01 00:50:38 +08:00
|
|
|
|
// 胡萝卜橙主题色
|
|
|
|
|
|
const THEME_COLORS = {
|
|
|
|
|
|
primary: '#FF6B35',
|
|
|
|
|
|
primaryLight: '#FF8C5A',
|
|
|
|
|
|
primaryDark: '#E55A2B',
|
|
|
|
|
|
};
|
2026-03-16 17:47:10 +08:00
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 步骤组件映射
|
|
|
|
|
|
const STEP_COMPONENTS = [
|
|
|
|
|
|
RegisterStep1Email,
|
|
|
|
|
|
RegisterStep2Verification,
|
|
|
|
|
|
RegisterStep3Profile,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
export const RegisterScreen: React.FC = () => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const register = useAuthStore((state) => state.register);
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 注册步骤状态
|
|
|
|
|
|
const currentStep = useRegisterStep();
|
|
|
|
|
|
const formData = useRegisterFormData();
|
|
|
|
|
|
const countdown = useRegisterCountdown();
|
|
|
|
|
|
const {
|
|
|
|
|
|
setCurrentStep,
|
|
|
|
|
|
updateFormData,
|
|
|
|
|
|
setCountdown,
|
|
|
|
|
|
decrementCountdown,
|
|
|
|
|
|
markStepCompleted,
|
|
|
|
|
|
resetRegisterData,
|
|
|
|
|
|
goToNextStep,
|
|
|
|
|
|
goToPrevStep,
|
|
|
|
|
|
} = useRegisterStore();
|
|
|
|
|
|
|
|
|
|
|
|
// 本地状态
|
|
|
|
|
|
const [loading, setLoading] = React.useState(false);
|
|
|
|
|
|
const [sendingCode, setSendingCode] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 动画值
|
|
|
|
|
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
|
|
const slideAnim = useRef(new Animated.Value(20)).current;
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复持久化的数据
|
|
|
|
|
|
const [isHydrated, setIsHydrated] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const hydrateData = async () => {
|
|
|
|
|
|
const { hydrate } = useRegisterStore.getState();
|
|
|
|
|
|
await hydrate();
|
|
|
|
|
|
setIsHydrated(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
hydrateData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已登录
|
2026-03-24 14:21:31 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (isAuthenticated) {
|
|
|
|
|
|
router.replace(hrefs.hrefHome());
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isAuthenticated, router]);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 启动入场动画
|
|
|
|
|
|
useEffect(() => {
|
2026-04-01 00:50:38 +08:00
|
|
|
|
Animated.parallel([
|
|
|
|
|
|
Animated.timing(fadeAnim, {
|
|
|
|
|
|
toValue: 1,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
duration: 400,
|
|
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
2026-04-01 00:50:38 +08:00
|
|
|
|
Animated.timing(slideAnim, {
|
|
|
|
|
|
toValue: 0,
|
|
|
|
|
|
duration: 400,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
]).start();
|
2026-04-01 04:48:38 +08:00
|
|
|
|
}, [currentStep]); // 步骤切换时重新触发动画
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 倒计时处理
|
2026-03-09 21:29:03 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (countdown <= 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const timer = setInterval(() => {
|
2026-04-01 04:48:38 +08:00
|
|
|
|
decrementCountdown();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}, 1000);
|
|
|
|
|
|
return () => clearInterval(timer);
|
2026-04-01 04:48:38 +08:00
|
|
|
|
}, [countdown, decrementCountdown]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 发送验证码
|
|
|
|
|
|
const handleSendCode = async (email: string): Promise<boolean> => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
setSendingCode(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ok = await authService.sendRegisterCode(email.trim());
|
|
|
|
|
|
if (ok) {
|
2026-04-01 04:48:38 +08:00
|
|
|
|
setCountdown(180); // 3分钟倒计时
|
|
|
|
|
|
showPrompt({
|
|
|
|
|
|
title: '发送成功',
|
|
|
|
|
|
message: '验证码已发送,请查收邮箱(有效期3分钟)',
|
|
|
|
|
|
type: 'success',
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
2026-04-01 04:48:38 +08:00
|
|
|
|
return false;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error: any) {
|
2026-04-01 04:48:38 +08:00
|
|
|
|
showPrompt({
|
|
|
|
|
|
title: '发送失败',
|
|
|
|
|
|
message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'),
|
|
|
|
|
|
type: 'error',
|
|
|
|
|
|
});
|
|
|
|
|
|
return false;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSendingCode(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 处理步骤完成
|
|
|
|
|
|
const handleStepComplete = () => {
|
|
|
|
|
|
markStepCompleted(currentStep as 0 | 1 | 2);
|
|
|
|
|
|
goToNextStep();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理返回上一步
|
|
|
|
|
|
const handleGoBack = () => {
|
|
|
|
|
|
if (currentStep > 0) {
|
|
|
|
|
|
goToPrevStep();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 处理注册
|
|
|
|
|
|
const handleRegister = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const success = await register({
|
2026-04-01 04:48:38 +08:00
|
|
|
|
username: formData.username,
|
|
|
|
|
|
password: formData.password,
|
|
|
|
|
|
nickname: formData.nickname,
|
|
|
|
|
|
email: formData.email.trim(),
|
|
|
|
|
|
verification_code: formData.verificationCode.trim(),
|
|
|
|
|
|
phone: formData.phone || undefined,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
});
|
2026-04-01 04:48:38 +08:00
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (success) {
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 注册成功,清空注册数据
|
|
|
|
|
|
resetRegisterData();
|
|
|
|
|
|
// 导航会自动切换到主页(通过 isAuthenticated 变化)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
const msg = useAuthStore.getState().error || '注册失败,请稍后重试';
|
|
|
|
|
|
showPrompt({ title: '注册失败', message: msg, type: 'error' });
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
2026-04-01 04:48:38 +08:00
|
|
|
|
showPrompt({
|
|
|
|
|
|
title: '注册失败',
|
|
|
|
|
|
message: resolveAuthApiError(error, '请稍后重试'),
|
|
|
|
|
|
type: 'error',
|
|
|
|
|
|
});
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-01 04:48:38 +08:00
|
|
|
|
// 渲染当前步骤的组件
|
|
|
|
|
|
const StepComponent = STEP_COMPONENTS[currentStep];
|
|
|
|
|
|
|
|
|
|
|
|
// 准备步骤组件的 props
|
|
|
|
|
|
const stepProps: RegisterStepProps = {
|
|
|
|
|
|
formData,
|
|
|
|
|
|
updateFormData,
|
|
|
|
|
|
onNext: currentStep === 2 ? handleRegister : handleStepComplete,
|
|
|
|
|
|
onPrev: handleGoBack,
|
|
|
|
|
|
colors,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
sendingCode,
|
|
|
|
|
|
countdown,
|
|
|
|
|
|
onSendCode: handleSendCode,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已完成的步骤
|
|
|
|
|
|
const completedSteps = [0, 1, 2].filter(
|
|
|
|
|
|
(step) => useRegisterStore.getState().completedSteps[step as 0 | 1 | 2]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 跳转到登录页
|
|
|
|
|
|
const handleGoToLogin = () => {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefAuthLogin());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-01 00:50:38 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
|
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
|
|
|
|
|
<KeyboardAvoidingView
|
|
|
|
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
|
|
|
|
style={styles.keyboardView}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
>
|
2026-04-01 00:50:38 +08:00
|
|
|
|
<ScrollView
|
|
|
|
|
|
contentContainerStyle={styles.scrollContent}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
|
|
|
>
|
2026-04-01 04:48:38 +08:00
|
|
|
|
{/* 顶部导航 */}
|
|
|
|
|
|
<View style={styles.header}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
onPress={currentStep > 0 ? handleGoBack : handleGoToLogin}
|
|
|
|
|
|
style={styles.backIconButton}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name={currentStep > 0 ? 'arrow-left' : 'close'}
|
|
|
|
|
|
size={24}
|
|
|
|
|
|
color={colors.text?.primary || '#333'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
<Text style={styles.headerTitle}>注册</Text>
|
|
|
|
|
|
<View style={styles.headerRight} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 步骤指示器 */}
|
|
|
|
|
|
<RegisterStepIndicator
|
|
|
|
|
|
currentStep={currentStep}
|
|
|
|
|
|
containerStyle={styles.stepIndicator}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 步骤内容 */}
|
|
|
|
|
|
<Animated.View
|
2026-04-01 00:50:38 +08:00
|
|
|
|
style={[
|
|
|
|
|
|
styles.content,
|
|
|
|
|
|
{
|
|
|
|
|
|
opacity: fadeAnim,
|
|
|
|
|
|
transform: [{ translateY: slideAnim }],
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-04-01 04:48:38 +08:00
|
|
|
|
<StepComponent {...stepProps} />
|
2026-04-01 00:50:38 +08:00
|
|
|
|
</Animated.View>
|
2026-04-01 04:48:38 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 底部登录提示 */}
|
|
|
|
|
|
<View style={styles.footer}>
|
|
|
|
|
|
<Text style={styles.footerText}>已有账号?</Text>
|
|
|
|
|
|
<TouchableOpacity onPress={handleGoToLogin}>
|
|
|
|
|
|
<Text style={styles.loginLink}>立即登录</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
2026-04-01 00:50:38 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</KeyboardAvoidingView>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
function createRegisterStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
2026-04-01 00:50:38 +08:00
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
|
|
},
|
|
|
|
|
|
keyboardView: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
scrollContent: {
|
|
|
|
|
|
flexGrow: 1,
|
2026-04-01 04:48:38 +08:00
|
|
|
|
paddingHorizontal: 24,
|
|
|
|
|
|
paddingTop: 20,
|
|
|
|
|
|
paddingBottom: 40,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
header: {
|
2026-04-01 00:50:38 +08:00
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-01 04:48:38 +08:00
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
marginBottom: 24,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
backIconButton: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
borderRadius: 20,
|
|
|
|
|
|
backgroundColor: '#F5F5F7',
|
2026-04-01 00:50:38 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
headerTitle: {
|
|
|
|
|
|
fontSize: 18,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
fontWeight: '600',
|
2026-04-01 04:48:38 +08:00
|
|
|
|
color: colors.text?.primary || '#333',
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
headerRight: {
|
|
|
|
|
|
width: 40,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
stepIndicator: {
|
|
|
|
|
|
marginBottom: 32,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
2026-04-01 04:48:38 +08:00
|
|
|
|
content: {
|
|
|
|
|
|
flex: 1,
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
footer: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginTop: 32,
|
|
|
|
|
|
},
|
|
|
|
|
|
footerText: {
|
|
|
|
|
|
fontSize: 15,
|
2026-04-01 04:48:38 +08:00
|
|
|
|
color: colors.text?.secondary || '#666',
|
2026-04-01 00:50:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
loginLink: {
|
|
|
|
|
|
fontSize: 15,
|
2026-04-01 04:48:38 +08:00
|
|
|
|
color: '#FF6B35',
|
2026-04-01 00:50:38 +08:00
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
marginLeft: 6,
|
|
|
|
|
|
},
|
2026-03-25 05:16:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
export default RegisterScreen;
|