feat: 登录

This commit is contained in:
jinyaqiu 2025-08-08 16:25:32 +08:00
parent e2cd78f6a0
commit 027e72a364
3 changed files with 167 additions and 367 deletions

View File

@ -1,291 +1,183 @@
import { Fonts } from '@/constants/Fonts';
import { checkAuthStatus } from '@/lib/auth'; import { checkAuthStatus } from '@/lib/auth';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Animated, Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, {
interpolate,
useAnimatedStyle,
useSharedValue,
withDelay,
withRepeat,
withSequence,
withTiming
} from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
// Worklet function for animations
const runShakeAnimation = (value: Animated.SharedValue<number>) => {
'worklet';
return withRepeat(
withSequence(
withTiming(1, { duration: 300 }),
withTiming(-1, { duration: 300 }),
withTiming(1, { duration: 300 }),
withTiming(-1, { duration: 300 }),
withTiming(0, { duration: 200 }),
withDelay(1000, withTiming(0, { duration: 0 }))
),
-1
);
};
const runWaveAnimation = (value: Animated.SharedValue<number>) => {
'worklet';
return withRepeat(
withSequence(
withTiming(1, { duration: 500 }),
withTiming(-1, { duration: 500 }),
withTiming(0, { duration: 500 }),
withDelay(1000, withTiming(0, { duration: 0 }))
),
-1
);
};
export default function HomeScreen() { export default function HomeScreen() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = React.useState(false);
// 获取屏幕宽度
const screenWidth = Dimensions.get('window').width; const screenWidth = Dimensions.get('window').width;
// 动画值 // Animation values
const fadeAnim = useRef(new Animated.Value(0)).current; // IP图标的淡入动画 const fadeAnim = useSharedValue(0);
const shakeAnim = useRef(new Animated.Value(0)).current; // IP图标的摇晃动画 const shakeAnim = useSharedValue(0);
const animationRef = useRef<Animated.CompositeAnimation | null>(null); // 动画引用 const waveAnim = useSharedValue(0);
const descriptionAnim = useRef(new Animated.Value(0)).current; // 描述文本的淡入动画 const buttonShakeAnim = useSharedValue(0);
const buttonAnim = useRef(new Animated.Value(0)).current; // 按钮的淡入动画 const fadeInAnim = useSharedValue(0);
const buttonShakeAnim = useRef(new Animated.Value(0)).current; // 按钮的摇晃动画 const descriptionAnim = useSharedValue(0);
const buttonLoopAnim = useRef<Animated.CompositeAnimation | null>(null); // 按钮循环动画引用 const textAnimations = {
const fadeInAnim = useRef(new Animated.Value(0)).current; line1: useSharedValue(0),
line2: useSharedValue(0),
line3: useSharedValue(0),
subtitle: useSharedValue(0),
};
// 文本行动画值 // Animation styles
const [textAnimations] = useState(() => ({ const ipAnimatedStyle = useAnimatedStyle(() => ({
line1: new Animated.Value(0), // 第一行文本动画 opacity: fadeAnim.value,
line2: new Animated.Value(0), // 第二行文本动画 transform: [
line3: new Animated.Value(0), // 第三行文本动画 { translateX: interpolate(shakeAnim.value, [-1, 1], [-2, 2]) },
subtitle: new Animated.Value(0), // 副标题动画 { rotate: `${interpolate(shakeAnim.value, [-1, 1], [-2, 2])}deg` },
],
})); }));
// 添加挥手动画值 const waveAnimatedStyle = useAnimatedStyle(() => ({
const waveAnim = useRef(new Animated.Value(0)).current; transform: [
{ rotate: `${interpolate(waveAnim.value, [-1, 0, 1], [-15, 0, 15])}deg` },
],
}));
// 启动IP图标摇晃动画 const buttonStyle = useAnimatedStyle(() => ({
const startShaking = () => { opacity: fadeInAnim.value,
// 停止任何正在进行的动画 transform: [
if (animationRef.current) { { translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) },
animationRef.current.stop(); { translateX: interpolate(buttonShakeAnim.value, [-1, 0, 1], [-5, 0, 5]) }
} ]
}));
// 创建动画序列 const welcomeStyle = useAnimatedStyle(() => ({
const sequence = Animated.sequence([ opacity: fadeInAnim.value,
// 第一次左右摇晃 transform: [{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }]
Animated.timing(shakeAnim, { }));
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: -1,
duration: 300,
useNativeDriver: true,
}),
// 第二次左右摇晃
Animated.timing(shakeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: -1,
duration: 300,
useNativeDriver: true,
}),
// 回到中心位置
Animated.timing(shakeAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
// 1秒延迟
Animated.delay(1000),
]);
// 循环播放动画序列 const descriptionStyle = useAnimatedStyle(() => ({
animationRef.current = Animated.loop(sequence); opacity: descriptionAnim.value,
animationRef.current.start(); transform: [{ translateY: interpolate(descriptionAnim.value, [0, 1], [20, 0]) }]
}; }));
// 启动文本动画 const textLine1Style = useAnimatedStyle(() => ({
const startTextAnimations = () => { opacity: textAnimations.line1.value,
// 按顺序延迟启动每行文本动画 transform: [{ translateY: interpolate(textAnimations.line1.value, [0, 1], [10, 0]) }]
return new Promise<void>((resolve) => { }));
Animated.stagger(300, [
Animated.timing(textAnimations.line1, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(textAnimations.line2, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(textAnimations.line3, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(textAnimations.subtitle, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
]).start(() => resolve());
});
};
// 启动描述文本动画 const textLine2Style = useAnimatedStyle(() => ({
const startDescriptionAnimation = () => { opacity: textAnimations.line2.value,
// IP图标显示后淡入描述文本 transform: [{ translateY: interpolate(textAnimations.line2.value, [0, 1], [10, 0]) }]
return new Promise<void>((resolve) => { }));
Animated.sequence([
Animated.delay(200), // IP图标显示后延迟200ms
Animated.timing(descriptionAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => resolve());
});
};
// 启动欢迎语动画
const startWelcomeAnimation = () => {
// IP图标显示后淡入描述文本
return new Promise<void>((resolve) => {
Animated.sequence([
Animated.delay(200), // IP图标显示后延迟200ms
Animated.timing(fadeInAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => resolve());
});
};
// 启动按钮动画 const textLine3Style = useAnimatedStyle(() => ({
const startButtonAnimation = () => { opacity: textAnimations.line3.value,
// 首先淡入按钮 transform: [{ translateY: interpolate(textAnimations.line3.value, [0, 1], [10, 0]) }]
Animated.sequence([ }));
Animated.timing(buttonAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => {
// 淡入完成后开始循环摇晃动画
startButtonShakeLoop();
});
};
// 启动按钮循环摇晃动画 const subtitleStyle = useAnimatedStyle(() => ({
const startButtonShakeLoop = () => { opacity: textAnimations.subtitle.value,
// 停止任何正在进行的动画 transform: [{ translateY: interpolate(textAnimations.subtitle.value, [0, 1], [10, 0]) }]
if (buttonLoopAnim.current) { }));
buttonLoopAnim.current.stop();
}
// 创建摇晃动画序列 // Start animations
const shakeSequence = Animated.sequence([
// 向右摇晃
Animated.timing(buttonShakeAnim, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
// 向左摇晃
Animated.timing(buttonShakeAnim, {
toValue: -1,
duration: 100,
useNativeDriver: true,
}),
// 再次向右摇晃
Animated.timing(buttonShakeAnim, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
// 回到中心位置
Animated.timing(buttonShakeAnim, {
toValue: 0,
duration: 100,
useNativeDriver: true,
}),
// 暂停3秒
Animated.delay(3000)
]);
// 循环播放动画序列
buttonLoopAnim.current = Animated.loop(shakeSequence);
buttonLoopAnim.current.start();
};
// 启动挥手动画
const startWaveAnimation = () => {
// 创建循环动画:左右摇摆
Animated.loop(
Animated.sequence([
Animated.timing(waveAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(waveAnim, {
toValue: -1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(waveAnim, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
Animated.delay(1000), // 暂停1秒
])
).start();
};
// 组件挂载时启动动画
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
checkAuthStatus(router, () => { checkAuthStatus(router, () => {
router.replace('/ask') router.replace('/ask');
}, false).then(() => { }, false).then(() => {
setIsLoading(false); setIsLoading(false);
}).catch(() => { }).catch(() => {
setIsLoading(false); setIsLoading(false);
}); });
// IP图标的淡入动画
Animated.timing(fadeAnim, { // Start fade in animation
toValue: 1, fadeAnim.value = withTiming(1, { duration: 1000 }, () => {
duration: 1000, // Start shake animation
useNativeDriver: true, shakeAnim.value = runShakeAnimation(shakeAnim);
}).start(() => {
// 淡入完成后开始摇晃动画 // Start text animations with delays
startShaking(); textAnimations.line1.value = withDelay(0, withTiming(1, { duration: 500 }));
// IP显示后开始文本动画 textAnimations.line2.value = withDelay(300, withTiming(1, { duration: 500 }));
startTextAnimations() textAnimations.line3.value = withDelay(600, withTiming(1, { duration: 500 }));
.then(() => startWelcomeAnimation()) textAnimations.subtitle.value = withDelay(900, withTiming(1, { duration: 500 }));
.then(() => startDescriptionAnimation())
.then(() => startButtonAnimation()) // Start welcome animation
.catch(console.error); fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }));
// 启动挥手动画
startWaveAnimation(); // Start description animation
descriptionAnim.value = withDelay(200, withTiming(1, { duration: 800 }));
// Start button animation
fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }, () => {
// Start button shake animation
buttonShakeAnim.value = withRepeat(
withSequence(
withTiming(1, { duration: 100 }),
withTiming(-1, { duration: 100 }),
withTiming(1, { duration: 100 }),
withTiming(0, { duration: 100 }),
withDelay(3000, withTiming(0, { duration: 0 }))
),
-1
);
}));
// Start wave animation
waveAnim.value = runWaveAnimation(waveAnim);
}); });
// 组件卸载时清理动画 // Cleanup
return () => { return () => {
if (buttonLoopAnim.current) { fadeAnim.value = 0;
buttonLoopAnim.current.stop(); shakeAnim.value = 0;
} waveAnim.value = 0;
if (animationRef.current) { buttonShakeAnim.value = 0;
animationRef.current.stop(); fadeInAnim.value = 0;
} descriptionAnim.value = 0;
Object.values(textAnimations).forEach(anim => anim.value = 0);
}; };
}, []); }, []);
// 动画样式
const animatedStyle = {
opacity: fadeAnim,
transform: [
{
translateX: shakeAnim.interpolate({
inputRange: [-1, 1],
outputRange: [-2, 2],
})
},
{
rotate: shakeAnim.interpolate({
inputRange: [-1, 1],
outputRange: ['-2deg', '2deg'],
}),
},
],
};
// 旋转动画插值
const rotate = waveAnim.interpolate({
inputRange: [-1, 0, 1],
outputRange: ['-15deg', '0deg', '15deg'],
});
if (isLoading) { if (isLoading) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
@ -297,86 +189,27 @@ export default function HomeScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}> <View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
{/* 标题区域 */}
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<Animated.Text <Animated.Text style={[styles.titleText, textLine1Style]}>
style={[
styles.titleText,
{
opacity: textAnimations.line1, transform: [{
translateY: textAnimations.line1.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.awaken', { ns: 'login' })} {t('auth.welcomeAwaken.awaken', { ns: 'login' })}
</Animated.Text> </Animated.Text>
<Animated.Text <Animated.Text style={[styles.titleText, textLine2Style]}>
style={[
styles.titleText,
{
opacity: textAnimations.line2, transform: [{
translateY: textAnimations.line2.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.your', { ns: 'login' })} {t('auth.welcomeAwaken.your', { ns: 'login' })}
</Animated.Text> </Animated.Text>
<Animated.Text <Animated.Text style={[styles.titleText, textLine3Style]}>
style={[
styles.titleText,
{
opacity: textAnimations.line3, transform: [{
translateY: textAnimations.line3.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.pm', { ns: 'login' })} {t('auth.welcomeAwaken.pm', { ns: 'login' })}
</Animated.Text> </Animated.Text>
<Animated.Text <Animated.Text style={[styles.subtitleText, subtitleStyle]}>
style={[
styles.subtitleText,
{
opacity: textAnimations.subtitle,
transform: [{
translateY: textAnimations.subtitle.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.slogan', { ns: 'login' })} {t('auth.welcomeAwaken.slogan', { ns: 'login' })}
</Animated.Text> </Animated.Text>
</View> </View>
{/* 欢迎语 */}
<View style={{ alignItems: 'flex-end' }}> <View style={{ alignItems: 'flex-end' }}>
<Animated.View <Animated.View style={[{
style={[{ height: screenWidth * 0.3,
height: screenWidth * 0.3, width: screenWidth * 0.3,
width: screenWidth * 0.3, marginTop: -screenWidth * 0.08,
marginTop: -screenWidth * 0.08, }, welcomeStyle]}>
opacity: fadeInAnim,
transform: [{
translateY: fadeInAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
}]
}]}
>
<Image <Image
source={require('@/assets/images/png/icon/think.png')} source={require('@/assets/images/png/icon/think.png')}
style={{ style={{
@ -388,62 +221,25 @@ export default function HomeScreen() {
</Animated.View> </Animated.View>
</View> </View>
{/* Animated IP */}
<View style={styles.ipContainer}> <View style={styles.ipContainer}>
<Animated.View style={[styles.ipWrapper, { transform: [{ rotate }] }]}> <Animated.View style={[styles.ipWrapper, waveAnimatedStyle]}>
<Image <Image
source={require('@/assets/images/png/icon/ip.png')} source={require('@/assets/images/png/icon/ip.png')}
style={{ width: screenWidth * 0.9, marginBottom: - screenWidth * 0.18, marginTop: -screenWidth * 0.22 }} style={{ width: screenWidth * 0.9, marginBottom: -screenWidth * 0.18, marginTop: -screenWidth * 0.22 }}
/> />
</Animated.View> </Animated.View>
</View> </View>
{/* 介绍文本 */} <Animated.Text style={[styles.descriptionText, descriptionStyle]}>
<Animated.Text
style={[
styles.descriptionText,
{
opacity: descriptionAnim,
transform: [{
translateY: descriptionAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.gallery', { ns: 'login' })} {t('auth.welcomeAwaken.gallery', { ns: 'login' })}
{"\n"} {"\n"}
{t('auth.welcomeAwaken.back', { ns: 'login' })} {t('auth.welcomeAwaken.back', { ns: 'login' })}
</Animated.Text> </Animated.Text>
{/* 唤醒按钮 */} <Animated.View style={[{ alignItems: "center" }, buttonStyle]}>
<Animated.View
style={{
alignItems: "center",
opacity: buttonAnim,
transform: [
{
translateY: buttonAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
},
{
translateX: buttonShakeAnim.interpolate({
inputRange: [-1, 0, 1],
outputRange: [-5, 0, 5]
})
}
]
}}
>
<TouchableOpacity <TouchableOpacity
style={styles.awakenButton} style={styles.awakenButton}
onPress={async () => { onPress={() => router.push('/login')}
router.push('/login');
}}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text style={styles.buttonText}> <Text style={styles.buttonText}>
@ -485,17 +281,19 @@ const styles = StyleSheet.create({
}, },
titleText: { titleText: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 30, fontSize: 32,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 12, marginBottom: 12,
textAlign: 'left', textAlign: 'left',
lineHeight: 36, lineHeight: 36,
fontFamily: Fonts['quicksand']
}, },
subtitleText: { subtitleText: {
color: 'rgba(255, 255, 255, 0.85)', color: 'rgba(255, 255, 255, 0.85)',
fontSize: 16, fontSize: 16,
textAlign: 'left', textAlign: 'left',
lineHeight: 24, lineHeight: 24,
fontFamily: Fonts['inter']
}, },
ipContainer: { ipContainer: {
alignItems: 'center', alignItems: 'center',
@ -514,11 +312,12 @@ const styles = StyleSheet.create({
opacity: 0.9, opacity: 0.9,
paddingHorizontal: 40, paddingHorizontal: 40,
marginTop: -16, marginTop: -16,
fontFamily: Fonts['inter']
}, },
awakenButton: { awakenButton: {
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 28, borderRadius: 28,
paddingVertical: 16, paddingVertical: 20,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
@ -532,5 +331,6 @@ const styles = StyleSheet.create({
color: '#4C320C', color: '#4C320C',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 18, fontSize: 18,
fontFamily: Fonts['quicksand']
}, },
}); });

View File

@ -83,7 +83,7 @@ const LoginScreen = () => {
className="absolute left-1/2 z-10" className="absolute left-1/2 z-10"
style={{ style={{
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0, top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0,
transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight - insets.top - 28 : -keyboardOffset }] transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight : -keyboardOffset }]
}} }}
> >
{ {
@ -98,7 +98,7 @@ const LoginScreen = () => {
className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]" className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]"
style={{ style={{
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0, top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0,
transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight - insets.top - 30 : -4 - keyboardOffset }] transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight : -4 - keyboardOffset }]
}} }}
> >
<Handers /> <Handers />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB