feat(auth): redesign auth screens with clean form style and add welcome page
All checks were successful
Frontend CI / ota-android (push) Successful in 11m41s
Frontend CI / build-and-push-web (push) Successful in 21m57s
Frontend CI / build-android-apk (push) Successful in 1h0m58s

- 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:
lafay
2026-04-01 00:50:38 +08:00
parent 259de04f3e
commit 20e9d69540
8 changed files with 1266 additions and 1705 deletions

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import { WelcomeScreen } from '../src/screens/auth';
export default function WelcomeRoute() {
return <WelcomeScreen />;
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,605 +126,257 @@ 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>
<Animated.View </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>
{/* 表单 */} {/* 表单区域 */}
{renderFormContent()} <View style={styles.formSection}>
</ScrollView> {/* 用户名输入框 */}
</KeyboardAvoidingView> <View style={styles.inputContainer}>
</LinearGradient> <Text style={styles.inputLabel}></Text>
<View style={styles.inputWrapper}>
<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 && (
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
)}
</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={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%',
// 移除这里的阴影,避免重复
},
}); });
} }
export default LoginScreen; export default LoginScreen;

File diff suppressed because it is too large Load Diff

View 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;

View File

@@ -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';