560 lines
16 KiB
TypeScript
560 lines
16 KiB
TypeScript
|
|
/**
|
|||
|
|
* 登录页 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<RootStackParamList, 'Auth'>;
|
|||
|
|
|
|||
|
|
export const LoginScreen: React.FC = () => {
|
|||
|
|
const navigation = useNavigation<LoginNavigationProp>();
|
|||
|
|
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<string | null>(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和标题区域 */}
|
|||
|
|
<Animated.View
|
|||
|
|
style={[
|
|||
|
|
styles.headerSection,
|
|||
|
|
isLandscape && styles.headerSectionLandscape,
|
|||
|
|
{
|
|||
|
|
opacity: fadeAnim,
|
|||
|
|
transform: [{ scale: scaleAnim }],
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<View style={[
|
|||
|
|
styles.logoContainer,
|
|||
|
|
{ width: logoContainerSize, height: logoContainerSize, borderRadius: logoContainerSize / 2 }
|
|||
|
|
]}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="carrot"
|
|||
|
|
size={logoSize}
|
|||
|
|
color="#FFF"
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
<Text style={[styles.appName, { fontSize: appNameSize }]}>胡萝卜</Text>
|
|||
|
|
<Text style={styles.subtitle}>发现有趣的内容,结识志同道合的朋友</Text>
|
|||
|
|
</Animated.View>
|
|||
|
|
|
|||
|
|
{/* 表单卡片 */}
|
|||
|
|
<Animated.View
|
|||
|
|
style={[
|
|||
|
|
styles.formCard,
|
|||
|
|
formMaxWidth && { maxWidth: formMaxWidth, alignSelf: 'center', width: '100%' },
|
|||
|
|
{
|
|||
|
|
opacity: inputFadeAnim,
|
|||
|
|
transform: [{ translateY: slideAnim }],
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Text style={styles.formTitle}>欢迎回来</Text>
|
|||
|
|
|
|||
|
|
{/* 用户名输入框 */}
|
|||
|
|
<View style={styles.inputContainer}>
|
|||
|
|
<View style={[
|
|||
|
|
styles.inputWrapper,
|
|||
|
|
isWideScreen && styles.inputWrapperWide,
|
|||
|
|
]}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="account-outline"
|
|||
|
|
size={22}
|
|||
|
|
color={colors.primary.main}
|
|||
|
|
style={styles.inputIcon}
|
|||
|
|
/>
|
|||
|
|
<TextInput
|
|||
|
|
style={[
|
|||
|
|
styles.input,
|
|||
|
|
isWideScreen && styles.inputWide,
|
|||
|
|
]}
|
|||
|
|
placeholder="用户名 / 邮箱 / 手机号"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
value={username}
|
|||
|
|
onChangeText={(v) => { setUsername(v); clearError(); }}
|
|||
|
|
autoCapitalize="none"
|
|||
|
|
autoCorrect={false}
|
|||
|
|
returnKeyType="next"
|
|||
|
|
/>
|
|||
|
|
{username.length > 0 && (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
onPress={() => setUsername('')}
|
|||
|
|
style={styles.clearButton}
|
|||
|
|
>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="close-circle"
|
|||
|
|
size={20}
|
|||
|
|
color={colors.text.hint}
|
|||
|
|
/>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 密码输入框 */}
|
|||
|
|
<View style={styles.inputContainer}>
|
|||
|
|
<View style={[
|
|||
|
|
styles.inputWrapper,
|
|||
|
|
isWideScreen && styles.inputWrapperWide,
|
|||
|
|
]}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="lock-outline"
|
|||
|
|
size={22}
|
|||
|
|
color={colors.primary.main}
|
|||
|
|
style={styles.inputIcon}
|
|||
|
|
/>
|
|||
|
|
<TextInput
|
|||
|
|
style={[
|
|||
|
|
styles.input,
|
|||
|
|
isWideScreen && styles.inputWide,
|
|||
|
|
]}
|
|||
|
|
placeholder="请输入密码"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
value={password}
|
|||
|
|
onChangeText={(v) => { setPassword(v); clearError(); }}
|
|||
|
|
secureTextEntry={!showPassword}
|
|||
|
|
autoCapitalize="none"
|
|||
|
|
returnKeyType="done"
|
|||
|
|
onSubmitEditing={handleLogin}
|
|||
|
|
/>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
onPress={() => setShowPassword(!showPassword)}
|
|||
|
|
style={styles.clearButton}
|
|||
|
|
>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
|||
|
|
size={20}
|
|||
|
|
color={colors.text.hint}
|
|||
|
|
/>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 忘记密码 */}
|
|||
|
|
<TouchableOpacity style={styles.forgotPassword} onPress={() => navigation.navigate('ForgotPassword' as any)}>
|
|||
|
|
<Text style={styles.forgotPasswordText}>忘记密码?</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
|
|||
|
|
{/* 内联错误提示 */}
|
|||
|
|
{errorMsg && (
|
|||
|
|
<View style={styles.errorBox}>
|
|||
|
|
<MaterialCommunityIcons
|
|||
|
|
name="alert-circle-outline"
|
|||
|
|
size={16}
|
|||
|
|
color={colors.error?.main ?? '#D32F2F'}
|
|||
|
|
style={{ marginRight: 6, marginTop: 1 }}
|
|||
|
|
/>
|
|||
|
|
<Text style={styles.errorText}>{errorMsg}</Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 登录按钮 */}
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.loginButton, loading && styles.loginButtonDisabled]}
|
|||
|
|
onPress={handleLogin}
|
|||
|
|
disabled={loading}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<LinearGradient
|
|||
|
|
colors={['#FF6B35', '#FF8F66']}
|
|||
|
|
start={{ x: 0, y: 0 }}
|
|||
|
|
end={{ x: 1, y: 0 }}
|
|||
|
|
style={[
|
|||
|
|
styles.loginButtonGradient,
|
|||
|
|
isWideScreen && styles.loginButtonGradientWide,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{loading ? (
|
|||
|
|
<ActivityIndicator size="small" color="#FFF" />
|
|||
|
|
) : (
|
|||
|
|
<Text style={styles.loginButtonText}>登 录</Text>
|
|||
|
|
)}
|
|||
|
|
</LinearGradient>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</Animated.View>
|
|||
|
|
|
|||
|
|
{/* 底部注册提示 */}
|
|||
|
|
<Animated.View
|
|||
|
|
style={[
|
|||
|
|
styles.footerSection,
|
|||
|
|
{ opacity: inputFadeAnim },
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Text style={styles.footerText}>还没有账号?</Text>
|
|||
|
|
<TouchableOpacity onPress={handleGoToRegister}>
|
|||
|
|
<Text style={styles.registerLink}>立即注册</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</Animated.View>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView style={styles.container}>
|
|||
|
|
<LinearGradient
|
|||
|
|
colors={['#FF6B35', '#FF8F66', '#FFB088']}
|
|||
|
|
start={{ x: 0, y: 0 }}
|
|||
|
|
end={{ x: 1, y: 1 }}
|
|||
|
|
style={styles.gradient}
|
|||
|
|
>
|
|||
|
|
<KeyboardAvoidingView
|
|||
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|||
|
|
style={styles.keyboardView}
|
|||
|
|
>
|
|||
|
|
{isWideScreen ? (
|
|||
|
|
<ResponsiveContainer maxWidth={600}>
|
|||
|
|
<ScrollView
|
|||
|
|
contentContainerStyle={[
|
|||
|
|
styles.scrollContent,
|
|||
|
|
isLandscape && styles.scrollContentLandscape,
|
|||
|
|
]}
|
|||
|
|
showsVerticalScrollIndicator={false}
|
|||
|
|
keyboardShouldPersistTaps="handled"
|
|||
|
|
>
|
|||
|
|
{/* 装饰性背景元素 */}
|
|||
|
|
<View style={styles.decorCircle1} />
|
|||
|
|
<View style={styles.decorCircle2} />
|
|||
|
|
|
|||
|
|
{renderFormContent()}
|
|||
|
|
</ScrollView>
|
|||
|
|
</ResponsiveContainer>
|
|||
|
|
) : (
|
|||
|
|
<ScrollView
|
|||
|
|
contentContainerStyle={[
|
|||
|
|
styles.scrollContent,
|
|||
|
|
isLandscape && styles.scrollContentLandscape,
|
|||
|
|
]}
|
|||
|
|
showsVerticalScrollIndicator={false}
|
|||
|
|
keyboardShouldPersistTaps="handled"
|
|||
|
|
>
|
|||
|
|
{/* 装饰性背景元素 */}
|
|||
|
|
<View style={styles.decorCircle1} />
|
|||
|
|
<View style={styles.decorCircle2} />
|
|||
|
|
|
|||
|
|
{renderFormContent()}
|
|||
|
|
</ScrollView>
|
|||
|
|
)}
|
|||
|
|
</KeyboardAvoidingView>
|
|||
|
|
</LinearGradient>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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;
|