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

@@ -1,8 +1,10 @@
import { AnimatedNumber } from '@/components/AnimatedNumber';
import { CircularRing } from '@/components/CircularRing';
import { ProgressBar } from '@/components/ProgressBar';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchTodayHealthData } from '@/utils/health';
import { ensureHealthPermissions, fetchHealthDataForDate, fetchTodayHealthData } from '@/utils/health';
import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
@@ -52,8 +54,11 @@ export default function ExploreScreen() {
const [stepCount, setStepCount] = useState<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
// 用于触发动画重置的 token当日期或数据变化时更新
const [animToken, setAnimToken] = useState(0);
const [trainingProgress, setTrainingProgress] = useState(0.8); // 暂定静态80%
const loadHealthData = async () => {
const loadHealthData = async (targetDate?: Date) => {
try {
console.log('=== 开始HealthKit初始化流程 ===');
setIsLoading(true);
@@ -66,11 +71,12 @@ export default function ExploreScreen() {
}
console.log('权限获取成功,开始获取健康数据...');
const data = await fetchTodayHealthData();
const data = targetDate ? await fetchHealthDataForDate(targetDate) : await fetchTodayHealthData();
console.log('设置UI状态:', data);
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
setAnimToken((t) => t + 1);
console.log('=== HealthKit数据获取完成 ===');
} catch (error) {
@@ -86,6 +92,16 @@ export default function ExploreScreen() {
}, [])
);
// 日期点击时,加载对应日期数据
const onSelectDate = (index: number) => {
setSelectedIndex(index);
scrollToIndex(index);
const target = days[index]?.date?.toDate();
if (target) {
loadHealthData(target);
}
};
return (
<View style={styles.container}>
@@ -110,10 +126,7 @@ export default function ExploreScreen() {
<View key={`${d.dayOfMonth}`} style={styles.dayItemWrapper}>
<TouchableOpacity
style={[styles.dayPill, selected ? styles.dayPillSelected : styles.dayPillNormal]}
onPress={() => {
setSelectedIndex(i);
scrollToIndex(i);
}}
onPress={() => onSelectDate(i)}
activeOpacity={0.8}
>
<Text style={[styles.dayLabel, selected && styles.dayLabelSelected]}> {d.weekdayZh} </Text>
@@ -128,47 +141,52 @@ export default function ExploreScreen() {
{/* 今日报告 标题 */}
<Text style={styles.sectionTitle}></Text>
{/* 健康数据错误提示 */}
{isLoading && (
<View style={styles.errorContainer}>
<Ionicons name="warning-outline" size={20} color="#E54D4D" />
<Text style={styles.errorText}>...</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={loadHealthData} disabled={isLoading}
>
<Ionicons
name="refresh-outline"
size={16}
color={isLoading ? '#9AA3AE' : '#E54D4D'}
/>
</TouchableOpacity>
</View>
)}
{/* 取消卡片内 loading保持静默刷新提升体验 */}
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
<View style={styles.metricsRow}>
<View style={[styles.trainingCard, styles.metricsLeft]}>
<Text style={styles.cardTitleSecondary}></Text>
<View style={styles.trainingContent}>
<View style={styles.trainingRingTrack} />
<View style={styles.trainingRingProgress} />
<Text style={styles.trainingPercent}>80%</Text>
<CircularRing
size={120}
strokeWidth={12}
trackColor="#E2D9FD"
progressColor="#8B74F3"
progress={trainingProgress}
resetToken={animToken}
/>
</View>
</View>
<View style={styles.metricsRight}>
<View style={[styles.metricsRightCard, styles.caloriesCard, { minHeight: 88 }]}>
<Text style={styles.cardTitleSecondary}></Text>
<Text style={styles.caloriesValue}>
{isLoading ? '加载中...' : activeCalories != null ? `${activeCalories} 千卡` : '——'}
</Text>
{activeCalories != null ? (
<AnimatedNumber
value={activeCalories}
resetToken={animToken}
style={styles.caloriesValue}
format={(v) => `${Math.round(v)} 千卡`}
/>
) : (
<Text style={styles.caloriesValue}></Text>
)}
</View>
<View style={[styles.metricsRightCard, styles.stepsCard, { minHeight: 88 }]}>
<View style={styles.cardHeaderRow}>
<View style={styles.iconSquare}><Ionicons name="footsteps-outline" size={18} color="#192126" /></View>
<Text style={styles.cardTitle}></Text>
</View>
<Text style={styles.stepsValue}>{isLoading ? '加载中.../2000' : stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
{stepCount != null ? (
<AnimatedNumber
value={stepCount}
resetToken={animToken}
style={styles.stepsValue}
format={(v) => `${Math.round(v)}/2000`}
/>
) : (
<Text style={styles.stepsValue}>/2000</Text>
)}
<ProgressBar progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))} height={12} trackColor="#FFEBCB" fillColor="#FFC365" />
</View>
</View>