Files
frontend/src/screens/auth/ForgotPasswordScreen.tsx
lafay 2ddb9cadd8
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m27s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h16m45s
refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- 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.
2026-03-24 14:21:31 +08:00

305 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;