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

358 lines
9.4 KiB
TypeScript
Raw Normal View History

/**
*
* BBS -
*
*
* 1.
* 2.
* 3.
*
* 使 registerStore
*/
import React, { useEffect, useMemo, useRef } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
Animated,
2026-03-16 17:47:10 +08:00
StatusBar,
TouchableOpacity,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
2026-04-13 01:30:37 +08:00
import { authService, resolveAuthApiError } from '@/services/auth';
import { useAuthStore } from '../../stores';
import {
useRegisterStore,
useRegisterStep,
useRegisterFormData,
useRegisterCountdown,
} from '../../stores/registerStore';
import * as hrefs from '../../navigation/hrefs';
2026-04-13 01:30:37 +08:00
import { showPrompt } from '@/services/ui';
import { RegisterStepIndicator } from '../../components/common';
import { RegisterStepProps, RegisterStep } from './types';
import { RegisterStep1Email } from './RegisterStep1Email';
import { RegisterStep2Verification } from './RegisterStep2Verification';
import { RegisterStep3Profile } from './RegisterStep3Profile';
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
2026-03-16 17:47:10 +08:00
// 步骤组件映射
const STEP_COMPONENTS = [
RegisterStep1Email,
RegisterStep2Verification,
RegisterStep3Profile,
];
export const RegisterScreen: React.FC = () => {
const colors = useAppColors();
const resolved = useResolvedColorScheme();
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
const router = useRouter();
const register = useAuthStore((state) => state.register);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
// 注册步骤状态
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();
}, []);
// 检查是否已登录
useEffect(() => {
if (isAuthenticated) {
router.replace(hrefs.hrefHome());
}
}, [isAuthenticated, router]);
// 启动入场动画
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]).start();
}, [currentStep]); // 步骤切换时重新触发动画
// 倒计时处理
useEffect(() => {
if (countdown <= 0) {
return;
}
const timer = setInterval(() => {
decrementCountdown();
}, 1000);
return () => clearInterval(timer);
}, [countdown, decrementCountdown]);
// 发送验证码
const handleSendCode = async (email: string): Promise<boolean> => {
setSendingCode(true);
try {
const ok = await authService.sendRegisterCode(email.trim());
if (ok) {
setCountdown(180); // 3分钟倒计时
showPrompt({
title: '发送成功',
message: '验证码已发送请查收邮箱有效期3分钟',
type: 'success',
});
return true;
}
return false;
} catch (error: any) {
showPrompt({
title: '发送失败',
message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'),
type: 'error',
});
return false;
} finally {
setSendingCode(false);
}
};
// 处理步骤完成
const handleStepComplete = () => {
markStepCompleted(currentStep as 0 | 1 | 2);
goToNextStep();
};
// 处理返回上一步
const handleGoBack = () => {
if (currentStep > 0) {
goToPrevStep();
}
};
// 处理注册
const handleRegister = async () => {
setLoading(true);
try {
const success = await register({
username: formData.username,
password: formData.password,
nickname: formData.nickname,
email: formData.email.trim(),
verification_code: formData.verificationCode.trim(),
phone: formData.phone || undefined,
});
if (success) {
// 注册成功,清空注册数据
resetRegisterData();
// 导航会自动切换到主页(通过 isAuthenticated 变化)
} 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 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]
);
// 跳转到登录页
const handleGoToLogin = () => {
router.push(hrefs.hrefAuthLogin());
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* 顶部导航 */}
<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
style={[
styles.content,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
<StepComponent {...stepProps} />
</Animated.View>
{/* 底部登录提示 */}
<View style={styles.footer}>
<Text style={styles.footerText}></Text>
<TouchableOpacity onPress={handleGoToLogin}>
<Text style={styles.loginLink}></Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
function createRegisterStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
keyboardView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
paddingTop: 20,
paddingBottom: 40,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
},
backIconButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.background.default,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text?.primary || '#333',
},
headerRight: {
width: 40,
},
stepIndicator: {
marginBottom: 32,
},
content: {
flex: 1,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 32,
},
footerText: {
fontSize: 15,
color: colors.text?.secondary || '#666',
},
loginLink: {
fontSize: 15,
color: colors.primary.main,
fontWeight: '600',
marginLeft: 6,
},
});
}
export default RegisterScreen;