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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user