feat: 更新健康数据功能和用户个人信息页面

- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据
- 重构健康数据获取逻辑,支持根据日期获取健康数据
- 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入
- 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果
- 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖
- 修改布局以支持新功能的显示和交互
This commit is contained in:
richarjiang
2025-08-12 18:54:15 +08:00
parent 2fac3f899c
commit 8ffebfb297
14 changed files with 1034 additions and 72 deletions

View File

@@ -4,7 +4,9 @@ import { useColorScheme } from '@/hooks/useColorScheme';
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import React, { useMemo, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import {
Alert,
SafeAreaView,
@@ -29,6 +31,71 @@ export default function PersonalScreen() {
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
type WeightUnit = 'kg' | 'lb';
type HeightUnit = 'cm' | 'ft';
type UserProfile = {
fullName?: string;
email?: string;
gender?: 'male' | 'female' | '';
age?: string;
weightKg?: number;
heightCm?: number;
unitPref?: { weight: WeightUnit; height: HeightUnit };
};
const [profile, setProfile] = useState<UserProfile>({});
const load = async () => {
try {
const [p, o] = await Promise.all([
AsyncStorage.getItem('@user_profile'),
AsyncStorage.getItem('@user_personal_info'),
]);
let next: UserProfile = {};
if (o) {
try {
const parsed = JSON.parse(o);
next = {
...next,
age: parsed?.age ? String(parsed.age) : undefined,
gender: parsed?.gender || '',
heightCm: parsed?.height ? parseFloat(parsed.height) : undefined,
weightKg: parsed?.weight ? parseFloat(parsed.weight) : undefined,
};
} catch { }
}
if (p) {
try { next = { ...next, ...JSON.parse(p) }; } catch { }
}
setProfile(next);
} catch (e) {
console.warn('加载用户资料失败', e);
}
};
useEffect(() => { load(); }, []);
useFocusEffect(React.useCallback(() => { load(); return () => { }; }, []));
const formatHeight = () => {
if (profile.heightCm == null) return '--';
const unit = profile.unitPref?.height ?? 'cm';
if (unit === 'cm') return `${Math.round(profile.heightCm)}cm`;
return `${round(profile.heightCm / 30.48, 1)}ft`;
};
const formatWeight = () => {
if (profile.weightKg == null) return '--';
const unit = profile.unitPref?.weight ?? 'kg';
if (unit === 'kg') return `${round(profile.weightKg, 1)}kg`;
return `${round(profile.weightKg * 2.2046226218, 1)}lb`;
};
const formatAge = () => (profile.age ? `${profile.age}` : '--');
const round = (n: number, d = 0) => {
const p = Math.pow(10, d); return Math.round(n * p) / p;
};
const handleResetOnboarding = () => {
Alert.alert(
'重置引导',
@@ -74,13 +141,13 @@ export default function PersonalScreen() {
{/* 用户信息 */}
<View style={styles.userDetails}>
<Text style={styles.userName}>Masi Ramezanzade</Text>
<Text style={styles.userProgram}>Lose a Fat Program</Text>
<Text style={styles.userName}>{profile.fullName || '未设置姓名'}</Text>
<Text style={styles.userProgram}></Text>
</View>
{/* 编辑按钮 */}
<TouchableOpacity style={dynamicStyles.editButton}>
<Text style={dynamicStyles.editButtonText}>Edit</Text>
<TouchableOpacity style={dynamicStyles.editButton} onPress={() => router.push('/profile/edit')}>
<Text style={dynamicStyles.editButtonText}></Text>
</TouchableOpacity>
</View>
</View>
@@ -89,16 +156,16 @@ export default function PersonalScreen() {
const StatsSection = () => (
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>180cm</Text>
<Text style={styles.statLabel}>Height</Text>
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text>
<Text style={styles.statLabel}></Text>
</View>
<View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>65kg</Text>
<Text style={styles.statLabel}>Weight</Text>
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text>
<Text style={styles.statLabel}></Text>
</View>
<View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>22yo</Text>
<Text style={styles.statLabel}>Age</Text>
<Text style={dynamicStyles.statValue}>{formatAge()}</Text>
<Text style={styles.statLabel}></Text>
</View>
</View>
);
@@ -176,25 +243,26 @@ export default function PersonalScreen() {
icon: 'person-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Personal Data',
title: '个人资料',
onPress: () => router.push('/profile/edit'),
},
{
icon: 'trophy-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Achievement',
title: '成就',
},
{
icon: 'time-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Activity History',
title: '活动历史',
},
{
icon: 'stats-chart-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Workout Progress',
title: '训练进度',
},
];
@@ -203,7 +271,7 @@ export default function PersonalScreen() {
icon: 'notifications-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Pop-up Notification',
title: '弹窗通知',
type: 'switch',
},
];
@@ -213,19 +281,19 @@ export default function PersonalScreen() {
icon: 'mail-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Contact Us',
title: '联系我们',
},
{
icon: 'shield-checkmark-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Privacy Policy',
title: '隐私政策',
},
{
icon: 'settings-outline',
iconBg: '#E8F5E8',
iconColor: '#4ADE80',
title: 'Settings',
title: '设置',
},
];
@@ -250,10 +318,10 @@ export default function PersonalScreen() {
>
<UserInfoSection />
<StatsSection />
<MenuSection title="Account" items={accountItems} />
<MenuSection title="Notification" items={notificationItems} />
<MenuSection title="Other" items={otherItems} />
<MenuSection title="Developer" items={developerItems} />
<MenuSection title="账户" items={accountItems} />
<MenuSection title="通知" items={notificationItems} />
<MenuSection title="其他" items={otherItems} />
<MenuSection title="开发者" items={developerItems} />
{/* 底部浮动按钮 */}
<View style={[styles.floatingButtonContainer, { bottom: Math.max(30, tabBarHeight / 2) + (insets?.bottom ?? 0) }]}>