feat(notifications): 新增晚餐和心情提醒功能,支持HRV压力检测和后台处理
- 新增晚餐提醒(18:00)和心情提醒(21:00)的定时通知 - 实现基于HRV数据的压力检测和智能鼓励通知 - 添加后台任务处理支持,修改iOS后台模式为processing - 优化营养记录页面使用Redux状态管理,支持实时数据更新 - 重构卡路里计算公式,移除目标卡路里概念,改为基代+运动-饮食 - 新增营养目标动态计算功能,基于用户身体数据智能推荐 - 完善通知点击跳转逻辑,支持多种提醒类型的路由处理
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { NutritionSummary } from '@/services/dietRecords';
|
||||
import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { router } from 'expo-router';
|
||||
@@ -11,10 +12,15 @@ import { RadarCategory, RadarChart } from './RadarChart';
|
||||
|
||||
export type NutritionRadarCardProps = {
|
||||
nutritionSummary: NutritionSummary | null;
|
||||
/** 营养目标 */
|
||||
nutritionGoals?: NutritionGoals;
|
||||
/** 基础代谢消耗的卡路里 */
|
||||
burnedCalories?: number;
|
||||
/** 卡路里缺口 */
|
||||
calorieDeficit?: number;
|
||||
/** 基础代谢率 */
|
||||
basalMetabolism?: number;
|
||||
/** 运动消耗卡路里 */
|
||||
activeCalories?: number;
|
||||
|
||||
/** 动画重置令牌 */
|
||||
resetToken?: number;
|
||||
/** 餐次点击回调 */
|
||||
@@ -33,21 +39,24 @@ const NUTRITION_DIMENSIONS: RadarCategory[] = [
|
||||
|
||||
export function NutritionRadarCard({
|
||||
nutritionSummary,
|
||||
nutritionGoals,
|
||||
burnedCalories = 1618,
|
||||
calorieDeficit = 0,
|
||||
basalMetabolism,
|
||||
activeCalories,
|
||||
|
||||
resetToken,
|
||||
onMealPress
|
||||
}: NutritionRadarCardProps) {
|
||||
const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||
const radarValues = useMemo(() => {
|
||||
// 基于推荐日摄入量计算分数
|
||||
// 基于动态计算的营养目标或默认推荐值
|
||||
const recommendations = {
|
||||
calories: 2000, // 卡路里
|
||||
protein: 50, // 蛋白质(g)
|
||||
carbohydrate: 300, // 碳水化合物(g)
|
||||
fat: 65, // 脂肪(g)
|
||||
fiber: 25, // 膳食纤维(g)
|
||||
sodium: 2300, // 钠(mg)
|
||||
calories: nutritionGoals?.calories ?? 2000, // 卡路里
|
||||
protein: nutritionGoals?.proteinGoal ?? 50, // 蛋白质(g)
|
||||
carbohydrate: nutritionGoals?.carbsGoal ?? 300, // 碳水化合物(g)
|
||||
fat: nutritionGoals?.fatGoal ?? 65, // 脂肪(g)
|
||||
fiber: nutritionGoals?.fiberGoal ?? 25, // 膳食纤维(g)
|
||||
sodium: nutritionGoals?.sodiumGoal ?? 2300, // 钠(mg)
|
||||
};
|
||||
|
||||
if (!nutritionSummary) return [0, 0, 0, 0, 0, 0];
|
||||
@@ -68,7 +77,7 @@ export function NutritionRadarCard({
|
||||
fiber > 0 ? Math.min(5, (fiber / recommendations.fiber) * 5) : 0,
|
||||
sodium > 0 ? Math.min(5, Math.max(0, 5 - (sodium / recommendations.sodium) * 5)) : 0, // 钠含量越低越好
|
||||
];
|
||||
}, [nutritionSummary]);
|
||||
}, [nutritionSummary, nutritionGoals]);
|
||||
|
||||
const nutritionStats = useMemo(() => {
|
||||
return [
|
||||
@@ -83,7 +92,16 @@ export function NutritionRadarCard({
|
||||
|
||||
// 计算还能吃的卡路里
|
||||
const consumedCalories = nutritionSummary?.totalCalories || 0;
|
||||
const remainingCalories = burnedCalories - consumedCalories - calorieDeficit;
|
||||
|
||||
// 使用分离的代谢和运动数据,如果没有提供则从burnedCalories推算
|
||||
const effectiveBasalMetabolism = basalMetabolism ?? (burnedCalories * 0.7); // 假设70%是基础代谢
|
||||
const effectiveActiveCalories = activeCalories ?? (burnedCalories * 0.3); // 假设30%是运动消耗
|
||||
|
||||
const remainingCalories = calculateRemainingCalories({
|
||||
basalMetabolism: effectiveBasalMetabolism,
|
||||
activeCalories: effectiveActiveCalories,
|
||||
consumedCalories,
|
||||
});
|
||||
|
||||
const handleNavigateToRecords = () => {
|
||||
router.push(ROUTES.NUTRITION_RECORDS);
|
||||
@@ -129,27 +147,38 @@ export function NutritionRadarCard({
|
||||
<View style={styles.calorieSection}>
|
||||
<View style={styles.calorieContent}>
|
||||
<View style={styles.calculationRow}>
|
||||
<Text style={styles.calorieSubtitle}>还能吃(千卡)</Text>
|
||||
<AnimatedNumber
|
||||
value={remainingCalories}
|
||||
resetToken={resetToken}
|
||||
style={styles.mainValue}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
<Text style={styles.calorieSubtitle}>还能吃</Text>
|
||||
<View style={styles.remainingCaloriesContainer}>
|
||||
<AnimatedNumber
|
||||
value={remainingCalories}
|
||||
resetToken={resetToken}
|
||||
style={styles.mainValue}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
<Text style={styles.calorieUnit}>千卡</Text>
|
||||
</View>
|
||||
<Text style={styles.calculationText}> = </Text>
|
||||
<View style={styles.calculationItem}>
|
||||
<Ionicons name="flame" size={16} color="#FF6B6B" />
|
||||
<Text style={styles.calculationLabel}>消耗</Text>
|
||||
<Text style={styles.calculationLabel}>基代</Text>
|
||||
</View>
|
||||
<AnimatedNumber
|
||||
value={burnedCalories}
|
||||
value={effectiveBasalMetabolism}
|
||||
resetToken={resetToken}
|
||||
style={styles.calculationValue}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
<Text style={styles.calculationText}> + </Text>
|
||||
<View style={styles.calculationItem}>
|
||||
<Text style={styles.calculationLabel}>运动</Text>
|
||||
</View>
|
||||
<AnimatedNumber
|
||||
value={effectiveActiveCalories}
|
||||
resetToken={resetToken}
|
||||
style={styles.calculationValue}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
<Text style={styles.calculationText}> - </Text>
|
||||
<View style={styles.calculationItem}>
|
||||
<Ionicons name="restaurant" size={16} color="#4ECDC4" />
|
||||
<Text style={styles.calculationLabel}>饮食</Text>
|
||||
</View>
|
||||
<AnimatedNumber
|
||||
@@ -158,6 +187,7 @@ export function NutritionRadarCard({
|
||||
style={styles.calculationValue}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -270,12 +300,12 @@ const styles = StyleSheet.create({
|
||||
gap: 4,
|
||||
},
|
||||
mainValue: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#192126',
|
||||
},
|
||||
calculationText: {
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#64748B',
|
||||
},
|
||||
@@ -285,15 +315,25 @@ const styles = StyleSheet.create({
|
||||
gap: 2,
|
||||
},
|
||||
calculationLabel: {
|
||||
fontSize: 8,
|
||||
fontSize: 9,
|
||||
color: '#64748B',
|
||||
fontWeight: '500',
|
||||
},
|
||||
calculationValue: {
|
||||
fontSize: 10,
|
||||
fontSize: 9,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
},
|
||||
remainingCaloriesContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
gap: 2,
|
||||
},
|
||||
calorieUnit: {
|
||||
fontSize: 10,
|
||||
color: '#64748B',
|
||||
fontWeight: '500',
|
||||
},
|
||||
mealsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
Reference in New Issue
Block a user