- 将应用版本更新至 1.0.2,修改相关配置文件 - 集成腾讯云 COS 上传功能,新增相关服务和钩子 - 更新 AI 体态评估页面,支持照片上传和评估结果展示 - 添加雷达图组件以展示评估结果 - 更新样式以适应新功能的展示和交互 - 修改登录页面背景效果,提升用户体验
280 lines
8.4 KiB
TypeScript
280 lines
8.4 KiB
TypeScript
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',
|
||
},
|
||
});
|
||
|
||
|