Files
digital-pilates/app/ai-posture-result.tsx
richarjiang 6a67fb21f7 feat: 更新应用名称和图标,优化用户界面
- 将应用名称修改为“Health Bot”,提升品牌识别度
- 更新应用图标为 logo.png,确保视觉一致性
- 删除不再使用的 ai-coach-chat 页面,简化代码结构
- 更新多个页面的导航和按钮文本,提升用户体验
- 添加体重历史记录功能,支持用户追踪健康数据
- 优化 Redux 状态管理,确保数据处理的准确性和稳定性
2025-08-17 21:34:04 +08:00

319 lines
9.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import { useRouter } from 'expo-router';
import React, { useMemo } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { RadarChart } from '@/components/RadarChart';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
type PoseView = 'front' | 'side' | 'back';
// 斯多特普拉提体态评估维度(示例)
const DIMENSIONS = [
{ key: 'head_neck', label: '头颈对齐' },
{ key: 'shoulder', label: '肩带稳定' },
{ key: 'ribs', label: '胸廓控制' },
{ key: 'pelvis', label: '骨盆中立' },
{ key: 'spine', label: '脊柱排列' },
{ key: 'hip_knee', label: '髋膝对线' },
];
type Issue = {
title: string;
severity: 'low' | 'medium' | 'high';
description: string;
suggestions: string[];
};
type ViewReport = {
score: number; // 0-5
issues: Issue[];
};
type ResultData = {
radar: number[]; // 与 DIMENSIONS 对应0-5
overview: string;
byView: Record<PoseView, ViewReport>;
};
// NOTE: 此处示例数据,后续可由 API 注入
const MOCK_RESULT: ResultData = {
radar: [4.2, 3.6, 3.2, 4.6, 3.8, 3.4],
overview: '整体体态较为均衡,骨盆与脊柱控制较好;肩带稳定性与胸廓控制仍有提升空间。',
byView: {
front: {
score: 3.8,
issues: [
{
title: '肩峰略前移,肩胛轻度外旋',
severity: 'medium',
description: '站立正面观察,右侧肩峰较左侧略有前移,提示肩带稳定性偏弱。',
suggestions: ['肩胛稳定训练(如天鹅摆臂分解)', '胸椎伸展与放松', '轻度弹力带外旋激活'],
},
],
},
side: {
score: 4.1,
issues: [
{
title: '骨盆接近中立,腰椎轻度前凸',
severity: 'low',
description: '侧面观察,骨盆位置接近中立位,腰椎存在轻度前凸,需注意腹压与肋骨下沉。',
suggestions: ['呼吸配合下的腹横肌激活', '猫牛流动改善胸椎灵活性'],
},
],
},
back: {
score: 3.5,
issues: [
{
title: '右侧肩胛轻度上抬',
severity: 'medium',
description: '背面观察,右肩胛较左侧轻度上抬,肩胛下回旋不足。',
suggestions: ['锯前肌激活训练', '低位划船,关注肩胛下沉与后缩'],
},
],
},
},
};
export default function AIPostureResultScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const theme = Colors.light;
const categories = useMemo(() => DIMENSIONS.map(d => ({ key: d.key, label: d.label })), []);
const ScoreBadge = ({ score }: { score: number }) => (
<View style={styles.scoreBadge}>
<Text style={styles.scoreText}>{score.toFixed(1)}</Text>
<Text style={styles.scoreUnit}>/5</Text>
</View>
);
const IssueItem = ({ issue }: { issue: Issue }) => (
<View style={styles.issueItem}>
<View style={[styles.issueDot, issue.severity === 'high' ? styles.dotHigh : issue.severity === 'medium' ? styles.dotMedium : styles.dotLow]} />
<View style={{ flex: 1 }}>
<Text style={styles.issueTitle}>{issue.title}</Text>
<Text style={styles.issueDesc}>{issue.description}</Text>
{!!issue.suggestions?.length && (
<View style={styles.suggestRow}>
{issue.suggestions.map((s, idx) => (
<View key={idx} style={styles.suggestChip}><Text style={styles.suggestText}>{s}</Text></View>
))}
</View>
)}
</View>
</View>
);
const ViewCard = ({ title, report }: { title: string; report: ViewReport }) => (
<Animated.View entering={FadeInDown.duration(400)} style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text>
<ScoreBadge score={report.score} />
</View>
{report.issues.map((iss, idx) => (<IssueItem key={idx} issue={iss} />))}
</Animated.View>
);
return (
<View style={[styles.screen, { backgroundColor: theme.background }]}>
<HeaderBar title="体态评估结果" onBack={() => router.back()} tone="light" transparent />
{/* 背景装饰 */}
<View style={[StyleSheet.absoluteFill, { zIndex: -1 }]} pointerEvents="none">
<BlurView intensity={20} tint="light" style={styles.bgBlobA} />
<BlurView intensity={20} tint="light" style={styles.bgBlobB} />
</View>
<ScrollView contentContainerStyle={{ paddingBottom: insets.bottom + 40 }} showsVerticalScrollIndicator={false}>
{/* 总览与雷达图 */}
<Animated.View entering={FadeInDown.duration(400)} style={styles.card}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.overview}>{MOCK_RESULT.overview}</Text>
<View style={styles.radarWrap}>
<RadarChart categories={categories} values={MOCK_RESULT.radar} />
</View>
</Animated.View>
{/* 视图分析 */}
<ViewCard title="正面视图" report={MOCK_RESULT.byView.front} />
<ViewCard title="侧面视图" report={MOCK_RESULT.byView.side} />
<ViewCard title="背面视图" report={MOCK_RESULT.byView.back} />
{/* 底部操作 */}
<View style={styles.actions}>
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: theme.primary }]} onPress={() => router.replace('/(tabs)/personal')}>
<Ionicons name="checkmark-circle" size={18} color={theme.onPrimary} />
<Text style={[styles.primaryBtnText, { color: theme.onPrimary }]}></Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.secondaryBtn, { borderColor: theme.border }]} onPress={() => router.push('/(tabs)/coach')}>
<Text style={[styles.secondaryBtnText, { color: theme.text }]}></Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
},
bgBlobA: {
position: 'absolute',
top: -60,
right: -40,
width: 200,
height: 200,
borderRadius: 100,
backgroundColor: 'rgba(187,242,70,0.18)',
},
bgBlobB: {
position: 'absolute',
bottom: 100,
left: -30,
width: 180,
height: 180,
borderRadius: 90,
backgroundColor: 'rgba(89, 198, 255, 0.16)',
},
card: {
marginTop: 16,
marginHorizontal: 16,
borderRadius: 16,
padding: 14,
backgroundColor: 'rgba(255,255,255,0.72)',
borderWidth: 1,
borderColor: 'rgba(25,33,38,0.08)',
},
sectionTitle: {
color: '#192126',
fontSize: 16,
fontWeight: '800',
marginBottom: 8,
},
overview: {
color: '#384046',
fontSize: 14,
lineHeight: 20,
},
radarWrap: {
marginTop: 10,
alignItems: 'center',
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
cardTitle: {
color: '#192126',
fontSize: 15,
fontWeight: '700',
},
scoreBadge: {
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: 'rgba(187,242,70,0.16)',
},
scoreText: {
color: '#192126',
fontSize: 18,
fontWeight: '800',
},
scoreUnit: {
color: '#5E6468',
fontSize: 12,
marginLeft: 4,
},
issueItem: {
flexDirection: 'row',
gap: 10,
paddingVertical: 10,
},
issueDot: {
width: 10,
height: 10,
borderRadius: 5,
marginTop: 6,
},
dotHigh: { backgroundColor: '#E24D4D' },
dotMedium: { backgroundColor: '#F0C23C' },
dotLow: { backgroundColor: '#2BCC7F' },
issueTitle: {
color: '#192126',
fontSize: 14,
fontWeight: '700',
},
issueDesc: {
color: '#5E6468',
fontSize: 13,
marginTop: 4,
},
suggestRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginTop: 8,
},
suggestChip: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 12,
backgroundColor: 'rgba(25,33,38,0.04)',
borderWidth: 1,
borderColor: 'rgba(25,33,38,0.08)',
},
suggestText: {
color: '#192126',
fontSize: 12,
},
actions: {
marginTop: 16,
paddingHorizontal: 16,
flexDirection: 'row',
gap: 10,
},
primaryBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
height: 48,
paddingHorizontal: 16,
borderRadius: 14,
},
primaryBtnText: {
color: '#192126',
fontSize: 15,
fontWeight: '800',
},
secondaryBtn: {
flex: 1,
height: 48,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'transparent',
},
secondaryBtnText: {
color: '#384046',
fontSize: 15,
fontWeight: '700',
},
});