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

View File

@ -83,7 +83,7 @@ const LoginScreen = () => {
className="absolute left-1/2 z-10"
style={{
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]"
style={{
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 />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB