feat(health): 重构营养卡片数据获取逻辑,支持基础代谢与运动消耗分离

- 新增 fetchCompleteNutritionCardData 异步 action,统一拉取营养、健康与基础代谢数据
- NutritionRadarCard 改用 Redux 数据源,移除 props 透传,自动根据日期刷新
- BasalMetabolismCard 新增详情弹窗,展示 BMR 计算公式、正常区间及提升策略
- StepsCard 与 StepsCardOptimized 引入 InteractionManager 与动画懒加载,减少 UI 阻塞
- HealthKitManager 新增饮水读写接口,支持将饮水记录同步至 HealthKit
- 移除 statistics 页面冗余 mock 与 nutrition/health 重复请求,缓存时间统一为 5 分钟
This commit is contained in:
richarjiang
2025-09-23 10:01:50 +08:00
parent d082c66b72
commit e6dfd4d59a
11 changed files with 1115 additions and 203 deletions

View File

@@ -1,7 +1,8 @@
import { AnimatedNumber } from '@/components/AnimatedNumber';
import { ROUTES } from '@/constants/Routes';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { NutritionSummary } from '@/services/dietRecords';
import { fetchCompleteNutritionCardData, selectNutritionCardDataByDate } from '@/store/nutritionSlice';
import { triggerLightHaptic } from '@/utils/haptics';
import { calculateRemainingCalories } from '@/utils/nutrition';
import dayjs from 'dayjs';
@@ -14,14 +15,8 @@ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null;
/** 基础代谢消耗的卡路里 */
burnedCalories?: number;
/** 基础代谢率 */
basalMetabolism?: number;
/** 运动消耗卡路里 */
activeCalories?: number;
selectedDate?: Date;
style?: object;
/** 动画重置令牌 */
resetToken?: number;
};
@@ -93,15 +88,40 @@ const SimpleRingProgress = ({
};
export function NutritionRadarCard({
nutritionSummary,
burnedCalories = 1618,
basalMetabolism,
activeCalories,
selectedDate,
style,
resetToken,
}: NutritionRadarCardProps) {
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
const [loading, setLoading] = useState(false);
const { pushIfAuthedElseLogin } = useAuthGuard()
const { pushIfAuthedElseLogin } = useAuthGuard();
const dispatch = useAppDispatch();
const dateKey = useMemo(() => {
return selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
}, [selectedDate]);
const cardData = useAppSelector(selectNutritionCardDataByDate(dateKey));
const { nutritionSummary, healthData, basalMetabolism } = cardData;
// 获取营养和健康数据
useEffect(() => {
const loadNutritionCardData = async () => {
const targetDate = selectedDate || new Date();
try {
setLoading(true);
await dispatch(fetchCompleteNutritionCardData(targetDate)).unwrap();
} catch (error) {
console.error('NutritionRadarCard: 获取营养卡片数据失败:', error);
} finally {
setLoading(false);
}
};
loadNutritionCardData();
}, [selectedDate, dispatch]);
const nutritionStats = useMemo(() => {
return [
@@ -117,9 +137,9 @@ export function NutritionRadarCard({
// 计算还能吃的卡路里
const consumedCalories = nutritionSummary?.totalCalories || 0;
// 使用分离的代谢和运动数据如果没有提供则从burnedCalories推算
const effectiveBasalMetabolism = basalMetabolism ?? (burnedCalories * 0.7); // 假设70%是基础代谢
const effectiveActiveCalories = activeCalories ?? (burnedCalories * 0.3); // 假设30%是运动消耗
// 使用从HealthKit获取的数据如果没有则使用默认值
const effectiveBasalMetabolism = basalMetabolism || 0; // 基础代谢默认值
const effectiveActiveCalories = healthData?.activeCalories || 0; // 运动消耗卡路里
const remainingCalories = calculateRemainingCalories({
basalMetabolism: effectiveBasalMetabolism,
@@ -134,7 +154,7 @@ export function NutritionRadarCard({
return (
<TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}>
<TouchableOpacity style={[styles.card, style]} onPress={handleNavigateToRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}>
<View style={styles.titleContainer}>
<Image
@@ -143,14 +163,16 @@ export function NutritionRadarCard({
/>
<Text style={styles.cardTitle}></Text>
</View>
<Text style={styles.cardSubtitle}>: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text>
<Text style={styles.cardSubtitle}>
{loading ? '加载中...' : `更新: ${dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}`}
</Text>
</View>
<View style={styles.contentContainer}>
<View style={styles.radarContainer}>
<SimpleRingProgress
remainingCalories={remainingCalories}
totalAvailable={effectiveBasalMetabolism + effectiveActiveCalories}
remainingCalories={loading ? 0 : remainingCalories}
totalAvailable={loading ? 0 : effectiveBasalMetabolism + effectiveActiveCalories}
/>
</View>
@@ -173,10 +195,10 @@ export function NutritionRadarCard({
<Text style={styles.calorieSubtitle}></Text>
<View style={styles.remainingCaloriesContainer}>
<AnimatedNumber
value={remainingCalories}
value={loading ? 0 : remainingCalories}
resetToken={resetToken}
style={styles.mainValue}
format={(v) => Math.round(v).toString()}
format={(v) => loading ? '--' : Math.round(v).toString()}
/>
<Text style={styles.calorieUnit}></Text>
</View>
@@ -185,30 +207,30 @@ export function NutritionRadarCard({
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={effectiveBasalMetabolism}
value={loading ? 0 : effectiveBasalMetabolism}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
format={(v) => loading ? '--' : Math.round(v).toString()}
/>
<Text style={styles.calculationText}> + </Text>
<View style={styles.calculationItem}>
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={effectiveActiveCalories}
value={loading ? 0 : effectiveActiveCalories}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
format={(v) => loading ? '--' : Math.round(v).toString()}
/>
<Text style={styles.calculationText}> - </Text>
<View style={styles.calculationItem}>
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={consumedCalories}
value={loading ? 0 : consumedCalories}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
format={(v) => loading ? '--' : Math.round(v).toString()}
/>
</View>