feat(auth): redesign auth screens with clean form style and add welcome page
- Replace split/responsive layout with unified clean form design - Update LoginScreen, RegisterScreen, and ForgotPasswordScreen styles - Add WelcomeScreen as new entry point for unauthenticated users - Redirect unauthenticated users to /welcome instead of /login - Add hrefAuthWelcome() navigation helper BREAKING CHANGE: Auth screens no longer support split layout, unified mobile-first design
This commit is contained in:
@@ -7,5 +7,5 @@ export default function Index() {
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return <Redirect href="/home" />;
|
return <Redirect href="/home" />;
|
||||||
}
|
}
|
||||||
return <Redirect href="/login" />;
|
return <Redirect href="/welcome" />;
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/welcome.tsx
Normal file
5
app/welcome.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { WelcomeScreen } from '../src/screens/auth';
|
||||||
|
|
||||||
|
export default function WelcomeRoute() {
|
||||||
|
return <WelcomeScreen />;
|
||||||
|
}
|
||||||
@@ -156,6 +156,10 @@ export function hrefAuthForgot(): string {
|
|||||||
return '/forgot-password';
|
return '/forgot-password';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hrefAuthWelcome(): string {
|
||||||
|
return '/welcome';
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Materials (学习资料) ====================
|
// ==================== Materials (学习资料) ====================
|
||||||
|
|
||||||
export function hrefMaterials(): string {
|
export function hrefMaterials(): string {
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
/**
|
||||||
|
* 忘记密码页 ForgotPasswordScreen(简洁表单样式)
|
||||||
|
* 胡萝卜BBS - 找回密码
|
||||||
|
*
|
||||||
|
* 设计风格:
|
||||||
|
* - 纯白背景,扁平化设计
|
||||||
|
* - 增大间距
|
||||||
|
* - 输入框带灰色背景填充
|
||||||
|
* - 橙色圆角按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -9,123 +20,28 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Animated,
|
||||||
|
StatusBar,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
|
|
||||||
import { authService, resolveAuthApiError } from '../../services/authService';
|
import { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
|
|
||||||
function createForgotPasswordStyles(colors: AppColors) {
|
// 胡萝卜橙主题色
|
||||||
return StyleSheet.create({
|
const THEME_COLORS = {
|
||||||
container: { flex: 1 },
|
primary: '#FF6B35',
|
||||||
gradient: { flex: 1 },
|
primaryLight: '#FF8C5A',
|
||||||
keyboardView: { flex: 1 },
|
primaryDark: '#E55A2B',
|
||||||
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 const ForgotPasswordScreen: React.FC = () => {
|
export const ForgotPasswordScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
|
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [verificationCode, setVerificationCode] = useState('');
|
const [verificationCode, setVerificationCode] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
@@ -136,6 +52,26 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(0);
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
|
||||||
|
// 动画值
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const slideAnim = useRef(new Animated.Value(20)).current;
|
||||||
|
|
||||||
|
// 启动入场动画
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 400,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 400,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (countdown <= 0) {
|
if (countdown <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -208,101 +144,308 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<LinearGradient colors={['#FF6B35', '#FF8F66', '#FFB088']} style={styles.gradient}>
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardView}>
|
<KeyboardAvoidingView
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
<View style={styles.formCard}>
|
style={styles.keyboardView}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.content,
|
||||||
|
{
|
||||||
|
opacity: fadeAnim,
|
||||||
|
transform: [{ translateY: slideAnim }],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* 标题区域 */}
|
||||||
|
<View style={styles.titleSection}>
|
||||||
<Text style={styles.title}>找回密码</Text>
|
<Text style={styles.title}>找回密码</Text>
|
||||||
<Text style={styles.subtitle}>请输入邮箱并完成验证码验证</Text>
|
<View style={styles.underline} />
|
||||||
|
<Text style={styles.subtitle}>请输入邮箱并完成验证</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputWrapper}>
|
{/* 表单区域 */}
|
||||||
<MaterialCommunityIcons name="email-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
<View style={styles.formSection}>
|
||||||
<TextInput
|
{/* 邮箱输入框 */}
|
||||||
style={styles.input}
|
<View style={styles.inputContainer}>
|
||||||
placeholder="邮箱"
|
<Text style={styles.inputLabel}>邮箱</Text>
|
||||||
placeholderTextColor={colors.text.hint}
|
<View style={styles.inputWrapper}>
|
||||||
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
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="验证码"
|
placeholder="请输入邮箱地址"
|
||||||
placeholderTextColor={colors.text.hint}
|
placeholderTextColor={colors.text.hint}
|
||||||
value={verificationCode}
|
value={email}
|
||||||
onChangeText={setVerificationCode}
|
onChangeText={setEmail}
|
||||||
keyboardType="number-pad"
|
autoCapitalize="none"
|
||||||
maxLength={6}
|
keyboardType="email-address"
|
||||||
/>
|
/>
|
||||||
</View>
|
{email.length > 0 && validateEmail(email) && (
|
||||||
<TouchableOpacity
|
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
|
||||||
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>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputWrapper}>
|
{/* 验证码 */}
|
||||||
<MaterialCommunityIcons name="lock-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
<View style={styles.inputContainer}>
|
||||||
<TextInput
|
<Text style={styles.inputLabel}>验证码</Text>
|
||||||
style={styles.input}
|
<View style={styles.codeRow}>
|
||||||
placeholder="新密码(至少6位)"
|
<View style={[styles.inputWrapper, styles.codeInputWrapper]}>
|
||||||
placeholderTextColor={colors.text.hint}
|
<TextInput
|
||||||
value={newPassword}
|
style={styles.input}
|
||||||
onChangeText={setNewPassword}
|
placeholder="6位验证码"
|
||||||
secureTextEntry={!showPassword}
|
placeholderTextColor={colors.text.hint}
|
||||||
autoCapitalize="none"
|
value={verificationCode}
|
||||||
/>
|
onChangeText={setVerificationCode}
|
||||||
<TouchableOpacity onPress={() => setShowPassword((v) => !v)}>
|
keyboardType="number-pad"
|
||||||
<MaterialCommunityIcons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} color={colors.text.hint} />
|
maxLength={6}
|
||||||
</TouchableOpacity>
|
/>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.sendCodeButton, (sendingCode || countdown > 0) && styles.sendCodeButtonDisabled]}
|
||||||
|
onPress={handleSendCode}
|
||||||
|
disabled={sendingCode || countdown > 0}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{sendingCode ? (
|
||||||
|
<ActivityIndicator size="small" color={THEME_COLORS.primary} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '获取验证码'}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputWrapper}>
|
{/* 新密码输入框 */}
|
||||||
<MaterialCommunityIcons name="lock-check-outline" size={22} color={colors.primary.main} style={styles.inputIcon} />
|
<View style={styles.inputContainer}>
|
||||||
<TextInput
|
<Text style={styles.inputLabel}>新密码</Text>
|
||||||
style={styles.input}
|
<View style={styles.inputWrapper}>
|
||||||
placeholder="确认新密码"
|
<TextInput
|
||||||
placeholderTextColor={colors.text.hint}
|
style={styles.input}
|
||||||
value={confirmPassword}
|
placeholder="至少6位"
|
||||||
onChangeText={setConfirmPassword}
|
placeholderTextColor={colors.text.hint}
|
||||||
secureTextEntry={!showConfirmPassword}
|
value={newPassword}
|
||||||
autoCapitalize="none"
|
onChangeText={setNewPassword}
|
||||||
/>
|
secureTextEntry={!showPassword}
|
||||||
<TouchableOpacity onPress={() => setShowConfirmPassword((v) => !v)}>
|
autoCapitalize="none"
|
||||||
<MaterialCommunityIcons name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'} size={20} color={colors.text.hint} />
|
/>
|
||||||
</TouchableOpacity>
|
<TouchableOpacity
|
||||||
|
onPress={() => setShowPassword((v) => !v)}
|
||||||
|
style={styles.eyeButton}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||||
|
size={20}
|
||||||
|
color={colors.text.hint}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 确认密码输入框 */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Text style={styles.inputLabel}>确认密码</Text>
|
||||||
|
<View style={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="再次输入新密码"
|
||||||
|
placeholderTextColor={colors.text.hint}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChangeText={setConfirmPassword}
|
||||||
|
secureTextEntry={!showConfirmPassword}
|
||||||
|
autoCapitalize="none"
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={handleResetPassword}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setShowConfirmPassword((v) => !v)}
|
||||||
|
style={styles.eyeButton}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||||
|
size={20}
|
||||||
|
color={colors.text.hint}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 重置密码按钮 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.submitButton, loading && styles.submitButtonDisabled]}
|
style={[styles.submitButton, loading && styles.submitButtonDisabled]}
|
||||||
onPress={handleResetPassword}
|
onPress={handleResetPassword}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
{loading ? <ActivityIndicator size="small" color="#fff" /> : <Text style={styles.submitButtonText}>重置密码</Text>}
|
{loading ? (
|
||||||
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.submitButtonText}>重置密码</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
{/* 返回登录 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={() => router.back()}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="arrow-left" size={18} color={THEME_COLORS.primary} style={{ marginRight: 4 }} />
|
||||||
<Text style={styles.backButtonText}>返回登录</Text>
|
<Text style={styles.backButtonText}>返回登录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</Animated.View>
|
||||||
</KeyboardAvoidingView>
|
</ScrollView>
|
||||||
</LinearGradient>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createForgotPasswordStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
keyboardView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingHorizontal: 28,
|
||||||
|
paddingTop: 50,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
maxWidth: 400,
|
||||||
|
width: '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 标题区域
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 40,
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提交按钮
|
||||||
|
submitButton: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: THEME_COLORS.primary,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
submitButtonDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
submitButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 返回按钮
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 24,
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
color: THEME_COLORS.primary,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default ForgotPasswordScreen;
|
export default ForgotPasswordScreen;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* 登录页 LoginScreen(响应式适配 - 分栏布局)
|
* 登录页 LoginScreen(简洁表单样式)
|
||||||
* 胡萝卜BBS - 用户登录
|
* 胡萝卜BBS - 用户登录
|
||||||
*
|
*
|
||||||
* 布局规则:
|
* 设计风格:
|
||||||
* - 屏幕宽度 < 768px:单栏布局,橙色渐变背景
|
* - 纯白背景,扁平化设计
|
||||||
* - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰
|
* - 增大间距,避免页面太空
|
||||||
|
* - 输入框带灰色背景填充
|
||||||
|
* - 橙色圆角按钮
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
@@ -24,15 +26,18 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
|
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { useResponsive, useResponsiveValue } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
|
|
||||||
// 分栏布局断点
|
// 胡萝卜橙主题色
|
||||||
const SPLIT_BREAKPOINT = 768;
|
const THEME_COLORS = {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryDark: '#E55A2B',
|
||||||
|
};
|
||||||
|
|
||||||
export const LoginScreen: React.FC = () => {
|
export const LoginScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
@@ -40,67 +45,29 @@ export const LoginScreen: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const login = useAuthStore((state) => state.login);
|
const login = useAuthStore((state) => state.login);
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
const storeError = useAuthStore((state) => state.error);
|
|
||||||
const setStoreError = useAuthStore((state) => state.setError);
|
const setStoreError = useAuthStore((state) => state.setError);
|
||||||
|
|
||||||
// 响应式布局
|
const { isLandscape } = useResponsive();
|
||||||
const { isLandscape, width } = useResponsive();
|
|
||||||
|
|
||||||
// 是否使用分栏布局
|
|
||||||
const isSplitLayout = width >= SPLIT_BREAKPOINT;
|
|
||||||
|
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 动画值
|
// 动画值
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
const slideAnim = useRef(new Animated.Value(50)).current;
|
const slideAnim = useRef(new Animated.Value(20)).current;
|
||||||
const scaleAnim = useRef(new Animated.Value(0.9)).current;
|
|
||||||
const inputFadeAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
// 响应式值 - 大屏模式单独放大Logo尺寸,更显大气
|
|
||||||
const logoSize = useResponsiveValue({
|
|
||||||
xs: 64, sm: 72, md: 80,
|
|
||||||
lg: isSplitLayout ? 140 : 96, // 大屏模式Logo图标进一步放大
|
|
||||||
xl: isSplitLayout ? 160 : 110
|
|
||||||
});
|
|
||||||
const logoContainerSize = useResponsiveValue({
|
|
||||||
xs: 110, sm: 120, md: 130,
|
|
||||||
lg: isSplitLayout ? 200 : 150, // 大屏模式Logo容器进一步放大
|
|
||||||
xl: isSplitLayout ? 220 : 170
|
|
||||||
});
|
|
||||||
const appNameSize = useResponsiveValue({
|
|
||||||
xs: 28, sm: 30, md: 32,
|
|
||||||
lg: isSplitLayout ? 60 : 36, // 大屏模式标题文字大幅放大
|
|
||||||
xl: isSplitLayout ? 72 : 40
|
|
||||||
});
|
|
||||||
const formMaxWidth = isSplitLayout ? 420 : undefined;
|
|
||||||
|
|
||||||
// 启动入场动画
|
// 启动入场动画
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Animated.sequence([
|
Animated.parallel([
|
||||||
Animated.parallel([
|
Animated.timing(fadeAnim, {
|
||||||
Animated.timing(fadeAnim, {
|
toValue: 1,
|
||||||
toValue: 1,
|
duration: 400,
|
||||||
duration: 600,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(scaleAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 600,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
Animated.timing(slideAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 500,
|
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(inputFadeAnim, {
|
Animated.timing(slideAnim, {
|
||||||
toValue: 1,
|
toValue: 0,
|
||||||
duration: 400,
|
duration: 400,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
@@ -124,13 +91,6 @@ export const LoginScreen: React.FC = () => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// store error 同步到本地
|
|
||||||
useEffect(() => {
|
|
||||||
if (storeError) {
|
|
||||||
setErrorMsg(storeError);
|
|
||||||
}
|
|
||||||
}, [storeError]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
router.replace(hrefs.hrefHome());
|
router.replace(hrefs.hrefHome());
|
||||||
@@ -138,7 +98,6 @@ export const LoginScreen: React.FC = () => {
|
|||||||
}, [isAuthenticated, router]);
|
}, [isAuthenticated, router]);
|
||||||
|
|
||||||
const clearError = () => {
|
const clearError = () => {
|
||||||
setErrorMsg(null);
|
|
||||||
setStoreError(null);
|
setStoreError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,11 +111,11 @@ export const LoginScreen: React.FC = () => {
|
|||||||
const success = await login({ username, password });
|
const success = await login({ username, password });
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (!useAuthStore.getState().error) {
|
if (!useAuthStore.getState().error) {
|
||||||
setErrorMsg('登录失败,请稍后重试');
|
showPrompt({ title: '登录失败', message: '请检查用户名和密码', type: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setErrorMsg(error.message || '登录失败,请检查用户名和密码');
|
showPrompt({ title: '登录失败', message: error.message || '请检查用户名和密码', type: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -167,604 +126,256 @@ export const LoginScreen: React.FC = () => {
|
|||||||
router.push(hrefs.hrefAuthRegister());
|
router.push(hrefs.hrefAuthRegister());
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染左侧面板(Logo和品牌信息)- 优化大屏布局
|
return (
|
||||||
const renderLeftPanel = () => (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.leftPanel,
|
|
||||||
isSplitLayout && styles.splitLeftPanel, // 大屏模式专用布局
|
|
||||||
{
|
|
||||||
opacity: fadeAnim,
|
|
||||||
transform: [{ scale: scaleAnim }],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* 胡萝卜图标 - 靠上显示 */}
|
|
||||||
<View style={[
|
|
||||||
styles.logoContainer,
|
|
||||||
styles.logoContainerTop,
|
|
||||||
isSplitLayout && styles.splitLogoContainer, // 大屏模式Logo容器样式
|
|
||||||
{ width: logoContainerSize, height: logoContainerSize, borderRadius: logoContainerSize / 2 }
|
|
||||||
]}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="carrot"
|
|
||||||
size={logoSize}
|
|
||||||
color="#FFF"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 品牌名称 - 优化大屏排版 */}
|
|
||||||
<Text style={[styles.appName, isSplitLayout && styles.splitAppName]}>萝卜社区</Text>
|
|
||||||
<Text style={[styles.subtitle, isSplitLayout && styles.splitSubtitle]}>发现有趣的内容,结识志同道合的朋友</Text>
|
|
||||||
|
|
||||||
{/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */}
|
|
||||||
<View style={[styles.leftPanelDecorBottom, isSplitLayout && styles.splitLeftPanelDecorBottom]}>
|
|
||||||
{isSplitLayout ? (
|
|
||||||
// 大屏模式:水平排列 + 大幅放大图标
|
|
||||||
<View style={styles.splitDecorRow}>
|
|
||||||
<MaterialCommunityIcons name="chat-outline" size={80} color="rgba(255,255,255,0.4)" style={styles.splitDecorIcon} />
|
|
||||||
<MaterialCommunityIcons name="account-group-outline" size={90} color="rgba(255,255,255,0.45)" style={styles.splitDecorIcon} />
|
|
||||||
<MaterialCommunityIcons name="bell-outline" size={85} color="rgba(255,255,255,0.35)" style={styles.splitDecorIcon} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
// 小屏模式:保持原有横向排列
|
|
||||||
<View style={styles.decorRow}>
|
|
||||||
<MaterialCommunityIcons name="chat-outline" size={48} color="rgba(255,255,255,0.35)" />
|
|
||||||
<MaterialCommunityIcons name="account-group-outline" size={44} color="rgba(255,255,255,0.3)" />
|
|
||||||
<MaterialCommunityIcons name="bell-outline" size={40} color="rgba(255,255,255,0.25)" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染表单内容
|
|
||||||
const renderFormContent = () => (
|
|
||||||
<View style={isSplitLayout ? styles.splitFormWrapper : styles.singleFormWrapper}>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.formCard,
|
|
||||||
// 核心修改:大屏模式使用专用的阴影样式,小屏用原有阴影
|
|
||||||
isSplitLayout ? styles.splitFormCardShadow : styles.normalFormCardShadow,
|
|
||||||
formMaxWidth && { maxWidth: formMaxWidth, width: '100%' },
|
|
||||||
{
|
|
||||||
opacity: inputFadeAnim,
|
|
||||||
transform: [{ translateY: slideAnim }],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.formTitle}>欢迎回来</Text>
|
|
||||||
|
|
||||||
{/* 用户名输入框 */}
|
|
||||||
<View style={styles.inputContainer}>
|
|
||||||
<View style={styles.inputWrapper}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="account-outline"
|
|
||||||
size={22}
|
|
||||||
color={colors.primary.main}
|
|
||||||
style={styles.inputIcon}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
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}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="lock-outline"
|
|
||||||
size={22}
|
|
||||||
color={colors.primary.main}
|
|
||||||
style={styles.inputIcon}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
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={() => router.push(hrefs.hrefAuthForgot())}
|
|
||||||
>
|
|
||||||
<Text style={styles.forgotPasswordText}>忘记密码?</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* 内联错误提示 */}
|
|
||||||
{errorMsg && (
|
|
||||||
<View style={styles.errorBox}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="alert-circle-outline"
|
|
||||||
size={16}
|
|
||||||
color={colors.error.dark}
|
|
||||||
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={[colors.primary.main, colors.primary.light]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={styles.loginButtonGradient}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染分栏布局
|
|
||||||
const renderSplitLayout = () => (
|
|
||||||
<View style={styles.splitContainer}>
|
|
||||||
{/* 左侧橙色面板 */}
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.primary.main, colors.primary.light, '#FFB088']}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.leftPanelContainer}
|
|
||||||
>
|
|
||||||
{/* 装饰性背景元素 */}
|
|
||||||
<View style={styles.decorCircle1} />
|
|
||||||
<View style={styles.decorCircle2} />
|
|
||||||
|
|
||||||
<StatusBar barStyle="light-content" />
|
|
||||||
<SafeAreaView style={styles.leftPanelSafeArea}>
|
|
||||||
{renderLeftPanel()}
|
|
||||||
</SafeAreaView>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{/* 右侧中性灰面板 */}
|
|
||||||
<View style={styles.rightPanelContainer}>
|
|
||||||
<StatusBar barStyle="dark-content" />
|
|
||||||
<SafeAreaView style={styles.rightPanelSafeArea}>
|
|
||||||
{/* 核心修复:用View包裹,确保垂直+水平居中 */}
|
|
||||||
<View style={styles.splitRightContentWrapper}>
|
|
||||||
{renderFormContent()}
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染单栏布局
|
|
||||||
const renderSingleLayout = () => (
|
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||||||
<LinearGradient
|
<KeyboardAvoidingView
|
||||||
colors={[colors.primary.main, colors.primary.light, '#FFB088']}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
start={{ x: 0, y: 0 }}
|
style={styles.keyboardView}
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.gradient}
|
|
||||||
>
|
>
|
||||||
<KeyboardAvoidingView
|
<ScrollView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
contentContainerStyle={[
|
||||||
style={styles.keyboardView}
|
styles.scrollContent,
|
||||||
|
isLandscape && styles.scrollContentLandscape,
|
||||||
|
]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<ScrollView
|
<Animated.View
|
||||||
contentContainerStyle={[
|
style={[
|
||||||
styles.scrollContent,
|
styles.content,
|
||||||
isLandscape && styles.scrollContentLandscape,
|
{
|
||||||
|
opacity: fadeAnim,
|
||||||
|
transform: [{ translateY: slideAnim }],
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
keyboardShouldPersistTaps="handled"
|
|
||||||
>
|
>
|
||||||
{/* 装饰性背景元素 */}
|
{/* 标题区域 - 增大间距 */}
|
||||||
<View style={styles.decorCircle1} />
|
<View style={styles.titleSection}>
|
||||||
<View style={styles.decorCircle2} />
|
<Text style={styles.title}>欢迎回来</Text>
|
||||||
|
<View style={styles.underline} />
|
||||||
|
<Text style={styles.subtitle}>请登录您的账号</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* 头部 */}
|
{/* 表单区域 */}
|
||||||
<Animated.View
|
<View style={styles.formSection}>
|
||||||
style={[
|
{/* 用户名输入框 */}
|
||||||
styles.headerSection,
|
<View style={styles.inputContainer}>
|
||||||
isLandscape && styles.headerSectionLandscape,
|
<Text style={styles.inputLabel}>用户名</Text>
|
||||||
{
|
<View style={styles.inputWrapper}>
|
||||||
opacity: fadeAnim,
|
<TextInput
|
||||||
transform: [{ scale: scaleAnim }],
|
style={styles.input}
|
||||||
},
|
placeholder="请输入用户名"
|
||||||
]}
|
placeholderTextColor={colors.text.hint}
|
||||||
>
|
value={username}
|
||||||
<View style={[
|
onChangeText={(v) => { setUsername(v); clearError(); }}
|
||||||
styles.logoContainer,
|
autoCapitalize="none"
|
||||||
{ width: logoContainerSize, height: logoContainerSize, borderRadius: logoContainerSize / 2 }
|
autoCorrect={false}
|
||||||
]}>
|
returnKeyType="next"
|
||||||
<MaterialCommunityIcons
|
/>
|
||||||
name="carrot"
|
{username.length > 0 && (
|
||||||
size={logoSize}
|
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
|
||||||
color="#FFF"
|
)}
|
||||||
/>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.appName, { fontSize: appNameSize }]}>萝卜社区</Text>
|
|
||||||
<Text style={styles.subtitle}>发现有趣的内容,结识志同道合的朋友</Text>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
{/* 表单 */}
|
{/* 密码输入框 */}
|
||||||
{renderFormContent()}
|
<View style={styles.inputContainer}>
|
||||||
</ScrollView>
|
<Text style={styles.inputLabel}>密码</Text>
|
||||||
</KeyboardAvoidingView>
|
<View style={styles.inputWrapper}>
|
||||||
</LinearGradient>
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
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.eyeButton}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||||
|
size={20}
|
||||||
|
color={colors.text.hint}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 忘记密码 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.forgotPassword}
|
||||||
|
onPress={() => router.push(hrefs.hrefAuthForgot())}
|
||||||
|
>
|
||||||
|
<Text style={styles.forgotPasswordText}>忘记密码?</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 登录按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.loginButton, loading && styles.loginButtonDisabled]}
|
||||||
|
onPress={handleLogin}
|
||||||
|
disabled={loading}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator size="small" color="#FFF" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.loginButtonText}>登 录</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部注册提示 */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Text style={styles.footerText}>还没有账号?</Text>
|
||||||
|
<TouchableOpacity onPress={handleGoToRegister}>
|
||||||
|
<Text style={styles.registerLink}>立即注册</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isSplitLayout ? renderSplitLayout() : renderSingleLayout();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createLoginStyles(colors: AppColors) {
|
function createLoginStyles(colors: AppColors) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
backgroundColor: '#fff',
|
||||||
gradient: {
|
},
|
||||||
flex: 1,
|
keyboardView: {
|
||||||
},
|
flex: 1,
|
||||||
keyboardView: {
|
},
|
||||||
flex: 1,
|
scrollContent: {
|
||||||
},
|
flexGrow: 1,
|
||||||
// 单栏布局内容
|
paddingHorizontal: 28,
|
||||||
scrollContent: {
|
paddingTop: 60,
|
||||||
flexGrow: 1,
|
paddingBottom: 40,
|
||||||
paddingHorizontal: spacing.xl,
|
},
|
||||||
paddingVertical: spacing['2xl'],
|
scrollContentLandscape: {
|
||||||
},
|
paddingTop: 30,
|
||||||
scrollContentCentered: {
|
},
|
||||||
alignItems: 'center',
|
content: {
|
||||||
justifyContent: 'center',
|
flex: 1,
|
||||||
flex: 1,
|
maxWidth: 400,
|
||||||
},
|
width: '100%',
|
||||||
scrollContentLandscape: {
|
alignSelf: 'center',
|
||||||
paddingVertical: spacing.lg,
|
},
|
||||||
},
|
|
||||||
// 装饰性背景元素
|
// 标题区域 - 增大间距
|
||||||
decorCircle1: {
|
titleSection: {
|
||||||
position: 'absolute',
|
marginBottom: 48,
|
||||||
top: -100,
|
marginTop: 20,
|
||||||
right: -100,
|
},
|
||||||
width: 300,
|
title: {
|
||||||
height: 300,
|
fontSize: 32,
|
||||||
borderRadius: 150,
|
fontWeight: '700',
|
||||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
color: colors.text.primary,
|
||||||
},
|
lineHeight: 40,
|
||||||
decorCircle2: {
|
},
|
||||||
position: 'absolute',
|
underline: {
|
||||||
bottom: 100,
|
width: 40,
|
||||||
left: -150,
|
height: 4,
|
||||||
width: 250,
|
backgroundColor: THEME_COLORS.primary,
|
||||||
height: 250,
|
borderRadius: 2,
|
||||||
borderRadius: 125,
|
marginTop: 12,
|
||||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
// 头部区域
|
subtitle: {
|
||||||
headerSection: {
|
fontSize: 16,
|
||||||
alignItems: 'center',
|
color: colors.text.secondary,
|
||||||
marginTop: spacing['3xl'],
|
},
|
||||||
marginBottom: spacing['3xl'],
|
|
||||||
},
|
// 表单区域
|
||||||
headerSectionLandscape: {
|
formSection: {
|
||||||
marginTop: spacing.lg,
|
marginBottom: 32,
|
||||||
marginBottom: spacing.lg,
|
},
|
||||||
},
|
|
||||||
logoContainer: {
|
// 输入框
|
||||||
backgroundColor: 'rgba(255,255,255,0.25)',
|
inputContainer: {
|
||||||
alignItems: 'center',
|
marginBottom: 24,
|
||||||
justifyContent: 'center',
|
},
|
||||||
marginBottom: spacing.lg,
|
inputLabel: {
|
||||||
borderWidth: 2,
|
fontSize: 14,
|
||||||
borderColor: 'rgba(255,255,255,0.4)',
|
fontWeight: '500',
|
||||||
...shadows.md,
|
color: colors.text.secondary,
|
||||||
},
|
marginBottom: 10,
|
||||||
// 大屏模式Logo容器样式 - 增加阴影和内边距,更立体
|
},
|
||||||
splitLogoContainer: {
|
inputWrapper: {
|
||||||
borderWidth: 3,
|
flexDirection: 'row',
|
||||||
borderColor: 'rgba(255,255,255,0.5)',
|
alignItems: 'center',
|
||||||
...shadows.lg,
|
backgroundColor: '#F5F5F7',
|
||||||
},
|
borderRadius: 14,
|
||||||
logoContainerTop: {
|
paddingHorizontal: 18,
|
||||||
marginTop: spacing['3xl'],
|
height: 56,
|
||||||
},
|
},
|
||||||
appName: {
|
input: {
|
||||||
fontWeight: '800',
|
flex: 1,
|
||||||
color: '#FFFFFF',
|
fontSize: 16,
|
||||||
marginBottom: spacing.sm,
|
color: colors.text.primary,
|
||||||
textShadowColor: 'rgba(0,0,0,0.1)',
|
height: 56,
|
||||||
textShadowOffset: { width: 0, height: 2 },
|
},
|
||||||
textShadowRadius: 4,
|
checkIcon: {
|
||||||
},
|
marginLeft: 8,
|
||||||
// 大屏模式标题样式 - 大幅放大,增加间距和文字效果
|
},
|
||||||
splitAppName: {
|
eyeButton: {
|
||||||
fontSize: 72, // 大幅放大主标题
|
padding: 4,
|
||||||
marginBottom: spacing['2xl'],
|
marginLeft: 4,
|
||||||
textShadowOffset: { width: 0, height: 4 },
|
},
|
||||||
textShadowRadius: 8,
|
|
||||||
letterSpacing: 4, // 增加字间距,更显大气
|
// 忘记密码
|
||||||
fontWeight: '900',
|
forgotPassword: {
|
||||||
},
|
alignSelf: 'flex-end',
|
||||||
subtitle: {
|
marginBottom: 28,
|
||||||
fontSize: fontSizes.md,
|
marginTop: -8,
|
||||||
color: 'rgba(255,255,255,0.9)',
|
},
|
||||||
textAlign: 'center',
|
forgotPasswordText: {
|
||||||
paddingHorizontal: spacing.xl,
|
fontSize: 14,
|
||||||
},
|
color: THEME_COLORS.primary,
|
||||||
// 大屏模式副标题样式 - 大幅放大
|
fontWeight: '500',
|
||||||
splitSubtitle: {
|
},
|
||||||
fontSize: 24, // 放大副标题
|
|
||||||
marginBottom: spacing['6xl'], // 增加底部间距
|
// 登录按钮
|
||||||
lineHeight: 36,
|
loginButton: {
|
||||||
paddingHorizontal: spacing['4xl'],
|
height: 56,
|
||||||
letterSpacing: 1.5,
|
borderRadius: 14,
|
||||||
fontWeight: '500',
|
backgroundColor: THEME_COLORS.primary,
|
||||||
},
|
alignItems: 'center',
|
||||||
// 表单卡片(基础样式,无阴影)
|
justifyContent: 'center',
|
||||||
formCard: {
|
},
|
||||||
backgroundColor: colors.background.paper,
|
loginButtonDisabled: {
|
||||||
borderRadius: borderRadius['2xl'],
|
opacity: 0.7,
|
||||||
padding: spacing['2xl'],
|
},
|
||||||
marginBottom: spacing.xl,
|
loginButtonText: {
|
||||||
},
|
fontSize: 17,
|
||||||
// 小屏模式:原有默认阴影
|
fontWeight: '600',
|
||||||
normalFormCardShadow: {
|
color: '#FFFFFF',
|
||||||
...shadows.md,
|
},
|
||||||
},
|
|
||||||
// 大屏模式:专用全向阴影
|
// 底部
|
||||||
splitFormCardShadow: {
|
footer: {
|
||||||
// iOS 全向柔和阴影
|
flexDirection: 'row',
|
||||||
shadowColor: '#000',
|
justifyContent: 'center',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
alignItems: 'center',
|
||||||
shadowOpacity: 0.12,
|
marginTop: 40,
|
||||||
shadowRadius: 12,
|
},
|
||||||
// Android 高程 + 轻微边框(模拟顶部阴影)
|
footerText: {
|
||||||
elevation: 8,
|
fontSize: 15,
|
||||||
// 新增:极细微的顶部边框,强化区分度
|
color: colors.text.secondary,
|
||||||
borderTopWidth: 1,
|
},
|
||||||
borderTopColor: 'rgba(0,0,0,0.05)',
|
registerLink: {
|
||||||
},
|
fontSize: 15,
|
||||||
formTitle: {
|
color: THEME_COLORS.primary,
|
||||||
fontSize: fontSizes['2xl'],
|
fontWeight: '600',
|
||||||
fontWeight: '700',
|
marginLeft: 6,
|
||||||
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,
|
|
||||||
},
|
|
||||||
inputIcon: {
|
|
||||||
marginRight: spacing.sm,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: fontSizes.md,
|
|
||||||
color: colors.text.primary,
|
|
||||||
},
|
|
||||||
clearButton: {
|
|
||||||
padding: spacing.xs,
|
|
||||||
},
|
|
||||||
forgotPassword: {
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
marginBottom: spacing.md,
|
|
||||||
},
|
|
||||||
forgotPasswordText: {
|
|
||||||
fontSize: fontSizes.sm,
|
|
||||||
color: colors.primary.main,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
errorBox: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
backgroundColor: colors.accent.light,
|
|
||||||
borderRadius: borderRadius.md,
|
|
||||||
paddingHorizontal: spacing.md,
|
|
||||||
paddingVertical: spacing.sm,
|
|
||||||
marginBottom: spacing.md,
|
|
||||||
borderLeftWidth: 3,
|
|
||||||
borderLeftColor: colors.accent.main,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: fontSizes.sm,
|
|
||||||
color: colors.accent.dark,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
loginButton: {
|
|
||||||
borderRadius: borderRadius.lg,
|
|
||||||
overflow: 'hidden',
|
|
||||||
...shadows.md,
|
|
||||||
},
|
|
||||||
loginButtonGradient: {
|
|
||||||
height: 52,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
loginButtonDisabled: {
|
|
||||||
opacity: 0.7,
|
|
||||||
},
|
|
||||||
loginButtonText: {
|
|
||||||
fontSize: fontSizes.lg,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
|
||||||
// 底部区域
|
|
||||||
footerSection: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: spacing.xl,
|
|
||||||
},
|
|
||||||
footerText: {
|
|
||||||
fontSize: fontSizes.md,
|
|
||||||
color: colors.text.primary,
|
|
||||||
},
|
|
||||||
registerLink: {
|
|
||||||
fontSize: fontSizes.md,
|
|
||||||
color: colors.primary.main,
|
|
||||||
fontWeight: '700',
|
|
||||||
marginLeft: spacing.xs,
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
},
|
|
||||||
// ========== 分栏布局样式 ==========
|
|
||||||
splitContainer: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
leftPanelContainer: {
|
|
||||||
flex: 1, // 50%
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
leftPanelSafeArea: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: spacing['4xl'], // 增加内边距,不贴边
|
|
||||||
},
|
|
||||||
leftPanel: {
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: spacing['2xl'],
|
|
||||||
},
|
|
||||||
// 大屏模式左侧面板布局
|
|
||||||
splitLeftPanel: {
|
|
||||||
width: '100%',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: spacing['6xl'],
|
|
||||||
},
|
|
||||||
leftPanelDecor: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: spacing['4xl'],
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
leftPanelDecorBottom: {
|
|
||||||
marginTop: spacing['2xl'],
|
|
||||||
},
|
|
||||||
// 大屏模式装饰元素布局
|
|
||||||
splitLeftPanelDecorBottom: {
|
|
||||||
marginTop: spacing['6xl'],
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
// 大屏模式装饰元素水平排列
|
|
||||||
splitDecorRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
width: '100%',
|
|
||||||
gap: spacing['2xl'], // 增加图标间距
|
|
||||||
},
|
|
||||||
// 大屏模式装饰图标样式
|
|
||||||
splitDecorIcon: {
|
|
||||||
transform: [{ scale: 1.1 }],
|
|
||||||
opacity: 0.9,
|
|
||||||
},
|
|
||||||
decorRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
rightPanelContainer: {
|
|
||||||
flex: 1, // 50%
|
|
||||||
backgroundColor: colors.background.paper,
|
|
||||||
},
|
|
||||||
rightPanelSafeArea: {
|
|
||||||
flex: 1,
|
|
||||||
// 移除左右内边距,交给子容器控制
|
|
||||||
},
|
|
||||||
// 分栏布局右侧内容包裹器(核心修复)
|
|
||||||
splitRightContentWrapper: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
// 新增:左右内边距,保证表单和边缘有间距
|
|
||||||
paddingHorizontal: spacing['2xl'],
|
|
||||||
},
|
|
||||||
// 表单内容包裹器(区分大屏/小屏)
|
|
||||||
splitFormWrapper: {
|
|
||||||
// 关键修改:设置最大宽度 + 自适应宽度,实现水平居中
|
|
||||||
maxWidth: 420,
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
singleFormWrapper: {
|
|
||||||
flexGrow: 1,
|
|
||||||
width: '100%',
|
|
||||||
// 移除这里的阴影,避免重复
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
265
src/screens/auth/WelcomeScreen.tsx
Normal file
265
src/screens/auth/WelcomeScreen.tsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 欢迎页 WelcomeScreen
|
||||||
|
* 胡萝卜BBS - 用户欢迎页
|
||||||
|
*
|
||||||
|
* 亮色模式:白色主色调 + 胡萝卜橙辅色
|
||||||
|
* 暗色模式:深色背景 + 胡萝卜橙辅色
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
Animated,
|
||||||
|
StatusBar,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||||
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
export const WelcomeScreen: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
|
const styles = useMemo(() => createWelcomeStyles(colors), [colors]);
|
||||||
|
const isDark = resolved === 'dark';
|
||||||
|
|
||||||
|
// 动画值
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const slideUpAnim = useRef(new Animated.Value(50)).current;
|
||||||
|
const scaleAnim = useRef(new Animated.Value(0.9)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 启动入场动画
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 800,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideUpAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 800,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(scaleAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 800,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSignIn = () => {
|
||||||
|
router.push(hrefs.hrefAuthLogin());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAccount = () => {
|
||||||
|
router.push(hrefs.hrefAuthRegister());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
|
||||||
|
|
||||||
|
{/* 装饰性背景元素 */}
|
||||||
|
<View style={styles.decorCircle1} />
|
||||||
|
<View style={styles.decorCircle2} />
|
||||||
|
<View style={styles.decorCircle3} />
|
||||||
|
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.content,
|
||||||
|
{
|
||||||
|
opacity: fadeAnim,
|
||||||
|
transform: [
|
||||||
|
{ translateY: slideUpAnim },
|
||||||
|
{ scale: scaleAnim },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* 品牌区域 */}
|
||||||
|
<View style={styles.brandSection}>
|
||||||
|
<Text style={styles.welcomeEn}>Welcome To</Text>
|
||||||
|
<Text style={styles.brandName}>萝卜社区</Text>
|
||||||
|
<View style={styles.brandUnderline} />
|
||||||
|
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
发现有趣的内容{'\n'}
|
||||||
|
结识志同道合的朋友
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 按钮区域 */}
|
||||||
|
<View style={styles.buttonSection}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.signInButton}
|
||||||
|
onPress={handleSignIn}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.signInButtonText}>登录</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.createAccountButton}
|
||||||
|
onPress={handleCreateAccount}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.createAccountButtonText}>创建账号</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function createWelcomeStyles(colors: AppColors) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingTop: 60,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 装饰性背景元素 - 使用胡萝卜橙的淡色
|
||||||
|
decorCircle1: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -100,
|
||||||
|
right: -100,
|
||||||
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
borderRadius: 200,
|
||||||
|
backgroundColor: colors.primary.main + '08', // 非常淡的胡萝卜橙
|
||||||
|
},
|
||||||
|
decorCircle2: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: height * 0.3,
|
||||||
|
left: -80,
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: colors.primary.main + '10',
|
||||||
|
},
|
||||||
|
decorCircle3: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -50,
|
||||||
|
right: -50,
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
borderRadius: 125,
|
||||||
|
backgroundColor: colors.primary.main + '08',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 品牌区域
|
||||||
|
brandSection: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: colors.primary.main + '15',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 20,
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
color: colors.primary.main,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
welcomeEn: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '400',
|
||||||
|
color: colors.text.secondary,
|
||||||
|
marginBottom: 4,
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
brandName: {
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: colors.text.primary,
|
||||||
|
letterSpacing: 4,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
brandUnderline: {
|
||||||
|
width: 80,
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: colors.primary.main,
|
||||||
|
borderRadius: 2,
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: colors.text.primary,
|
||||||
|
marginBottom: 16,
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.text.secondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 24,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
buttonSection: {
|
||||||
|
width: '100%',
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
signInButton: {
|
||||||
|
backgroundColor: colors.primary.main,
|
||||||
|
borderRadius: 16,
|
||||||
|
height: 56,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
shadowColor: colors.primary.main,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
signInButtonText: {
|
||||||
|
color: colors.primary.contrast,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
createAccountButton: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderRadius: 16,
|
||||||
|
height: 56,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.primary.main,
|
||||||
|
},
|
||||||
|
createAccountButtonText: {
|
||||||
|
color: colors.primary.main,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WelcomeScreen;
|
||||||
@@ -6,3 +6,4 @@
|
|||||||
export { LoginScreen } from './LoginScreen';
|
export { LoginScreen } from './LoginScreen';
|
||||||
export { RegisterScreen } from './RegisterScreen';
|
export { RegisterScreen } from './RegisterScreen';
|
||||||
export { ForgotPasswordScreen } from './ForgotPasswordScreen';
|
export { ForgotPasswordScreen } from './ForgotPasswordScreen';
|
||||||
|
export { WelcomeScreen } from './WelcomeScreen';
|
||||||
|
|||||||
Reference in New Issue
Block a user