/** * 登录页 LoginScreen(响应式适配) * 胡萝卜BBS - 用户登录 * 现代化设计 - 渐变背景 + 动画效果 * 登录表单在桌面端居中显示,限制最大宽度 * 支持横屏模式下的布局调整 */ import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, TextInput, KeyboardAvoidingView, Platform, ScrollView, ActivityIndicator, Animated, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; import { useAuthStore } from '../../stores'; import { RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveValue } from '../../hooks'; import { ResponsiveContainer } from '../../components/common'; import { showPrompt } from '../../services/promptService'; type LoginNavigationProp = NativeStackNavigationProp; export const LoginScreen: React.FC = () => { const navigation = useNavigation(); const login = useAuthStore((state) => state.login); const storeError = useAuthStore((state) => state.error); const setStoreError = useAuthStore((state) => state.setError); // 响应式布局 const { isWideScreen, isLandscape, isDesktop, width } = useResponsive(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); const [errorMsg, setErrorMsg] = useState(null); // 动画值 const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(50)).current; const scaleAnim = useRef(new Animated.Value(0.9)).current; const inputFadeAnim = useRef(new Animated.Value(0)).current; // 响应式值 const logoSize = useResponsiveValue({ xs: 64, sm: 72, md: 80, lg: 96, xl: 110 }); const logoContainerSize = useResponsiveValue({ xs: 110, sm: 120, md: 130, lg: 150, xl: 170 }); const appNameSize = useResponsiveValue({ xs: 28, sm: 30, md: 32, lg: 36, xl: 40 }); const formMaxWidth = isDesktop ? 480 : isWideScreen ? 420 : undefined; // 启动入场动画 useEffect(() => { Animated.sequence([ Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true, }), Animated.timing(scaleAnim, { toValue: 1, duration: 600, useNativeDriver: true, }), ]), Animated.timing(slideAnim, { toValue: 0, duration: 500, useNativeDriver: true, }), Animated.timing(inputFadeAnim, { toValue: 1, duration: 400, useNativeDriver: true, }), ]).start(); }, []); // 表单验证 const validateForm = (): boolean => { if (!username.trim()) { 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; } return true; }; // store error 同步到本地,方便在输入时清除 useEffect(() => { if (storeError) { setErrorMsg(storeError); } }, [storeError]); const clearError = () => { setErrorMsg(null); setStoreError(null); }; // 处理登录 const handleLogin = async () => { if (!validateForm()) return; clearError(); setLoading(true); try { const success = await login({ username, password }); if (!success) { // authStore 已将错误写入 storeError,useEffect 会同步到 errorMsg // 若 storeError 为空则显示通用提示 if (!useAuthStore.getState().error) { setErrorMsg('登录失败,请稍后重试'); } } } catch (error: any) { setErrorMsg(error.message || '登录失败,请检查用户名和密码'); } finally { setLoading(false); } }; // 跳转到注册页 const handleGoToRegister = () => { navigation.navigate('Register' as any); }; // 渲染表单内容 const renderFormContent = () => ( <> {/* Logo和标题区域 */} 胡萝卜 发现有趣的内容,结识志同道合的朋友 {/* 表单卡片 */} 欢迎回来 {/* 用户名输入框 */} { setUsername(v); clearError(); }} autoCapitalize="none" autoCorrect={false} returnKeyType="next" /> {username.length > 0 && ( setUsername('')} style={styles.clearButton} > )} {/* 密码输入框 */} { setPassword(v); clearError(); }} secureTextEntry={!showPassword} autoCapitalize="none" returnKeyType="done" onSubmitEditing={handleLogin} /> setShowPassword(!showPassword)} style={styles.clearButton} > {/* 忘记密码 */} navigation.navigate('ForgotPassword' as any)}> 忘记密码? {/* 内联错误提示 */} {errorMsg && ( {errorMsg} )} {/* 登录按钮 */} {loading ? ( ) : ( 登 录 )} {/* 底部注册提示 */} 还没有账号? 立即注册 ); return ( {isWideScreen ? ( {/* 装饰性背景元素 */} {renderFormContent()} ) : ( {/* 装饰性背景元素 */} {renderFormContent()} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, }, gradient: { flex: 1, }, keyboardView: { flex: 1, }, scrollContent: { flexGrow: 1, paddingHorizontal: spacing.xl, paddingVertical: spacing['2xl'], }, scrollContentLandscape: { paddingVertical: spacing.lg, }, // 装饰性背景元素 decorCircle1: { position: 'absolute', top: -100, right: -100, width: 300, height: 300, borderRadius: 150, backgroundColor: 'rgba(255,255,255,0.1)', }, decorCircle2: { position: 'absolute', bottom: 100, left: -150, width: 250, height: 250, borderRadius: 125, backgroundColor: 'rgba(255,255,255,0.08)', }, // 头部区域 headerSection: { alignItems: 'center', marginTop: spacing['3xl'], marginBottom: spacing['3xl'], }, headerSectionLandscape: { marginTop: spacing.lg, marginBottom: spacing.lg, }, logoContainer: { backgroundColor: 'rgba(255,255,255,0.25)', alignItems: 'center', justifyContent: 'center', marginBottom: spacing.lg, borderWidth: 2, borderColor: 'rgba(255,255,255,0.4)', ...shadows.md, }, appName: { fontWeight: '800', color: '#FFFFFF', marginBottom: spacing.sm, textShadowColor: 'rgba(0,0,0,0.1)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, }, subtitle: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', textAlign: 'center', paddingHorizontal: spacing.xl, }, // 表单卡片 formCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius['2xl'], padding: spacing['2xl'], marginBottom: spacing.xl, ...shadows.md, }, formTitle: { fontSize: fontSizes['2xl'], fontWeight: '700', color: colors.text.primary, marginBottom: spacing.xl, textAlign: 'center', }, inputContainer: { 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: 56, }, inputWrapperWide: { height: 60, }, inputIcon: { marginRight: spacing.sm, }, input: { flex: 1, fontSize: fontSizes.md, color: colors.text.primary, }, inputWide: { fontSize: fontSizes.lg, }, clearButton: { padding: spacing.xs, }, forgotPassword: { alignSelf: 'flex-end', marginBottom: spacing.md, }, forgotPasswordText: { fontSize: fontSizes.sm, color: colors.primary.main, fontWeight: '500', }, errorBox: { flexDirection: 'row', alignItems: 'flex-start', backgroundColor: '#FFEBEE', borderRadius: borderRadius.md, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, marginBottom: spacing.md, borderLeftWidth: 3, borderLeftColor: '#D32F2F', }, errorText: { flex: 1, fontSize: fontSizes.sm, color: '#D32F2F', lineHeight: 20, }, loginButton: { borderRadius: borderRadius.lg, overflow: 'hidden', ...shadows.md, }, loginButtonGradient: { height: 52, alignItems: 'center', justifyContent: 'center', }, loginButtonGradientWide: { height: 56, }, loginButtonDisabled: { opacity: 0.7, }, loginButtonText: { fontSize: fontSizes.lg, fontWeight: '700', color: '#FFFFFF', }, // 底部区域 footerSection: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 'auto', paddingTop: spacing.xl, }, footerText: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', }, registerLink: { fontSize: fontSizes.md, color: '#FFFFFF', fontWeight: '700', marginLeft: spacing.xs, textDecorationLine: 'underline', }, }); export default LoginScreen;