Files
frontend/src/screens/auth/RegisterScreen.tsx
lan 4fde3e403a
Some checks failed
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / ota-android (push) Successful in 1m39s
Frontend CI / build-android-apk (push) Failing after 1m52s
Frontend CI / build-and-push-web (push) Successful in 22m5s
refactor(core): optimize state management and component rendering performance
Improve application stability and performance by optimizing Zustand store usage, implementing memoization patterns, and introducing persistent authentication.

- **State Management**:
  - Refactor Zustand selectors to use `useShallow` and `getState()` to prevent unnecessary re-renders and infinite loops in hooks.
  - Implement `persist` middleware for `authStore` to maintain user sessions across restarts.
  - Introduce `buildStateCached` in `themeStore` to reduce redundant theme object computations.
- **Performance & Rendering**:
  - Implement `useMemo` for stable object/array references in `ImageGallery` and list components to prevent expensive re-renders.
  - Replace inline arrow functions with `useCallback` in complex screens like `ChatScreen` and `MessageListScreen`.
  - Optimize `FlashList` usage by providing stable `key` and `extraData` props.
- **Architecture**:
  - Decouple unread count fetching by introducing `useUnreadCountQuery` (React Query) for better caching and synchronization.
  - Add `useChannels` hook to centralize channel data fetching.
  - Refine `SessionGate` logic to allow immediate rendering of authenticated users while verifying in the background.
2026-05-18 00:39:25 +08:00

351 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 注册页面(分步向导版)
* 威友 - 用户注册流程
*
* 步骤:
* 1. 输入邮箱
* 2. 填写验证码
* 3. 设置用户信息
*
* 使用 registerStore 纯内存状态管理
*/
import React, { useEffect, useMemo, useRef } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
Animated,
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';
import { authService, resolveAuthApiError } from '@/services/auth';
import { useAuthStore } from '../../stores';
import {
useRegisterStore,
useRegisterStep,
useRegisterFormData,
useRegisterCountdown,
} from '../../stores/auth';
import * as hrefs from '../../navigation/hrefs';
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',
};
// 步骤组件映射
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.getState();
// 本地状态
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;
// 页面挂载时重置倒计时,避免上次残留的倒计时导致无法重新获取验证码
useEffect(() => {
setCountdown(0);
}, []);
// 检查是否已登录
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,
alignItems: 'center',
justifyContent: 'center',
marginLeft: -12,
padding: 0,
},
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;