feat: 更新健康数据功能和用户个人信息页面
- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据 - 重构健康数据获取逻辑,支持根据日期获取健康数据 - 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入 - 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果 - 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖 - 修改布局以支持新功能的显示和交互
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user