- Updated App entry point to utilize Expo Router, simplifying the navigation setup. - Removed legacy navigation components and services to streamline the codebase. - Adjusted package.json to reflect the new entry point and updated dependencies for compatibility. - Enhanced overall application structure by consolidating navigation logic and improving maintainability.
305 lines
10 KiB
TypeScript
305 lines
10 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
ScrollView,
|
||
ActivityIndicator,
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import { useRouter } from 'expo-router';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme';
|
||
import { authService, resolveAuthApiError } from '../../services/authService';
|
||
import { showPrompt } from '../../services/promptService';
|
||
|
||
export const ForgotPasswordScreen: React.FC = () => {
|
||
const router = useRouter();
|
||
const [email, setEmail] = useState('');
|
||
const [verificationCode, setVerificationCode] = useState('');
|
||
const [newPassword, setNewPassword] = useState('');
|
||
const [confirmPassword, setConfirmPassword] = useState('');
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||
const [sendingCode, setSendingCode] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [countdown, setCountdown] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (countdown <= 0) {
|
||
return;
|
||
}
|
||
const timer = setInterval(() => {
|
||
setCountdown((prev) => (prev <= 1 ? 0 : prev - 1));
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, [countdown]);
|
||
|
||
const validateEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
|
||
const handleSendCode = async () => {
|
||
if (!validateEmail(email.trim())) {
|
||
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
|
||
return;
|
||
}
|
||
if (countdown > 0) {
|
||
return;
|
||
}
|
||
setSendingCode(true);
|
||
try {
|
||
const ok = await authService.sendPasswordResetCode(email.trim());
|
||
if (ok) {
|
||
setCountdown(60);
|
||
showPrompt({ title: '发送成功', message: '验证码已发送,请查收邮箱', type: 'success' });
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败'), type: 'error' });
|
||
} finally {
|
||
setSendingCode(false);
|
||
}
|
||
};
|
||
|
||
const handleResetPassword = async () => {
|
||
if (!validateEmail(email.trim())) {
|
||
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
|
||
return;
|
||
}
|
||
if (!verificationCode.trim()) {
|
||
showPrompt({ title: '提示', message: '请输入验证码', type: 'warning' });
|
||
return;
|
||
}
|
||
if (newPassword.length < 6) {
|
||
showPrompt({ title: '提示', message: '新密码至少 6 位', type: 'warning' });
|
||
return;
|
||
}
|
||
if (newPassword !== confirmPassword) {
|
||
showPrompt({ title: '提示', message: '两次输入的密码不一致', type: 'warning' });
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
try {
|
||
const ok = await authService.resetPassword({
|
||
email: email.trim(),
|
||
verification_code: verificationCode.trim(),
|
||
new_password: newPassword,
|
||
});
|
||
if (ok) {
|
||
showPrompt({ title: '重置成功', message: '密码已重置,请重新登录', type: 'success' });
|
||
router.back();
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '重置失败', message: resolveAuthApiError(error, '请稍后重试'), type: 'error' });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<LinearGradient colors={['#FF6B35', '#FF8F66', '#FFB088']} style={styles.gradient}>
|
||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardView}>
|
||
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
||
<View style={styles.formCard}>
|
||
<Text style={styles.title}>找回密码</Text>
|
||
<Text style={styles.subtitle}>请输入邮箱并完成验证码验证</Text>
|
||
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="email-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="邮箱"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={email}
|
||
onChangeText={setEmail}
|
||
autoCapitalize="none"
|
||
keyboardType="email-address"
|
||
/>
|
||
</View>
|
||
|
||
<View style={styles.codeRow}>
|
||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||
<MaterialCommunityIcons name="shield-key-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="验证码"
|
||
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}
|
||
>
|
||
{sendingCode ? (
|
||
<ActivityIndicator size="small" color="#fff" />
|
||
) : (
|
||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="lock-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="新密码(至少6位)"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={newPassword}
|
||
onChangeText={setNewPassword}
|
||
secureTextEntry={!showPassword}
|
||
autoCapitalize="none"
|
||
/>
|
||
<TouchableOpacity onPress={() => setShowPassword((v) => !v)}>
|
||
<MaterialCommunityIcons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} color={colors.text.hint} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="lock-check-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="确认新密码"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={confirmPassword}
|
||
onChangeText={setConfirmPassword}
|
||
secureTextEntry={!showConfirmPassword}
|
||
autoCapitalize="none"
|
||
/>
|
||
<TouchableOpacity onPress={() => setShowConfirmPassword((v) => !v)}>
|
||
<MaterialCommunityIcons name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'} size={20} color={colors.text.hint} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[styles.submitButton, loading && styles.submitButtonDisabled]}
|
||
onPress={handleResetPassword}
|
||
disabled={loading}
|
||
>
|
||
{loading ? <ActivityIndicator size="small" color="#fff" /> : <Text style={styles.submitButtonText}>重置密码</Text>}
|
||
</TouchableOpacity>
|
||
|
||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||
<Text style={styles.backButtonText}>返回登录</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</ScrollView>
|
||
</KeyboardAvoidingView>
|
||
</LinearGradient>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
container: { flex: 1 },
|
||
gradient: { flex: 1 },
|
||
keyboardView: { flex: 1 },
|
||
scrollContent: {
|
||
flexGrow: 1,
|
||
justifyContent: 'center',
|
||
paddingHorizontal: spacing.xl,
|
||
paddingVertical: spacing['2xl'],
|
||
},
|
||
formCard: {
|
||
backgroundColor: colors.background.paper,
|
||
borderRadius: borderRadius['2xl'],
|
||
padding: spacing.xl,
|
||
...shadows.md,
|
||
},
|
||
title: {
|
||
fontSize: fontSizes['2xl'],
|
||
fontWeight: '700',
|
||
color: colors.text.primary,
|
||
textAlign: 'center',
|
||
marginBottom: spacing.xs,
|
||
},
|
||
subtitle: {
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.secondary,
|
||
textAlign: 'center',
|
||
marginBottom: spacing.lg,
|
||
},
|
||
inputWrapper: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: borderRadius.lg,
|
||
borderWidth: 1.5,
|
||
borderColor: colors.divider,
|
||
paddingHorizontal: spacing.md,
|
||
height: 52,
|
||
marginBottom: spacing.md,
|
||
},
|
||
inputIcon: {
|
||
marginRight: spacing.sm,
|
||
},
|
||
input: {
|
||
flex: 1,
|
||
fontSize: fontSizes.md,
|
||
color: colors.text.primary,
|
||
},
|
||
codeRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: spacing.sm,
|
||
marginBottom: spacing.md,
|
||
},
|
||
codeInput: {
|
||
flex: 1,
|
||
marginBottom: 0,
|
||
},
|
||
sendCodeButton: {
|
||
height: 52,
|
||
minWidth: 110,
|
||
borderRadius: borderRadius.lg,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingHorizontal: spacing.md,
|
||
},
|
||
sendCodeButtonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
sendCodeButtonText: {
|
||
color: '#fff',
|
||
fontSize: fontSizes.sm,
|
||
fontWeight: '600',
|
||
},
|
||
submitButton: {
|
||
height: 50,
|
||
borderRadius: borderRadius.lg,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginTop: spacing.sm,
|
||
},
|
||
submitButtonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
submitButtonText: {
|
||
color: '#fff',
|
||
fontSize: fontSizes.lg,
|
||
fontWeight: '700',
|
||
},
|
||
backButton: {
|
||
alignItems: 'center',
|
||
marginTop: spacing.md,
|
||
},
|
||
backButtonText: {
|
||
color: colors.primary.main,
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '500',
|
||
},
|
||
});
|
||
|
||
export default ForgotPasswordScreen;
|