feat: 更新应用版本和集成腾讯云 COS 上传功能
- 将应用版本更新至 1.0.2,修改相关配置文件 - 集成腾讯云 COS 上传功能,新增相关服务和钩子 - 更新 AI 体态评估页面,支持照片上传和评估结果展示 - 添加雷达图组件以展示评估结果 - 更新样式以适应新功能的展示和交互 - 修改登录页面背景效果,提升用户体验
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Animated, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
@@ -17,7 +18,52 @@ export default function LoginScreen() {
|
||||
const searchParams = useLocalSearchParams<{ redirectTo?: string; redirectParams?: string }>();
|
||||
const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const color = Colors[scheme];
|
||||
const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background;
|
||||
const dispatch = useAppDispatch();
|
||||
const AnimatedLinear = useMemo(() => Animated.createAnimatedComponent(LinearGradient), []);
|
||||
|
||||
// 背景动效:轻微平移/旋转与呼吸动画
|
||||
const translateAnim = useRef(new Animated.Value(0)).current;
|
||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||
const pulseAnimA = useRef(new Animated.Value(0)).current;
|
||||
const pulseAnimB = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
const loopTranslate = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(translateAnim, { toValue: 1, duration: 6000, useNativeDriver: true }),
|
||||
Animated.timing(translateAnim, { toValue: 0, duration: 6000, useNativeDriver: true }),
|
||||
])
|
||||
);
|
||||
const loopRotate = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(rotateAnim, { toValue: 1, duration: 10000, useNativeDriver: true }),
|
||||
Animated.timing(rotateAnim, { toValue: 0, duration: 10000, useNativeDriver: true }),
|
||||
])
|
||||
);
|
||||
const loopPulseA = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnimA, { toValue: 1, duration: 3500, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnimA, { toValue: 0, duration: 3500, useNativeDriver: true }),
|
||||
])
|
||||
);
|
||||
const loopPulseB = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnimB, { toValue: 1, duration: 4200, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnimB, { toValue: 0, duration: 4200, useNativeDriver: true }),
|
||||
])
|
||||
);
|
||||
loopTranslate.start();
|
||||
loopRotate.start();
|
||||
loopPulseA.start();
|
||||
loopPulseB.start();
|
||||
return () => {
|
||||
loopTranslate.stop();
|
||||
loopRotate.stop();
|
||||
loopPulseA.stop();
|
||||
loopPulseB.stop();
|
||||
};
|
||||
}, [pulseAnimA, pulseAnimB, rotateAnim, translateAnim]);
|
||||
|
||||
const [hasAgreed, setHasAgreed] = useState<boolean>(false);
|
||||
const [appleAvailable, setAppleAvailable] = useState<boolean>(false);
|
||||
@@ -29,7 +75,21 @@ export default function LoginScreen() {
|
||||
|
||||
const guardAgreement = useCallback((action: () => void) => {
|
||||
if (!hasAgreed) {
|
||||
Alert.alert('请先阅读并同意', '勾选“我已阅读并同意用户协议与隐私政策”后才可继续登录');
|
||||
Alert.alert(
|
||||
'请先阅读并同意',
|
||||
'继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '同意并继续',
|
||||
onPress: () => {
|
||||
setHasAgreed(true);
|
||||
setTimeout(() => action(), 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
action();
|
||||
@@ -83,11 +143,79 @@ export default function LoginScreen() {
|
||||
}
|
||||
}, [router, searchParams?.redirectParams, searchParams?.redirectTo]);
|
||||
|
||||
const disabledStyle = useMemo(() => ({ opacity: hasAgreed ? 1 : 0.5 }), [hasAgreed]);
|
||||
// 登录按钮不再因未勾选协议而禁用,仅在加载中禁用
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={['top']} style={[styles.safeArea, { backgroundColor: color.background }]}>
|
||||
<ThemedView style={styles.container}>
|
||||
<SafeAreaView edges={['top']} style={[styles.safeArea, { backgroundColor: pageBackground }]}>
|
||||
<ThemedView style={[styles.container, { backgroundColor: pageBackground }]}>
|
||||
{/* 动态背景层(置于内容之下) */}
|
||||
<View pointerEvents="none" style={styles.bgWrap}>
|
||||
{/* 基础全屏渐变:保证覆盖全屏 */}
|
||||
<AnimatedLinear
|
||||
colors={
|
||||
scheme === 'light'
|
||||
? [color.pageBackgroundEmphasis, color.heroSurfaceTint, color.surface]
|
||||
: [color.background, '#0F1112', color.surface]
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={[styles.bgGradientFull]}
|
||||
/>
|
||||
|
||||
{/* 次级大面积渐变:对角线方向形成层次 */}
|
||||
<AnimatedLinear
|
||||
colors={
|
||||
scheme === 'light'
|
||||
? ['rgba(164,138,237,0.12)', 'rgba(187,242,70,0.16)', 'transparent']
|
||||
: ['rgba(164,138,237,0.16)', 'rgba(187,242,70,0.12)', 'transparent']
|
||||
}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={[
|
||||
styles.bgGradientCover,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
rotate: rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['-4deg', '6deg'] }),
|
||||
},
|
||||
],
|
||||
opacity: scheme === 'light' ? 0.9 : 0.65,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 动感色块 A(主色呼吸,置于左下) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.accentBlobLarge,
|
||||
{
|
||||
backgroundColor: color.ornamentPrimary,
|
||||
transform: [
|
||||
{ translateX: -80 },
|
||||
{ translateY: 320 },
|
||||
{ scale: pulseAnimA.interpolate({ inputRange: [0, 1], outputRange: [1, 1.05] }) },
|
||||
],
|
||||
opacity: scheme === 'light' ? 0.55 : 0.4,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 动感色块 B(辅色漂移,置于右上) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.accentBlobMedium,
|
||||
{
|
||||
backgroundColor: color.ornamentAccent,
|
||||
transform: [
|
||||
{ translateX: 240 },
|
||||
{ translateY: -40 },
|
||||
{ scale: pulseAnimB.interpolate({ inputRange: [0, 1], outputRange: [1, 1.07] }) },
|
||||
],
|
||||
opacity: scheme === 'light' ? 0.5 : 0.38,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
{/* 自定义头部,与其它页面风格一致 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity accessibilityRole="button" onPress={() => router.back()} style={styles.backButton}>
|
||||
@@ -108,11 +236,11 @@ export default function LoginScreen() {
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={() => guardAgreement(onAppleLogin)}
|
||||
disabled={!hasAgreed || loading}
|
||||
disabled={loading}
|
||||
style={({ pressed }) => [
|
||||
styles.appleButton,
|
||||
{ backgroundColor: '#000000' },
|
||||
disabledStyle,
|
||||
loading && { opacity: 0.7 },
|
||||
pressed && { transform: [{ scale: 0.98 }] },
|
||||
]}
|
||||
>
|
||||
@@ -125,16 +253,16 @@ export default function LoginScreen() {
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={() => guardAgreement(onGuestLogin)}
|
||||
disabled={!hasAgreed || loading}
|
||||
disabled={loading}
|
||||
style={({ pressed }) => [
|
||||
styles.guestButton,
|
||||
{ borderColor: color.border, backgroundColor: color.surface },
|
||||
disabledStyle,
|
||||
loading && { opacity: 0.7 },
|
||||
pressed && { transform: [{ scale: 0.98 }] },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="person-circle-outline" size={22} color={Colors.light.neutral200} style={{ marginRight: 8 }} />
|
||||
<Text style={[styles.guestText, { color: Colors.light.neutral200 }]}>以游客身份继续</Text>
|
||||
<Ionicons name="person-circle-outline" size={22} color={color.neutral200} style={{ marginRight: 8 }} />
|
||||
<Text style={[styles.guestText, { color: color.neutral200 }]}>以游客身份继续</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* 协议勾选 */}
|
||||
@@ -192,6 +320,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 32,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.5,
|
||||
lineHeight: 38,
|
||||
},
|
||||
subtitle: {
|
||||
marginTop: 8,
|
||||
@@ -248,6 +377,45 @@ const styles = StyleSheet.create({
|
||||
link: { fontSize: 12, fontWeight: '600' },
|
||||
footerHint: { marginTop: 24 },
|
||||
hintText: { fontSize: 12 },
|
||||
// 背景样式
|
||||
bgWrap: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
zIndex: 0,
|
||||
},
|
||||
bgGradientFull: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
bgGradientCover: {
|
||||
position: 'absolute',
|
||||
left: '-10%',
|
||||
top: '-15%',
|
||||
width: '130%',
|
||||
height: '70%',
|
||||
borderBottomLeftRadius: 36,
|
||||
borderBottomRightRadius: 36,
|
||||
},
|
||||
accentBlob: {
|
||||
position: 'absolute',
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 90,
|
||||
},
|
||||
accentBlobLarge: {
|
||||
position: 'absolute',
|
||||
width: 260,
|
||||
height: 260,
|
||||
borderRadius: 130,
|
||||
},
|
||||
accentBlobMedium: {
|
||||
position: 'absolute',
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 90,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user