feat: 更新应用版本和集成腾讯云 COS 上传功能
- 将应用版本更新至 1.0.2,修改相关配置文件 - 集成腾讯云 COS 上传功能,新增相关服务和钩子 - 更新 AI 体态评估页面,支持照片上传和评估结果展示 - 添加雷达图组件以展示评估结果 - 更新样式以适应新功能的展示和交互 - 修改登录页面背景效果,提升用户体验
This commit is contained in:
279
app/ai-posture-processing.tsx
Normal file
279
app/ai-posture-processing.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming } from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
export default function AIPostureProcessingScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const theme = Colors.dark;
|
||||
|
||||
// Core looping animations
|
||||
const spin = useSharedValue(0);
|
||||
const pulse = useSharedValue(0);
|
||||
const scanY = useSharedValue(0);
|
||||
const particle = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
spin.value = withRepeat(withTiming(1, { duration: 6000, easing: Easing.linear }), -1);
|
||||
pulse.value = withRepeat(withSequence(
|
||||
withTiming(1, { duration: 1600, easing: Easing.inOut(Easing.quad) }),
|
||||
withTiming(0, { duration: 1600, easing: Easing.inOut(Easing.quad) })
|
||||
), -1, true);
|
||||
scanY.value = withRepeat(withTiming(1, { duration: 3800, easing: Easing.inOut(Easing.cubic) }), -1, false);
|
||||
particle.value = withDelay(400, withRepeat(withTiming(1, { duration: 5200, easing: Easing.inOut(Easing.quad) }), -1, true));
|
||||
}, []);
|
||||
|
||||
const ringStyleOuter = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${spin.value * 360}deg` }],
|
||||
opacity: 0.8,
|
||||
}));
|
||||
const ringStyleInner = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${-spin.value * 360}deg` }, { scale: 0.98 + pulse.value * 0.04 }],
|
||||
}));
|
||||
const scannerStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: (scanY.value * (SCREEN_HEIGHT * 0.45)) - (SCREEN_HEIGHT * 0.225) }],
|
||||
opacity: 0.6 + Math.sin(scanY.value * Math.PI) * 0.2,
|
||||
}));
|
||||
const particleStyleA = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ translateX: Math.sin(particle.value * Math.PI * 2) * 40 },
|
||||
{ translateY: Math.cos(particle.value * Math.PI * 2) * 24 },
|
||||
{ rotate: `${particle.value * 360}deg` },
|
||||
],
|
||||
opacity: 0.5 + 0.5 * Math.abs(Math.sin(particle.value * Math.PI)),
|
||||
}));
|
||||
|
||||
return (
|
||||
<View style={[styles.screen, { backgroundColor: theme.background }]}>
|
||||
<HeaderBar title="AI评估进行中" onBack={() => router.back()} tone="light" transparent />
|
||||
|
||||
{/* Layered background */}
|
||||
<View style={[StyleSheet.absoluteFill, { zIndex: -1 }]} pointerEvents="none">
|
||||
<LinearGradient
|
||||
colors={["#F7FFE8", "#F0FBFF", "#FFF6E8"]}
|
||||
start={{ x: 0.1, y: 0 }}
|
||||
end={{ x: 0.9, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<BlurView intensity={20} tint="light" style={styles.blurBlobA} />
|
||||
<BlurView intensity={20} tint="light" style={styles.blurBlobB} />
|
||||
</View>
|
||||
|
||||
{/* Hero visualization */}
|
||||
<View style={styles.hero}>
|
||||
<View style={styles.heroBackdrop} />
|
||||
<Animated.View style={[styles.ringOuter, ringStyleOuter]} />
|
||||
<Animated.View style={[styles.ringInner, ringStyleInner]} />
|
||||
|
||||
<View style={styles.grid}>
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<View key={`row-${i}`} style={styles.gridRow}>
|
||||
{Array.from({ length: 9 }).map((__, j) => (
|
||||
<View key={`cell-${i}-${j}`} style={styles.gridCell} />
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
<Animated.View style={[styles.scanner, scannerStyle]} />
|
||||
</View>
|
||||
|
||||
<Animated.View style={[styles.particleA, particleStyleA]} />
|
||||
<Animated.View style={[styles.particleB, particleStyleA, { right: undefined, left: SCREEN_WIDTH * 0.2, top: undefined, bottom: 60 }]} />
|
||||
</View>
|
||||
|
||||
{/* Copy & actions */}
|
||||
<View style={[styles.panel, { paddingBottom: insets.bottom + 16 }]}>
|
||||
<Text style={styles.title}>正在进行体态特征提取与矢量评估</Text>
|
||||
<Text style={styles.subtitle}>这通常需要 10-30 秒。你可以停留在此页面等待结果,或点击返回稍后在个人中心查看。</Text>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: theme.primary }]}
|
||||
activeOpacity={0.9}
|
||||
// TODO: 评估完成后恢复为停留当前页面等待结果(不要跳转)
|
||||
onPress={() => router.replace('/ai-posture-result')}
|
||||
>
|
||||
<Ionicons name="time-outline" size={16} color="#192126" />
|
||||
<Text style={styles.primaryBtnText}>保持页面等待</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.secondaryBtn} activeOpacity={0.9} onPress={() => router.replace('/(tabs)/personal')}>
|
||||
<Text style={styles.secondaryBtnText}>返回个人中心</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const RING_SIZE = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.62;
|
||||
const INNER_RING_SIZE = RING_SIZE * 0.72;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
},
|
||||
hero: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
blurBlobA: {
|
||||
position: 'absolute',
|
||||
top: -80,
|
||||
right: -60,
|
||||
width: 240,
|
||||
height: 240,
|
||||
borderRadius: 120,
|
||||
backgroundColor: 'rgba(187,242,70,0.20)',
|
||||
},
|
||||
blurBlobB: {
|
||||
position: 'absolute',
|
||||
bottom: 120,
|
||||
left: -40,
|
||||
width: 220,
|
||||
height: 220,
|
||||
borderRadius: 110,
|
||||
backgroundColor: 'rgba(89, 198, 255, 0.16)',
|
||||
},
|
||||
heroBackdrop: {
|
||||
position: 'absolute',
|
||||
width: RING_SIZE * 1.08,
|
||||
height: RING_SIZE * 1.08,
|
||||
borderRadius: (RING_SIZE * 1.08) / 2,
|
||||
backgroundColor: 'rgba(25,33,38,0.25)',
|
||||
},
|
||||
ringOuter: {
|
||||
position: 'absolute',
|
||||
width: RING_SIZE,
|
||||
height: RING_SIZE,
|
||||
borderRadius: RING_SIZE / 2,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(25,33,38,0.16)',
|
||||
},
|
||||
ringInner: {
|
||||
position: 'absolute',
|
||||
width: INNER_RING_SIZE,
|
||||
height: INNER_RING_SIZE,
|
||||
borderRadius: INNER_RING_SIZE / 2,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(187,242,70,0.65)',
|
||||
shadowColor: '#BBF246',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 24,
|
||||
},
|
||||
grid: {
|
||||
width: RING_SIZE * 0.9,
|
||||
height: RING_SIZE * 0.9,
|
||||
borderRadius: RING_SIZE * 0.45,
|
||||
overflow: 'hidden',
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(25,33,38,0.08)',
|
||||
},
|
||||
gridRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
gridCell: {
|
||||
flex: 1,
|
||||
aspectRatio: 1,
|
||||
margin: 2,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
},
|
||||
scanner: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '50%',
|
||||
height: 60,
|
||||
marginTop: -30,
|
||||
backgroundColor: 'rgba(187,242,70,0.10)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(187,242,70,0.25)',
|
||||
},
|
||||
particleA: {
|
||||
position: 'absolute',
|
||||
right: SCREEN_WIDTH * 0.18,
|
||||
top: 40,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: '#BBF246',
|
||||
shadowColor: '#BBF246',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 16,
|
||||
},
|
||||
particleB: {
|
||||
position: 'absolute',
|
||||
right: SCREEN_WIDTH * 0.08,
|
||||
top: 120,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(89, 198, 255, 1)',
|
||||
shadowColor: 'rgba(89, 198, 255, 1)',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
},
|
||||
panel: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 8,
|
||||
},
|
||||
title: {
|
||||
color: '#ECEDEE',
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
marginTop: 14,
|
||||
},
|
||||
primaryBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
height: 44,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
},
|
||||
primaryBtnText: {
|
||||
color: '#192126',
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
},
|
||||
secondaryBtn: {
|
||||
flex: 1,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.18)',
|
||||
},
|
||||
secondaryBtnText: {
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user