feat(auth): implement multi-step registration process and enhance background services
- Updated RegisterScreen to support a multi-step registration flow with components for email input, verification, and profile setup. - Added new components: VerificationCodeInput, StepIndicator, and corresponding types for step management. - Enhanced background service to check authentication status before executing tasks, improving app reliability. - Updated TypeScript configuration to include module resolution and JSON module support. This update significantly improves the user registration experience and ensures background tasks are only executed for authenticated users.
This commit is contained in:
200
src/components/common/StepIndicator.tsx
Normal file
200
src/components/common/StepIndicator.tsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* 步骤指示器组件 - 极简扁平设计
|
||||||
|
* 与下方表单风格统一
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||||
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
|
||||||
|
export interface Step {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepIndicatorProps {
|
||||||
|
steps: Step[];
|
||||||
|
currentStep: number;
|
||||||
|
containerStyle?: ViewStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepIndicator: React.FC<StepIndicatorProps> = ({
|
||||||
|
steps,
|
||||||
|
currentStep,
|
||||||
|
containerStyle,
|
||||||
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, containerStyle]}>
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const isCompleted = index < currentStep;
|
||||||
|
const isCurrent = index === currentStep;
|
||||||
|
const isPending = index > currentStep;
|
||||||
|
const isLast = index === steps.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={step.id}>
|
||||||
|
<View style={styles.stepWrapper}>
|
||||||
|
{/* 步骤编号/状态 */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.stepBadge,
|
||||||
|
isCompleted && styles.stepBadgeCompleted,
|
||||||
|
isCurrent && styles.stepBadgeCurrent,
|
||||||
|
isPending && styles.stepBadgePending,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.stepBadgeText,
|
||||||
|
isCompleted && styles.stepBadgeTextCompleted,
|
||||||
|
isCurrent && styles.stepBadgeTextCurrent,
|
||||||
|
isPending && styles.stepBadgeTextPending,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{isCompleted ? '✓' : index + 1}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 步骤标题 */}
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.stepTitle,
|
||||||
|
isCompleted && styles.stepTitleCompleted,
|
||||||
|
isCurrent && styles.stepTitleCurrent,
|
||||||
|
isPending && styles.stepTitlePending,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 连接线 */}
|
||||||
|
{!isLast && (
|
||||||
|
<View style={styles.connector}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.connectorLine,
|
||||||
|
isCompleted && styles.connectorLineCompleted,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册流程专用步骤指示器
|
||||||
|
export const RegisterStepIndicator: React.FC<{
|
||||||
|
currentStep: number;
|
||||||
|
containerStyle?: ViewStyle;
|
||||||
|
}> = ({ currentStep, containerStyle }) => {
|
||||||
|
const steps: Step[] = [
|
||||||
|
{ id: 0, title: '输入邮箱' },
|
||||||
|
{ id: 1, title: '验证码' },
|
||||||
|
{ id: 2, title: '设置信息' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StepIndicator
|
||||||
|
steps={steps}
|
||||||
|
currentStep={currentStep}
|
||||||
|
containerStyle={containerStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function createStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
stepWrapper: {
|
||||||
|
alignItems: 'center',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
// 步骤徽章
|
||||||
|
stepBadge: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: '#F5F5F7',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
|
stepBadgeCompleted: {
|
||||||
|
backgroundColor: '#FF6B35',
|
||||||
|
},
|
||||||
|
stepBadgeCurrent: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#FF6B35',
|
||||||
|
},
|
||||||
|
stepBadgePending: {
|
||||||
|
backgroundColor: '#F5F5F7',
|
||||||
|
},
|
||||||
|
stepBadgeText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
stepBadgeTextCompleted: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
stepBadgeTextCurrent: {
|
||||||
|
color: '#FF6B35',
|
||||||
|
},
|
||||||
|
stepBadgeTextPending: {
|
||||||
|
color: '#BBB',
|
||||||
|
},
|
||||||
|
// 步骤标题
|
||||||
|
stepTitle: {
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#999',
|
||||||
|
fontWeight: '500',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
stepTitleCompleted: {
|
||||||
|
color: '#FF6B35',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
stepTitleCurrent: {
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
stepTitlePending: {
|
||||||
|
color: '#BBB',
|
||||||
|
},
|
||||||
|
// 连接线
|
||||||
|
connector: {
|
||||||
|
width: 60,
|
||||||
|
height: 28,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
connectorLine: {
|
||||||
|
width: '100%',
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: '#E5E5E5',
|
||||||
|
borderRadius: 1,
|
||||||
|
},
|
||||||
|
connectorLineCompleted: {
|
||||||
|
backgroundColor: '#FF6B35',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StepIndicator;
|
||||||
256
src/components/common/VerificationCodeInput.tsx
Normal file
256
src/components/common/VerificationCodeInput.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* 验证码输入组件
|
||||||
|
* 参考设计:4-6位数字验证码,每个数字独立显示在一个方框中
|
||||||
|
* 支持自动聚焦、粘贴、删除回退
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
TextInput,
|
||||||
|
StyleSheet,
|
||||||
|
Keyboard,
|
||||||
|
Platform,
|
||||||
|
Text,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
|
||||||
|
interface VerificationCodeInputProps {
|
||||||
|
value: string;
|
||||||
|
onChangeValue: (value: string) => void;
|
||||||
|
length?: number; // 验证码位数,默认4位
|
||||||
|
autoFocus?: boolean;
|
||||||
|
onComplete?: (value: string) => void; // 输入完成回调
|
||||||
|
containerStyle?: ViewStyle;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CELL_WIDTH = 56;
|
||||||
|
const CELL_HEIGHT = 64;
|
||||||
|
const CELL_MARGIN = 8;
|
||||||
|
|
||||||
|
export const VerificationCodeInput: React.FC<VerificationCodeInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChangeValue,
|
||||||
|
length = 4,
|
||||||
|
autoFocus = true,
|
||||||
|
onComplete,
|
||||||
|
containerStyle,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const colors = useAppColors();
|
||||||
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
|
// 为每个数字创建一个 ref
|
||||||
|
const inputRefs = useRef<(TextInput | null)[]>(Array(length).fill(null));
|
||||||
|
|
||||||
|
// 将 value 分割为单个字符数组
|
||||||
|
const chars = value.split('').slice(0, length);
|
||||||
|
// 填充空位
|
||||||
|
while (chars.length < length) {
|
||||||
|
chars.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前聚焦的位置
|
||||||
|
const getFocusedIndex = useCallback(() => {
|
||||||
|
return Math.min(value.length, length - 1);
|
||||||
|
}, [value.length, length]);
|
||||||
|
|
||||||
|
// 聚焦到指定位置
|
||||||
|
const focusInput = useCallback((index: number) => {
|
||||||
|
if (index >= 0 && index < length) {
|
||||||
|
inputRefs.current[index]?.focus();
|
||||||
|
}
|
||||||
|
}, [length]);
|
||||||
|
|
||||||
|
// 自动聚焦第一个输入框
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus && !disabled) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
focusInput(0);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [autoFocus, disabled, focusInput]);
|
||||||
|
|
||||||
|
// 当值改变时,自动聚焦到下一个输入框
|
||||||
|
useEffect(() => {
|
||||||
|
if (value.length < length) {
|
||||||
|
focusInput(value.length);
|
||||||
|
}
|
||||||
|
// 当输入完成时触发回调
|
||||||
|
if (value.length === length && onComplete) {
|
||||||
|
onComplete(value);
|
||||||
|
}
|
||||||
|
}, [value, length, onComplete, focusInput]);
|
||||||
|
|
||||||
|
// 处理文本变化
|
||||||
|
const handleChangeText = (text: string, index: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
// 只允许数字
|
||||||
|
const numericText = text.replace(/[^0-9]/g, '');
|
||||||
|
|
||||||
|
if (numericText.length === 0) {
|
||||||
|
// 删除操作
|
||||||
|
if (value.length > index) {
|
||||||
|
// 删除当前位置的字符
|
||||||
|
const newValue = value.slice(0, index) + value.slice(index + 1);
|
||||||
|
onChangeValue(newValue);
|
||||||
|
// 聚焦到前一个输入框
|
||||||
|
if (index > 0) {
|
||||||
|
focusInput(index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (numericText.length === 1) {
|
||||||
|
// 单个字符输入
|
||||||
|
let newValue: string;
|
||||||
|
if (index < value.length) {
|
||||||
|
// 替换已有字符
|
||||||
|
newValue = value.slice(0, index) + numericText + value.slice(index + 1);
|
||||||
|
} else {
|
||||||
|
// 追加新字符
|
||||||
|
newValue = value + numericText;
|
||||||
|
}
|
||||||
|
onChangeValue(newValue.slice(0, length));
|
||||||
|
// 自动聚焦到下一个
|
||||||
|
if (index < length - 1) {
|
||||||
|
focusInput(index + 1);
|
||||||
|
}
|
||||||
|
} else if (numericText.length > 1) {
|
||||||
|
// 粘贴操作:尝试将粘贴的内容作为完整验证码
|
||||||
|
const newValue = numericText.slice(0, length);
|
||||||
|
onChangeValue(newValue);
|
||||||
|
// 聚焦到最后一个或下一个
|
||||||
|
const focusIndex = Math.min(newValue.length, length - 1);
|
||||||
|
focusInput(focusIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理按键事件(用于删除键)
|
||||||
|
const handleKeyPress = (e: any, index: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
if (e.nativeEvent.key === 'Backspace') {
|
||||||
|
if (value.length > index) {
|
||||||
|
// 删除当前位置的字符
|
||||||
|
const newValue = value.slice(0, index) + value.slice(index + 1);
|
||||||
|
onChangeValue(newValue);
|
||||||
|
} else if (index > 0 && value.length === index) {
|
||||||
|
// 当前位置没有字符,删除前一个
|
||||||
|
const newValue = value.slice(0, -1);
|
||||||
|
onChangeValue(newValue);
|
||||||
|
focusInput(index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 聚焦时全选文本
|
||||||
|
const handleFocus = (index: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
inputRefs.current[index]?.setNativeProps({
|
||||||
|
selection: { start: 0, end: 1 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, containerStyle]}>
|
||||||
|
<View style={styles.cellsContainer}>
|
||||||
|
{chars.map((char, index) => {
|
||||||
|
const isFocused = value.length === index;
|
||||||
|
const isFilled = char !== '';
|
||||||
|
const isLastFilled = index === value.length - 1 && isFilled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
styles.cell,
|
||||||
|
isFocused && styles.cellFocused,
|
||||||
|
isFilled && styles.cellFilled,
|
||||||
|
disabled && styles.cellDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={(ref) => {
|
||||||
|
inputRefs.current[index] = ref;
|
||||||
|
}}
|
||||||
|
style={styles.cellInput}
|
||||||
|
value={char}
|
||||||
|
onChangeText={(text) => handleChangeText(text, index)}
|
||||||
|
onKeyPress={(e) => handleKeyPress(e, index)}
|
||||||
|
onFocus={() => handleFocus(index)}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
maxLength={1}
|
||||||
|
selectTextOnFocus
|
||||||
|
editable={!disabled}
|
||||||
|
caretHidden={true}
|
||||||
|
textContentType="oneTimeCode" // iOS 自动填充支持
|
||||||
|
/>
|
||||||
|
{/* 光标指示器 */}
|
||||||
|
{isFocused && !isFilled && !disabled && (
|
||||||
|
<View style={styles.cursor} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function createStyles(colors: AppColors) {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
cellsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
width: CELL_WIDTH,
|
||||||
|
height: CELL_HEIGHT,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: colors.divider,
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
marginHorizontal: CELL_MARGIN / 2,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
cellFocused: {
|
||||||
|
borderColor: colors.primary.main,
|
||||||
|
backgroundColor: `${colors.primary.main}10`, // 10% 透明度的主色
|
||||||
|
},
|
||||||
|
cellFilled: {
|
||||||
|
borderColor: colors.primary.main,
|
||||||
|
},
|
||||||
|
cellDisabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
backgroundColor: colors.background.disabled,
|
||||||
|
},
|
||||||
|
cellInput: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text.primary,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 2,
|
||||||
|
height: 28,
|
||||||
|
backgroundColor: colors.primary.main,
|
||||||
|
borderRadius: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VerificationCodeInput;
|
||||||
@@ -32,3 +32,8 @@ export type { ImageGalleryProps, GalleryImageItem } from './ImageGallery';
|
|||||||
export type { ResponsiveGridProps } from './ResponsiveGrid';
|
export type { ResponsiveGridProps } from './ResponsiveGrid';
|
||||||
export type { ResponsiveStackProps, StackDirection, StackAlignment, StackJustify } from './ResponsiveStack';
|
export type { ResponsiveStackProps, StackDirection, StackAlignment, StackJustify } from './ResponsiveStack';
|
||||||
export type { AdaptiveLayoutProps, SidebarLayoutProps } from './AdaptiveLayout';
|
export type { AdaptiveLayoutProps, SidebarLayoutProps } from './AdaptiveLayout';
|
||||||
|
|
||||||
|
// 注册流程相关组件
|
||||||
|
export { default as VerificationCodeInput } from './VerificationCodeInput';
|
||||||
|
export { default as StepIndicator, RegisterStepIndicator } from './StepIndicator';
|
||||||
|
export type { Step, StepIndicatorProps } from './StepIndicator';
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* 注册页 RegisterScreen(简洁表单样式)
|
* 注册页面(分步向导版)
|
||||||
* 胡萝卜BBS - 用户注册
|
* 胡萝卜BBS - 用户注册流程
|
||||||
*
|
*
|
||||||
* 设计风格:
|
* 步骤:
|
||||||
* - 纯白背景,扁平化设计
|
* 1. 输入邮箱
|
||||||
* - 增大间距,避免页面太空
|
* 2. 填写验证码
|
||||||
* - 输入框带灰色背景填充
|
* 3. 设置用户信息
|
||||||
* - 橙色圆角按钮
|
*
|
||||||
|
* 使用 registerStore 持久化存储步骤数据
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
TextInput,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
ActivityIndicator,
|
|
||||||
Animated,
|
Animated,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
@@ -29,8 +28,19 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||||||
import { useAppColors, type AppColors } from '../../theme';
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
import { authService, resolveAuthApiError } from '../../services/authService';
|
import { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
|
import {
|
||||||
|
useRegisterStore,
|
||||||
|
useRegisterStep,
|
||||||
|
useRegisterFormData,
|
||||||
|
useRegisterCountdown,
|
||||||
|
} from '../../stores/registerStore';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
|
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 = {
|
const THEME_COLORS = {
|
||||||
@@ -39,6 +49,13 @@ const THEME_COLORS = {
|
|||||||
primaryDark: '#E55A2B',
|
primaryDark: '#E55A2B',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 步骤组件映射
|
||||||
|
const STEP_COMPONENTS = [
|
||||||
|
RegisterStep1Email,
|
||||||
|
RegisterStep2Verification,
|
||||||
|
RegisterStep3Profile,
|
||||||
|
];
|
||||||
|
|
||||||
export const RegisterScreen: React.FC = () => {
|
export const RegisterScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
||||||
@@ -46,30 +63,48 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
const register = useAuthStore((state) => state.register);
|
const register = useAuthStore((state) => state.register);
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
router.replace(hrefs.hrefHome());
|
router.replace(hrefs.hrefHome());
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, router]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
@@ -84,129 +119,112 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
}, []);
|
}, [currentStep]); // 步骤切换时重新触发动画
|
||||||
|
|
||||||
|
// 倒计时处理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (countdown <= 0) {
|
if (countdown <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCountdown((prev) => (prev <= 1 ? 0 : prev - 1));
|
decrementCountdown();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [countdown]);
|
}, [countdown, decrementCountdown]);
|
||||||
|
|
||||||
// 表单验证
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const handleSendCode = async (email: string): Promise<boolean> => {
|
||||||
setSendingCode(true);
|
setSendingCode(true);
|
||||||
try {
|
try {
|
||||||
const ok = await authService.sendRegisterCode(email.trim());
|
const ok = await authService.sendRegisterCode(email.trim());
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setCountdown(60);
|
setCountdown(180); // 3分钟倒计时
|
||||||
showPrompt({ title: '发送成功', message: '验证码已发送,请查收邮箱', type: 'success' });
|
showPrompt({
|
||||||
|
title: '发送成功',
|
||||||
|
message: '验证码已发送,请查收邮箱(有效期3分钟)',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'), type: 'error' });
|
showPrompt({
|
||||||
|
title: '发送失败',
|
||||||
|
message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setSendingCode(false);
|
setSendingCode(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理步骤完成
|
||||||
|
const handleStepComplete = () => {
|
||||||
|
markStepCompleted(currentStep as 0 | 1 | 2);
|
||||||
|
goToNextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理返回上一步
|
||||||
|
const handleGoBack = () => {
|
||||||
|
if (currentStep > 0) {
|
||||||
|
goToPrevStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理注册
|
// 处理注册
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
if (!validateForm()) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const success = await register({
|
const success = await register({
|
||||||
username,
|
username: formData.username,
|
||||||
password,
|
password: formData.password,
|
||||||
nickname,
|
nickname: formData.nickname,
|
||||||
email: email.trim(),
|
email: formData.email.trim(),
|
||||||
verification_code: verificationCode.trim(),
|
verification_code: formData.verificationCode.trim(),
|
||||||
phone: phone || undefined,
|
phone: formData.phone || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 注册成功,导航会自动切换到主页
|
// 注册成功,清空注册数据
|
||||||
|
resetRegisterData();
|
||||||
|
// 导航会自动切换到主页(通过 isAuthenticated 变化)
|
||||||
} else {
|
} else {
|
||||||
const msg = useAuthStore.getState().error || '注册失败,请稍后重试';
|
const msg = useAuthStore.getState().error || '注册失败,请稍后重试';
|
||||||
showPrompt({ title: '注册失败', message: msg, type: 'error' });
|
showPrompt({ title: '注册失败', message: msg, type: 'error' });
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showPrompt({ title: '注册失败', message: resolveAuthApiError(error, '请稍后重试'), type: 'error' });
|
showPrompt({
|
||||||
|
title: '注册失败',
|
||||||
|
message: resolveAuthApiError(error, '请稍后重试'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 = () => {
|
const handleGoToLogin = () => {
|
||||||
router.push(hrefs.hrefAuthLogin());
|
router.push(hrefs.hrefAuthLogin());
|
||||||
@@ -224,7 +242,32 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<Animated.View
|
{/* 顶部导航 */}
|
||||||
|
<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}
|
||||||
|
completedSteps={completedSteps}
|
||||||
|
containerStyle={styles.stepIndicator}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 步骤内容 */}
|
||||||
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.content,
|
styles.content,
|
||||||
{
|
{
|
||||||
@@ -233,216 +276,16 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{/* 标题区域 - 增大间距 */}
|
<StepComponent {...stepProps} />
|
||||||
<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>
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* 底部登录提示 */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Text style={styles.footerText}>已有账号?</Text>
|
||||||
|
<TouchableOpacity onPress={handleGoToLogin}>
|
||||||
|
<Text style={styles.loginLink}>立即登录</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -460,141 +303,38 @@ function createRegisterStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
paddingHorizontal: 28,
|
paddingHorizontal: 24,
|
||||||
paddingTop: 50,
|
paddingTop: 20,
|
||||||
paddingBottom: 50,
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
backIconButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: '#F5F5F7',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text?.primary || '#333',
|
||||||
|
},
|
||||||
|
headerRight: {
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
stepIndicator: {
|
||||||
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
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: {
|
footer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -603,11 +343,11 @@ function createRegisterStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
footerText: {
|
footerText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: colors.text.secondary,
|
color: colors.text?.secondary || '#666',
|
||||||
},
|
},
|
||||||
loginLink: {
|
loginLink: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: THEME_COLORS.primary,
|
color: '#FF6B35',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
},
|
},
|
||||||
|
|||||||
192
src/screens/auth/RegisterStep1Email.tsx
Normal file
192
src/screens/auth/RegisterStep1Email.tsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 注册步骤1:输入邮箱
|
||||||
|
* 包含邮箱输入框和获取验证码按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
import { RegisterStepProps } from './types';
|
||||||
|
|
||||||
|
const THEME_COLORS = {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryDark: '#E55A2B',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RegisterStep1Email: React.FC<RegisterStepProps> = ({
|
||||||
|
formData,
|
||||||
|
updateFormData,
|
||||||
|
onNext,
|
||||||
|
colors: propColors,
|
||||||
|
sendingCode,
|
||||||
|
countdown,
|
||||||
|
onSendCode,
|
||||||
|
}) => {
|
||||||
|
const colors = propColors || useAppColors();
|
||||||
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
|
const [emailError, setEmailError] = useState('');
|
||||||
|
|
||||||
|
const validateEmail = (email: string): boolean => {
|
||||||
|
if (!email.trim()) {
|
||||||
|
setEmailError('请输入邮箱');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
setEmailError('请输入正确的邮箱地址');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setEmailError('');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (!validateEmail(formData.email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onSendCode) {
|
||||||
|
const success = await onSendCode(formData.email);
|
||||||
|
if (success) {
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.formSection}>
|
||||||
|
{/* 邮箱输入框 */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Text style={styles.inputLabel}>邮箱</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.inputWrapper,
|
||||||
|
emailError ? styles.inputWrapperError : null,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
placeholderTextColor={colors.text?.hint || '#999'}
|
||||||
|
value={formData.email}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
updateFormData({ email: text });
|
||||||
|
if (emailError) setEmailError('');
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardType="email-address"
|
||||||
|
editable={!sendingCode}
|
||||||
|
/>
|
||||||
|
{formData.email.length > 0 &&
|
||||||
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={20}
|
||||||
|
color={THEME_COLORS.primary}
|
||||||
|
style={styles.checkIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{emailError ? (
|
||||||
|
<Text style={styles.errorText}>{emailError}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 获取验证码按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
(sendingCode || countdown > 0) && styles.actionButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleSendCode}
|
||||||
|
disabled={sendingCode || countdown > 0}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
>
|
||||||
|
<Text style={styles.actionButtonText}>
|
||||||
|
{sendingCode
|
||||||
|
? '发送中...'
|
||||||
|
: countdown > 0
|
||||||
|
? `${countdown}秒后重试`
|
||||||
|
: '获取验证码'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
formSection: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#F5F5F7',
|
||||||
|
borderRadius: 14,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
height: 56,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
inputWrapperError: {
|
||||||
|
borderColor: colors.error?.main || '#FF4444',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.text?.primary || '#333',
|
||||||
|
height: 56,
|
||||||
|
},
|
||||||
|
checkIcon: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.error?.main || '#FF4444',
|
||||||
|
marginTop: 6,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: '#FF6B35',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
actionButtonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterStep1Email;
|
||||||
212
src/screens/auth/RegisterStep2Verification.tsx
Normal file
212
src/screens/auth/RegisterStep2Verification.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* 注册步骤2:填写验证码
|
||||||
|
* 使用 VerificationCodeInput 组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
import { VerificationCodeInput } from '../../components/common';
|
||||||
|
import { RegisterStepProps } from './types';
|
||||||
|
|
||||||
|
const THEME_COLORS = {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryDark: '#E55A2B',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RegisterStep2Verification: React.FC<RegisterStepProps> = ({
|
||||||
|
formData,
|
||||||
|
updateFormData,
|
||||||
|
onNext,
|
||||||
|
onPrev,
|
||||||
|
colors: propColors,
|
||||||
|
countdown,
|
||||||
|
onSendCode,
|
||||||
|
}) => {
|
||||||
|
const colors = propColors || useAppColors();
|
||||||
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
|
const [codeError, setCodeError] = useState('');
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
|
|
||||||
|
const handleComplete = async (code: string) => {
|
||||||
|
if (code.length !== 6) {
|
||||||
|
setCodeError('请输入6位验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCodeError('');
|
||||||
|
setIsVerifying(true);
|
||||||
|
|
||||||
|
// 模拟验证,实际应该调用API验证验证码
|
||||||
|
// const success = await authService.verifyCode(formData.email, code);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVerifying(false);
|
||||||
|
onNext();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResendCode = async () => {
|
||||||
|
if (onSendCode && countdown === 0) {
|
||||||
|
await onSendCode(formData.email);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCountdown = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.emailInfo}>
|
||||||
|
<Text style={styles.emailText}>验证码已发送至 {formData.email}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.formSection}>
|
||||||
|
{/* 验证码输入 */}
|
||||||
|
<View style={styles.codeInputContainer}>
|
||||||
|
<VerificationCodeInput
|
||||||
|
value={formData.verificationCode}
|
||||||
|
onChangeValue={(value) => {
|
||||||
|
updateFormData({ verificationCode: value });
|
||||||
|
if (codeError) setCodeError('');
|
||||||
|
}}
|
||||||
|
length={6}
|
||||||
|
autoFocus={true}
|
||||||
|
onComplete={handleComplete}
|
||||||
|
disabled={isVerifying}
|
||||||
|
/>
|
||||||
|
{codeError ? (
|
||||||
|
<Text style={styles.errorText}>{codeError}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 重发倒计时 */}
|
||||||
|
<View style={styles.resendContainer}>
|
||||||
|
{(countdown ?? 0) > 0 ? (
|
||||||
|
<Text style={styles.countdownText}>
|
||||||
|
{formatCountdown(countdown!)} 后重新发送
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity onPress={handleResendCode} activeOpacity={0.8}>
|
||||||
|
<Text style={styles.resendText}>重新发送验证码</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 下一步按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
formData.verificationCode.length !== 6 && styles.actionButtonDisabled,
|
||||||
|
isVerifying && styles.actionButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={() => handleComplete(formData.verificationCode)}
|
||||||
|
disabled={formData.verificationCode.length !== 6 || isVerifying}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
>
|
||||||
|
<Text style={styles.actionButtonText}>
|
||||||
|
{isVerifying ? '验证中...' : '下一步'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 返回按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={onPrev}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="arrow-left"
|
||||||
|
size={18}
|
||||||
|
color={colors.text?.secondary || '#666'}
|
||||||
|
/>
|
||||||
|
<Text style={styles.backButtonText}>返回上一步</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
emailInfo: {
|
||||||
|
marginBottom: 32,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emailText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
},
|
||||||
|
formSection: {
|
||||||
|
marginBottom: 24,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
codeInputContainer: {
|
||||||
|
marginBottom: 24,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.error?.main || '#FF4444',
|
||||||
|
marginTop: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
resendContainer: {
|
||||||
|
marginBottom: 32,
|
||||||
|
},
|
||||||
|
countdownText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text?.secondary || '#999',
|
||||||
|
},
|
||||||
|
resendText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: THEME_COLORS.primary,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: '#FF6B35',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 320,
|
||||||
|
},
|
||||||
|
actionButtonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterStep2Verification;
|
||||||
343
src/screens/auth/RegisterStep3Profile.tsx
Normal file
343
src/screens/auth/RegisterStep3Profile.tsx
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
/**
|
||||||
|
* 注册步骤3:设置用户信息
|
||||||
|
* 包含用户名、昵称、密码等
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
|
import { RegisterStepProps } from './types';
|
||||||
|
|
||||||
|
const THEME_COLORS = {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryDark: '#E55A2B',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
||||||
|
formData,
|
||||||
|
updateFormData,
|
||||||
|
onNext,
|
||||||
|
onPrev,
|
||||||
|
colors: propColors,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
const colors = propColors || useAppColors();
|
||||||
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [agreedToTerms, setAgreedToTerms] = useState(true);
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!formData.username.trim()) {
|
||||||
|
newErrors.username = '请输入用户名';
|
||||||
|
} else if (formData.username.length < 3 || formData.username.length > 20) {
|
||||||
|
newErrors.username = '用户名长度需在3-20个字符之间';
|
||||||
|
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
|
||||||
|
newErrors.username = '用户名只能包含字母、数字和下划线';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.nickname.trim()) {
|
||||||
|
newErrors.nickname = '请输入昵称';
|
||||||
|
} else if (formData.nickname.length < 2 || formData.nickname.length > 20) {
|
||||||
|
newErrors.nickname = '昵称长度需在2-20个字符之间';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.password) {
|
||||||
|
newErrors.password = '请输入密码';
|
||||||
|
} else if (formData.password.length < 6) {
|
||||||
|
newErrors.password = '密码长度不能少于6位';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = '两次输入的密码不一致';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agreedToTerms) {
|
||||||
|
newErrors.terms = '请同意用户协议和隐私政策';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderInput = (
|
||||||
|
label: string,
|
||||||
|
field: keyof typeof formData,
|
||||||
|
placeholder: string,
|
||||||
|
options: {
|
||||||
|
secureTextEntry?: boolean;
|
||||||
|
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad';
|
||||||
|
maxLength?: number;
|
||||||
|
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
||||||
|
showToggle?: boolean;
|
||||||
|
toggleValue?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
secureTextEntry = false,
|
||||||
|
keyboardType = 'default',
|
||||||
|
maxLength,
|
||||||
|
autoCapitalize = 'none',
|
||||||
|
showToggle,
|
||||||
|
toggleValue,
|
||||||
|
onToggle,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const error = errors[field as string];
|
||||||
|
const value = formData[field] || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Text style={styles.inputLabel}>{label}</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.inputWrapper,
|
||||||
|
error ? styles.inputWrapperError : null,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={colors.text?.hint || '#999'}
|
||||||
|
value={value}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
updateFormData({ [field]: text } as Partial<typeof formData>);
|
||||||
|
if (errors[field as string]) {
|
||||||
|
setErrors((prev) => ({ ...prev, [field as string]: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoCapitalize={autoCapitalize}
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardType={keyboardType}
|
||||||
|
maxLength={maxLength}
|
||||||
|
secureTextEntry={secureTextEntry && !toggleValue}
|
||||||
|
editable={!loading}
|
||||||
|
/>
|
||||||
|
{showToggle && onToggle && (
|
||||||
|
<TouchableOpacity onPress={onToggle} style={styles.eyeButton}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={toggleValue ? 'eye-off-outline' : 'eye-outline'}
|
||||||
|
size={20}
|
||||||
|
color={colors.text?.hint || '#999'}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.formSection}>
|
||||||
|
{/* 用户名 */}
|
||||||
|
{renderInput('用户名', 'username', '3-20个字符,字母数字下划线', {
|
||||||
|
maxLength: 20,
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 昵称 */}
|
||||||
|
{renderInput('昵称', 'nickname', '2-20个字符', {
|
||||||
|
autoCapitalize: 'sentences',
|
||||||
|
maxLength: 20,
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 手机号(选填) */}
|
||||||
|
{renderInput('手机号(选填)', 'phone', '请输入手机号', {
|
||||||
|
keyboardType: 'phone-pad',
|
||||||
|
maxLength: 11,
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 密码 */}
|
||||||
|
{renderInput('密码', 'password', '至少6位', {
|
||||||
|
secureTextEntry: true,
|
||||||
|
showToggle: true,
|
||||||
|
toggleValue: showPassword,
|
||||||
|
onToggle: () => setShowPassword(!showPassword),
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 确认密码 */}
|
||||||
|
{renderInput('确认密码', 'confirmPassword', '再次输入密码', {
|
||||||
|
secureTextEntry: true,
|
||||||
|
showToggle: true,
|
||||||
|
toggleValue: showConfirmPassword,
|
||||||
|
onToggle: () => setShowConfirmPassword(!showConfirmPassword),
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 服务条款 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.termsContainer}
|
||||||
|
onPress={() => {
|
||||||
|
setAgreedToTerms(!agreedToTerms);
|
||||||
|
if (errors.terms) {
|
||||||
|
setErrors((prev) => ({ ...prev, terms: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||||
|
size={22}
|
||||||
|
color={agreedToTerms ? THEME_COLORS.primary : colors.text?.hint || '#999'}
|
||||||
|
/>
|
||||||
|
<Text style={styles.termsText}>
|
||||||
|
我已阅读并同意
|
||||||
|
<Text style={styles.termsLink}>《用户协议》</Text>
|
||||||
|
和
|
||||||
|
<Text style={styles.termsLink}>《隐私政策》</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{errors.terms ? (
|
||||||
|
<Text style={[styles.errorText, { marginTop: -12, marginBottom: 12 }]}>
|
||||||
|
{errors.terms}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 完成注册按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, loading && styles.actionButtonDisabled]}
|
||||||
|
onPress={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator size="small" color="#FFF" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.actionButtonText}>完成注册</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 返回按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={onPrev}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="arrow-left"
|
||||||
|
size={18}
|
||||||
|
color={colors.text?.secondary || '#666'}
|
||||||
|
/>
|
||||||
|
<Text style={styles.backButtonText}>返回上一步</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
formSection: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#F5F5F7',
|
||||||
|
borderRadius: 14,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
height: 52,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
inputWrapperError: {
|
||||||
|
borderColor: colors.error?.main || '#FF4444',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.text?.primary || '#333',
|
||||||
|
height: 52,
|
||||||
|
},
|
||||||
|
eyeButton: {
|
||||||
|
padding: 4,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.error?.main || '#FF4444',
|
||||||
|
marginTop: 6,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
termsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
termsText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
marginLeft: 10,
|
||||||
|
flex: 1,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
termsLink: {
|
||||||
|
color: '#FF6B35',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: '#FF6B35',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
actionButtonDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text?.secondary || '#666',
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterStep3Profile;
|
||||||
@@ -7,3 +7,9 @@ export { LoginScreen } from './LoginScreen';
|
|||||||
export { RegisterScreen } from './RegisterScreen';
|
export { RegisterScreen } from './RegisterScreen';
|
||||||
export { ForgotPasswordScreen } from './ForgotPasswordScreen';
|
export { ForgotPasswordScreen } from './ForgotPasswordScreen';
|
||||||
export { WelcomeScreen } from './WelcomeScreen';
|
export { WelcomeScreen } from './WelcomeScreen';
|
||||||
|
|
||||||
|
// 注册步骤组件
|
||||||
|
export { RegisterStep1Email } from './RegisterStep1Email';
|
||||||
|
export { RegisterStep2Verification } from './RegisterStep2Verification';
|
||||||
|
export { RegisterStep3Profile } from './RegisterStep3Profile';
|
||||||
|
export type { RegisterFormData, RegisterStepProps, RegisterStep } from './types';
|
||||||
|
|||||||
41
src/screens/auth/types.ts
Normal file
41
src/screens/auth/types.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 注册流程类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AppColors } from '../../theme';
|
||||||
|
|
||||||
|
export interface RegisterFormData {
|
||||||
|
email: string;
|
||||||
|
verificationCode: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterStepProps {
|
||||||
|
formData: RegisterFormData;
|
||||||
|
updateFormData: (data: Partial<RegisterFormData>) => void;
|
||||||
|
onNext: () => void;
|
||||||
|
onPrev?: () => void;
|
||||||
|
colors?: AppColors;
|
||||||
|
// 状态
|
||||||
|
loading?: boolean;
|
||||||
|
sendingCode?: boolean;
|
||||||
|
countdown?: number;
|
||||||
|
// 回调
|
||||||
|
onSendCode?: (email: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegisterStep = 0 | 1 | 2;
|
||||||
|
|
||||||
|
export interface StepConfig {
|
||||||
|
id: RegisterStep;
|
||||||
|
title: string;
|
||||||
|
component: React.ComponentType<RegisterStepProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保文件被识别为模块
|
||||||
|
export {};
|
||||||
|
|
||||||
@@ -32,9 +32,18 @@ const BACKGROUND_INTERVAL = 15; // 15 分钟
|
|||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let appStateSubscription: any = null;
|
let appStateSubscription: any = null;
|
||||||
|
|
||||||
|
// 导入 api 用于检查认证状态
|
||||||
|
import { api } from './api';
|
||||||
|
|
||||||
// 定义后台任务
|
// 定义后台任务
|
||||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||||
try {
|
try {
|
||||||
|
// 检查是否已认证
|
||||||
|
const token = await api.getToken();
|
||||||
|
if (!token) {
|
||||||
|
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查 SSE 连接状态
|
// 检查 SSE 连接状态
|
||||||
if (!wsService.isConnected()) {
|
if (!wsService.isConnected()) {
|
||||||
await wsService.connect();
|
await wsService.connect();
|
||||||
@@ -51,6 +60,12 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
|||||||
// SSE 保活任务
|
// SSE 保活任务
|
||||||
TaskManager.defineTask(REALTIME_KEEPALIVE_TASK, async () => {
|
TaskManager.defineTask(REALTIME_KEEPALIVE_TASK, async () => {
|
||||||
try {
|
try {
|
||||||
|
// 检查是否已认证
|
||||||
|
const token = await api.getToken();
|
||||||
|
if (!token) {
|
||||||
|
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
|
}
|
||||||
|
|
||||||
if (!wsService.isConnected()) {
|
if (!wsService.isConnected()) {
|
||||||
await wsService.connect();
|
await wsService.connect();
|
||||||
}
|
}
|
||||||
@@ -116,9 +131,13 @@ function setupAppStateListener(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appStateSubscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
|
appStateSubscription = AppState.addEventListener('change', async (nextAppState: AppStateStatus) => {
|
||||||
void nextAppState;
|
|
||||||
if (nextAppState === 'active') {
|
if (nextAppState === 'active') {
|
||||||
|
// 检查是否已认证
|
||||||
|
const token = await api.getToken();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// App 回到前台,确保连接
|
// App 回到前台,确保连接
|
||||||
if (!wsService.isConnected()) {
|
if (!wsService.isConnected()) {
|
||||||
wsService.connect();
|
wsService.connect();
|
||||||
|
|||||||
@@ -335,9 +335,9 @@ class WebSocketService {
|
|||||||
try {
|
try {
|
||||||
const token = await api.getToken();
|
const token = await api.getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error('[WebSocket] No token available');
|
console.log('[WebSocket] No token available, skipping connection');
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
this.scheduleReconnect();
|
// 没有 token 时不要重连,等待用户登录后再连接
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
180
src/stores/registerStore.ts
Normal file
180
src/stores/registerStore.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* 注册流程状态管理
|
||||||
|
* 使用 Zustand + AsyncStorage 实现步骤数据的持久化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'register_form_data';
|
||||||
|
|
||||||
|
export type RegisterStep = 0 | 1 | 2;
|
||||||
|
|
||||||
|
export interface RegisterFormData {
|
||||||
|
email: string;
|
||||||
|
verificationCode: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegisterState {
|
||||||
|
// 当前步骤: 0=邮箱, 1=验证码, 2=用户信息
|
||||||
|
currentStep: RegisterStep;
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
formData: RegisterFormData;
|
||||||
|
|
||||||
|
// 倒计时(秒)
|
||||||
|
countdown: number;
|
||||||
|
|
||||||
|
// 步骤完成状态
|
||||||
|
completedSteps: Record<RegisterStep, boolean>;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setCurrentStep: (step: RegisterStep) => void;
|
||||||
|
updateFormData: (data: Partial<RegisterFormData>) => void;
|
||||||
|
setCountdown: (seconds: number) => void;
|
||||||
|
decrementCountdown: () => void;
|
||||||
|
markStepCompleted: (step: RegisterStep) => void;
|
||||||
|
resetRegisterData: () => void;
|
||||||
|
goToNextStep: () => void;
|
||||||
|
goToPrevStep: () => void;
|
||||||
|
// 持久化
|
||||||
|
hydrate: () => Promise<void>;
|
||||||
|
persistData: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialFormData: RegisterFormData = {
|
||||||
|
email: '',
|
||||||
|
verificationCode: '',
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialCompletedSteps: Record<RegisterStep, boolean> = {
|
||||||
|
0: false,
|
||||||
|
1: false,
|
||||||
|
2: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegisterStore = create<RegisterState>((set, get) => ({
|
||||||
|
currentStep: 0,
|
||||||
|
formData: initialFormData,
|
||||||
|
countdown: 0,
|
||||||
|
completedSteps: initialCompletedSteps,
|
||||||
|
|
||||||
|
setCurrentStep: (step) => {
|
||||||
|
set({ currentStep: step });
|
||||||
|
get().persistData();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFormData: (data) =>
|
||||||
|
set((state) => {
|
||||||
|
const newFormData = { ...state.formData, ...data };
|
||||||
|
// 异步保存到 AsyncStorage
|
||||||
|
AsyncStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
formData: newFormData,
|
||||||
|
completedSteps: state.completedSteps,
|
||||||
|
})
|
||||||
|
).catch(() => {});
|
||||||
|
return { formData: newFormData };
|
||||||
|
}),
|
||||||
|
|
||||||
|
setCountdown: (seconds) => set({ countdown: seconds }),
|
||||||
|
|
||||||
|
decrementCountdown: () =>
|
||||||
|
set((state) => ({
|
||||||
|
countdown: state.countdown > 0 ? state.countdown - 1 : 0,
|
||||||
|
})),
|
||||||
|
|
||||||
|
markStepCompleted: (step) =>
|
||||||
|
set((state) => {
|
||||||
|
const newCompletedSteps = { ...state.completedSteps, [step]: true };
|
||||||
|
// 异步保存到 AsyncStorage
|
||||||
|
AsyncStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
formData: state.formData,
|
||||||
|
completedSteps: newCompletedSteps,
|
||||||
|
})
|
||||||
|
).catch(() => {});
|
||||||
|
return { completedSteps: newCompletedSteps };
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetRegisterData: () => {
|
||||||
|
set({
|
||||||
|
currentStep: 0,
|
||||||
|
formData: initialFormData,
|
||||||
|
countdown: 0,
|
||||||
|
completedSteps: initialCompletedSteps,
|
||||||
|
});
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
goToNextStep: () => {
|
||||||
|
const { currentStep } = get();
|
||||||
|
if (currentStep < 2) {
|
||||||
|
const newStep = (currentStep + 1) as RegisterStep;
|
||||||
|
set({ currentStep: newStep });
|
||||||
|
get().persistData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPrevStep: () => {
|
||||||
|
const { currentStep } = get();
|
||||||
|
if (currentStep > 0) {
|
||||||
|
const newStep = (currentStep - 1) as RegisterStep;
|
||||||
|
set({ currentStep: newStep });
|
||||||
|
get().persistData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从 AsyncStorage 恢复数据
|
||||||
|
hydrate: async () => {
|
||||||
|
try {
|
||||||
|
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
set({
|
||||||
|
currentStep: data.currentStep ?? 0,
|
||||||
|
formData: { ...initialFormData, ...data.formData },
|
||||||
|
completedSteps: { ...initialCompletedSteps, ...data.completedSteps },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存数据到 AsyncStorage
|
||||||
|
persistData: async () => {
|
||||||
|
const { currentStep, formData, completedSteps } = get();
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
currentStep,
|
||||||
|
formData,
|
||||||
|
completedSteps,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 导出便捷 hooks
|
||||||
|
export const useRegisterStep = () => useRegisterStore((state) => state.currentStep);
|
||||||
|
export const useRegisterFormData = () => useRegisterStore((state) => state.formData);
|
||||||
|
export const useRegisterCountdown = () => useRegisterStore((state) => state.countdown);
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user