diff --git a/src/components/common/StepIndicator.tsx b/src/components/common/StepIndicator.tsx new file mode 100644 index 0000000..bda89fd --- /dev/null +++ b/src/components/common/StepIndicator.tsx @@ -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 = ({ + steps, + currentStep, + containerStyle, +}) => { + const colors = useAppColors(); + const styles = createStyles(colors); + + return ( + + {steps.map((step, index) => { + const isCompleted = index < currentStep; + const isCurrent = index === currentStep; + const isPending = index > currentStep; + const isLast = index === steps.length - 1; + + return ( + + + {/* 步骤编号/状态 */} + + + {isCompleted ? '✓' : index + 1} + + + + {/* 步骤标题 */} + + {step.title} + + + + {/* 连接线 */} + {!isLast && ( + + + + )} + + ); + })} + + ); +}; + +// 注册流程专用步骤指示器 +export const RegisterStepIndicator: React.FC<{ + currentStep: number; + containerStyle?: ViewStyle; +}> = ({ currentStep, containerStyle }) => { + const steps: Step[] = [ + { id: 0, title: '输入邮箱' }, + { id: 1, title: '验证码' }, + { id: 2, title: '设置信息' }, + ]; + + return ( + + ); +}; + +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; diff --git a/src/components/common/VerificationCodeInput.tsx b/src/components/common/VerificationCodeInput.tsx new file mode 100644 index 0000000..78ef14b --- /dev/null +++ b/src/components/common/VerificationCodeInput.tsx @@ -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 = ({ + 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 ( + + + {chars.map((char, index) => { + const isFocused = value.length === index; + const isFilled = char !== ''; + const isLastFilled = index === value.length - 1 && isFilled; + + return ( + + { + 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 && ( + + )} + + ); + })} + + + ); +}; + +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; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 6b737c4..04912a7 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -32,3 +32,8 @@ export type { ImageGalleryProps, GalleryImageItem } from './ImageGallery'; export type { ResponsiveGridProps } from './ResponsiveGrid'; export type { ResponsiveStackProps, StackDirection, StackAlignment, StackJustify } from './ResponsiveStack'; 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'; diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index bf77133..f39e883 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -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 { View, Text, StyleSheet, - TouchableOpacity, - TextInput, KeyboardAvoidingView, Platform, ScrollView, - ActivityIndicator, Animated, StatusBar, + TouchableOpacity, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; @@ -29,8 +28,19 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useAppColors, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { useAuthStore } from '../../stores'; +import { + useRegisterStore, + useRegisterStep, + useRegisterFormData, + useRegisterCountdown, +} from '../../stores/registerStore'; import * as hrefs from '../../navigation/hrefs'; 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 = { @@ -39,6 +49,13 @@ const THEME_COLORS = { primaryDark: '#E55A2B', }; +// 步骤组件映射 +const STEP_COMPONENTS = [ + RegisterStep1Email, + RegisterStep2Verification, + RegisterStep3Profile, +]; + export const RegisterScreen: React.FC = () => { const colors = useAppColors(); const styles = useMemo(() => createRegisterStyles(colors), [colors]); @@ -46,30 +63,48 @@ export const RegisterScreen: React.FC = () => { 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]); - 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(() => { Animated.parallel([ @@ -84,129 +119,112 @@ export const RegisterScreen: React.FC = () => { useNativeDriver: true, }), ]).start(); - }, []); + }, [currentStep]); // 步骤切换时重新触发动画 + // 倒计时处理 useEffect(() => { if (countdown <= 0) { return; } const timer = setInterval(() => { - setCountdown((prev) => (prev <= 1 ? 0 : prev - 1)); + decrementCountdown(); }, 1000); return () => clearInterval(timer); - }, [countdown]); - - // 表单验证 - const validateForm = (): boolean => { - if (!username.trim()) { - showPrompt({ title: '提示', message: '请输入用户名', type: 'warning' }); - return false; - } - if (username.length < 3 || username.length > 20) { - showPrompt({ title: '提示', message: '用户名长度需在3-20个字符之间', type: 'warning' }); - return false; - } - if (!/^[a-zA-Z0-9_]+$/.test(username)) { - showPrompt({ title: '提示', message: '用户名只能包含字母、数字和下划线', type: 'warning' }); - return false; - } - if (!nickname.trim()) { - showPrompt({ title: '提示', message: '请输入昵称', type: 'warning' }); - return false; - } - if (nickname.length < 2 || nickname.length > 20) { - showPrompt({ title: '提示', message: '昵称长度需在2-20个字符之间', type: 'warning' }); - return false; - } - if (!email.trim()) { - showPrompt({ title: '提示', message: '请输入邮箱', type: 'warning' }); - return false; - } - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' }); - return false; - } - if (!verificationCode.trim()) { - showPrompt({ title: '提示', message: '请输入邮箱验证码', type: 'warning' }); - return false; - } - if (phone && !/^1[3-9]\d{9}$/.test(phone)) { - showPrompt({ title: '提示', message: '请输入正确的手机号', type: 'warning' }); - return false; - } - if (!password) { - showPrompt({ title: '提示', message: '请输入密码', type: 'warning' }); - return false; - } - if (password.length < 6) { - showPrompt({ title: '提示', message: '密码长度不能少于6位', type: 'warning' }); - return false; - } - if (password !== confirmPassword) { - showPrompt({ title: '提示', message: '两次输入的密码不一致', type: 'warning' }); - return false; - } - if (!agreedToTerms) { - showPrompt({ title: '提示', message: '请同意用户协议和隐私政策', type: 'warning' }); - return false; - } - return true; - }; - - const handleSendCode = async () => { - if (!email.trim()) { - showPrompt({ title: '提示', message: '请先输入邮箱', type: 'warning' }); - return; - } - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' }); - return; - } - if (countdown > 0) { - return; - } + }, [countdown, decrementCountdown]); + // 发送验证码 + const handleSendCode = async (email: string): Promise => { setSendingCode(true); try { const ok = await authService.sendRegisterCode(email.trim()); if (ok) { - setCountdown(60); - showPrompt({ title: '发送成功', message: '验证码已发送,请查收邮箱', type: 'success' }); + setCountdown(180); // 3分钟倒计时 + showPrompt({ + title: '发送成功', + message: '验证码已发送,请查收邮箱(有效期3分钟)', + type: 'success', + }); + return true; } + return false; } catch (error: any) { - showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败,请稍后重试'), type: 'error' }); + 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 () => { - if (!validateForm()) return; - setLoading(true); try { const success = await register({ - username, - password, - nickname, - email: email.trim(), - verification_code: verificationCode.trim(), - phone: phone || undefined, + 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' }); + 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()); @@ -224,7 +242,32 @@ export const RegisterScreen: React.FC = () => { showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" > - + 0 ? handleGoBack : handleGoToLogin} + style={styles.backIconButton} + activeOpacity={0.8} + > + 0 ? 'arrow-left' : 'close'} + size={24} + color={colors.text?.primary || '#333'} + /> + + 注册 + + + + {/* 步骤指示器 */} + + + {/* 步骤内容 */} + { }, ]} > - {/* 标题区域 - 增大间距 */} - - 创建新账号 - - 填写以下信息完成注册 - - - {/* 表单区域 */} - - {/* 用户名输入框 */} - - 用户名 - - - {username.length > 0 && /^[a-zA-Z0-9_]{3,20}$/.test(username) && ( - - )} - - - - {/* 昵称输入框 */} - - 昵称 - - - {nickname.length > 0 && nickname.length >= 2 && ( - - )} - - - - {/* 邮箱输入框 */} - - 邮箱 - - - {email.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && ( - - )} - - - - {/* 邮箱验证码 */} - - 验证码 - - - - - 0) && styles.sendCodeButtonDisabled, - ]} - onPress={handleSendCode} - disabled={sendingCode || countdown > 0} - activeOpacity={0.8} - > - - {sendingCode ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码'} - - - - - - {/* 手机号输入框 */} - - 手机号(选填) - - - - - - {/* 密码输入框 */} - - 密码 - - - setShowPassword(!showPassword)} - style={styles.eyeButton} - > - - - - - - {/* 确认密码输入框 */} - - 确认密码 - - - setShowConfirmPassword(!showConfirmPassword)} - style={styles.eyeButton} - > - - - - - - {/* 服务条款 */} - setAgreedToTerms(!agreedToTerms)} - activeOpacity={0.8} - > - - - 我已阅读并同意 - 《用户协议》 - 和 - 《隐私政策》 - - - - {/* 注册按钮 */} - - {loading ? ( - - ) : ( - 注 册 - )} - - - - {/* 底部登录提示 */} - - 已有账号? - - 立即登录 - - + + + {/* 底部登录提示 */} + + 已有账号? + + 立即登录 + + @@ -460,141 +303,38 @@ function createRegisterStyles(colors: AppColors) { }, scrollContent: { flexGrow: 1, - paddingHorizontal: 28, - paddingTop: 50, - paddingBottom: 50, + paddingHorizontal: 24, + paddingTop: 20, + 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: { 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: { flexDirection: 'row', justifyContent: 'center', @@ -603,11 +343,11 @@ function createRegisterStyles(colors: AppColors) { }, footerText: { fontSize: 15, - color: colors.text.secondary, + color: colors.text?.secondary || '#666', }, loginLink: { fontSize: 15, - color: THEME_COLORS.primary, + color: '#FF6B35', fontWeight: '600', marginLeft: 6, }, diff --git a/src/screens/auth/RegisterStep1Email.tsx b/src/screens/auth/RegisterStep1Email.tsx new file mode 100644 index 0000000..b87c211 --- /dev/null +++ b/src/screens/auth/RegisterStep1Email.tsx @@ -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 = ({ + 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 ( + + + {/* 邮箱输入框 */} + + 邮箱 + + { + 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) && ( + + )} + + {emailError ? ( + {emailError} + ) : null} + + + {/* 获取验证码按钮 */} + 0) && styles.actionButtonDisabled, + ]} + onPress={handleSendCode} + disabled={sendingCode || countdown > 0} + activeOpacity={0.9} + > + + {sendingCode + ? '发送中...' + : countdown > 0 + ? `${countdown}秒后重试` + : '获取验证码'} + + + + + ); +} + +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; diff --git a/src/screens/auth/RegisterStep2Verification.tsx b/src/screens/auth/RegisterStep2Verification.tsx new file mode 100644 index 0000000..cb914f9 --- /dev/null +++ b/src/screens/auth/RegisterStep2Verification.tsx @@ -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 = ({ + 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 ( + + + 验证码已发送至 {formData.email} + + + + {/* 验证码输入 */} + + { + updateFormData({ verificationCode: value }); + if (codeError) setCodeError(''); + }} + length={6} + autoFocus={true} + onComplete={handleComplete} + disabled={isVerifying} + /> + {codeError ? ( + {codeError} + ) : null} + + + {/* 重发倒计时 */} + + {(countdown ?? 0) > 0 ? ( + + {formatCountdown(countdown!)} 后重新发送 + + ) : ( + + 重新发送验证码 + + )} + + + {/* 下一步按钮 */} + handleComplete(formData.verificationCode)} + disabled={formData.verificationCode.length !== 6 || isVerifying} + activeOpacity={0.9} + > + + {isVerifying ? '验证中...' : '下一步'} + + + + {/* 返回按钮 */} + + + 返回上一步 + + + + ); +} + +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; diff --git a/src/screens/auth/RegisterStep3Profile.tsx b/src/screens/auth/RegisterStep3Profile.tsx new file mode 100644 index 0000000..7df92fc --- /dev/null +++ b/src/screens/auth/RegisterStep3Profile.tsx @@ -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 = ({ + 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>({}); + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + 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 ( + + {label} + + { + updateFormData({ [field]: text } as Partial); + 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 && ( + + + + )} + + {error ? {error} : null} + + ); + }; + + return ( + + + {/* 用户名 */} + {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), + })} + + {/* 服务条款 */} + { + setAgreedToTerms(!agreedToTerms); + if (errors.terms) { + setErrors((prev) => ({ ...prev, terms: '' })); + } + }} + activeOpacity={0.8} + > + + + 我已阅读并同意 + 《用户协议》 + 和 + 《隐私政策》 + + + {errors.terms ? ( + + {errors.terms} + + ) : null} + + {/* 完成注册按钮 */} + + {loading ? ( + + ) : ( + 完成注册 + )} + + + {/* 返回按钮 */} + + + 返回上一步 + + + + ); +} + +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; diff --git a/src/screens/auth/index.ts b/src/screens/auth/index.ts index 620897d..d1d5bfe 100644 --- a/src/screens/auth/index.ts +++ b/src/screens/auth/index.ts @@ -7,3 +7,9 @@ export { LoginScreen } from './LoginScreen'; export { RegisterScreen } from './RegisterScreen'; export { ForgotPasswordScreen } from './ForgotPasswordScreen'; export { WelcomeScreen } from './WelcomeScreen'; + +// 注册步骤组件 +export { RegisterStep1Email } from './RegisterStep1Email'; +export { RegisterStep2Verification } from './RegisterStep2Verification'; +export { RegisterStep3Profile } from './RegisterStep3Profile'; +export type { RegisterFormData, RegisterStepProps, RegisterStep } from './types'; diff --git a/src/screens/auth/types.ts b/src/screens/auth/types.ts new file mode 100644 index 0000000..da39a2e --- /dev/null +++ b/src/screens/auth/types.ts @@ -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) => void; + onNext: () => void; + onPrev?: () => void; + colors?: AppColors; + // 状态 + loading?: boolean; + sendingCode?: boolean; + countdown?: number; + // 回调 + onSendCode?: (email: string) => Promise; +} + +export type RegisterStep = 0 | 1 | 2; + +export interface StepConfig { + id: RegisterStep; + title: string; + component: React.ComponentType; +} + +// 确保文件被识别为模块 +export {}; + diff --git a/src/services/backgroundService.ts b/src/services/backgroundService.ts index 9850a02..53f9575 100644 --- a/src/services/backgroundService.ts +++ b/src/services/backgroundService.ts @@ -32,9 +32,18 @@ const BACKGROUND_INTERVAL = 15; // 15 分钟 let isInitialized = false; let appStateSubscription: any = null; +// 导入 api 用于检查认证状态 +import { api } from './api'; + // 定义后台任务 TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { try { + // 检查是否已认证 + const token = await api.getToken(); + if (!token) { + return BackgroundFetch.BackgroundFetchResult.NoData; + } + // 检查 SSE 连接状态 if (!wsService.isConnected()) { await wsService.connect(); @@ -51,6 +60,12 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { // SSE 保活任务 TaskManager.defineTask(REALTIME_KEEPALIVE_TASK, async () => { try { + // 检查是否已认证 + const token = await api.getToken(); + if (!token) { + return BackgroundFetch.BackgroundFetchResult.NoData; + } + if (!wsService.isConnected()) { await wsService.connect(); } @@ -116,9 +131,13 @@ function setupAppStateListener(): void { return; } - appStateSubscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => { - void nextAppState; + appStateSubscription = AppState.addEventListener('change', async (nextAppState: AppStateStatus) => { if (nextAppState === 'active') { + // 检查是否已认证 + const token = await api.getToken(); + if (!token) { + return; + } // App 回到前台,确保连接 if (!wsService.isConnected()) { wsService.connect(); diff --git a/src/services/wsService.ts b/src/services/wsService.ts index d69b5c6..329b66a 100644 --- a/src/services/wsService.ts +++ b/src/services/wsService.ts @@ -335,9 +335,9 @@ class WebSocketService { try { const token = await api.getToken(); if (!token) { - console.error('[WebSocket] No token available'); + console.log('[WebSocket] No token available, skipping connection'); this.isConnecting = false; - this.scheduleReconnect(); + // 没有 token 时不要重连,等待用户登录后再连接 return false; } diff --git a/src/stores/registerStore.ts b/src/stores/registerStore.ts new file mode 100644 index 0000000..8268c77 --- /dev/null +++ b/src/stores/registerStore.ts @@ -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; + + // Actions + setCurrentStep: (step: RegisterStep) => void; + updateFormData: (data: Partial) => void; + setCountdown: (seconds: number) => void; + decrementCountdown: () => void; + markStepCompleted: (step: RegisterStep) => void; + resetRegisterData: () => void; + goToNextStep: () => void; + goToPrevStep: () => void; + // 持久化 + hydrate: () => Promise; + persistData: () => Promise; +} + +const initialFormData: RegisterFormData = { + email: '', + verificationCode: '', + username: '', + nickname: '', + phone: '', + password: '', + confirmPassword: '', +}; + +const initialCompletedSteps: Record = { + 0: false, + 1: false, + 2: false, +}; + +export const useRegisterStore = create((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); diff --git a/tsconfig.json b/tsconfig.json index d252b50..79991c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ "paths": { "@/*": ["src/*"] }, - "skipLibCheck": true + "skipLibCheck": true, + "moduleResolution": "bundler", + "resolveJsonModule": true } }