feat: 更新多个组件以优化用户体验和功能

- 在 CoachScreen 中移除不必要的 router 引入,简化代码结构
- 在 PersonalScreen 中移除未使用的 colorScheme 引入,优化组件性能
- 更新 NutritionRadarCard 组件,新增卡路里计算功能,提升营养数据展示
- 修改 Statistics 组件,调整样式以增强视觉效果
- 移除 iOS 项目中的多余健康数据权限设置,简化配置
This commit is contained in:
richarjiang
2025-08-25 17:41:42 +08:00
parent be0a8e7393
commit 91b7b0cb99
5 changed files with 244 additions and 33 deletions

View File

@@ -2,7 +2,7 @@ import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
@@ -115,10 +115,8 @@ const CardType = {
type CardType = typeof CardType[keyof typeof CardType]; type CardType = typeof CardType[keyof typeof CardType];
// const COACH_AVATAR = require('@/assets/images/logo.png');
export default function CoachScreen() { export default function CoachScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ name?: string }>(); const params = useLocalSearchParams<{ name?: string }>();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -290,10 +288,10 @@ export default function CoachScreen() {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} }
router.push('/mood/calendar'); pushIfAuthedElseLogin('/mood/calendar');
} }
}, },
], [router, planDraft, checkin]); ], [planDraft, checkin]);
const scrollToEnd = useCallback(() => { const scrollToEnd = useCallback(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {

View File

@@ -1,10 +1,8 @@
import ActivityHeatMap from '@/components/ActivityHeatMap'; import ActivityHeatMap from '@/components/ActivityHeatMap';
import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar'; import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice'; import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@@ -21,11 +19,9 @@ export default function PersonalScreen() {
const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const tabBarHeight = useBottomTabBarHeight(); const tabBarHeight = useBottomTabBarHeight();
const colorScheme = useColorScheme();
// 推送通知相关 // 推送通知相关
const { const {
isInitialized,
permissionStatus, permissionStatus,
requestPermission, requestPermission,
sendNotification, sendNotification,
@@ -38,9 +34,6 @@ export default function PersonalScreen() {
return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0); return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0);
}, [tabBarHeight, insets?.bottom]); }, [tabBarHeight, insets?.bottom]);
// 颜色主题
const colors = Colors[colorScheme ?? 'light'];
// 直接使用 Redux 中的用户信息,避免重复状态管理 // 直接使用 Redux 中的用户信息,避免重复状态管理
const userProfile = useAppSelector((state) => state.user.profile); const userProfile = useAppSelector((state) => state.user.profile);

View File

@@ -352,6 +352,14 @@ export default function ExploreScreen() {
{/* 营养摄入雷达图卡片 */} {/* 营养摄入雷达图卡片 */}
<NutritionRadarCard <NutritionRadarCard
nutritionSummary={nutritionSummary} nutritionSummary={nutritionSummary}
burnedCalories={basalMetabolism || 0}
calorieDeficit={0}
resetToken={animToken}
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
console.log('选择餐次:', mealType);
// 这里可以导航到营养记录页面
pushIfAuthedElseLogin('/nutrition/records');
}}
/> />
{/* 真正瀑布流布局 */} {/* 真正瀑布流布局 */}
@@ -720,13 +728,11 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 16, marginBottom: 16,
}, },
healthMetricsContainer: {
marginBottom: 16,
},
masonryContainer: { masonryContainer: {
marginBottom: 16, marginBottom: 16,
flexDirection: 'row', flexDirection: 'row',
gap: 12, gap: 12,
marginTop: 16,
}, },
masonryColumn: { masonryColumn: {
flex: 1, flex: 1,

View File

@@ -1,5 +1,7 @@
import { AnimatedNumber } from '@/components/AnimatedNumber';
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { NutritionSummary } from '@/services/dietRecords'; import { NutritionSummary } from '@/services/dietRecords';
import { Ionicons } from '@expo/vector-icons';
import Feather from '@expo/vector-icons/Feather'; import Feather from '@expo/vector-icons/Feather';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { router } from 'expo-router'; import { router } from 'expo-router';
@@ -9,6 +11,14 @@ import { RadarCategory, RadarChart } from './RadarChart';
export type NutritionRadarCardProps = { export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null; nutritionSummary: NutritionSummary | null;
/** 基础代谢消耗的卡路里 */
burnedCalories?: number;
/** 卡路里缺口 */
calorieDeficit?: number;
/** 动画重置令牌 */
resetToken?: number;
/** 餐次点击回调 */
onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void;
}; };
// 营养维度定义 // 营养维度定义
@@ -21,7 +31,13 @@ const NUTRITION_DIMENSIONS: RadarCategory[] = [
{ key: 'sodium', label: '钠' }, { key: 'sodium', label: '钠' },
]; ];
export function NutritionRadarCard({ nutritionSummary }: NutritionRadarCardProps) { export function NutritionRadarCard({
nutritionSummary,
burnedCalories = 1618,
calorieDeficit = 0,
resetToken,
onMealPress
}: NutritionRadarCardProps) {
const radarValues = useMemo(() => { const radarValues = useMemo(() => {
// 基于推荐日摄入量计算分数 // 基于推荐日摄入量计算分数
const recommendations = { const recommendations = {
@@ -35,13 +51,21 @@ export function NutritionRadarCard({ nutritionSummary }: NutritionRadarCardProps
if (!nutritionSummary) return [0, 0, 0, 0, 0, 0]; if (!nutritionSummary) return [0, 0, 0, 0, 0, 0];
// 检查每个营养素是否有实际值没有则返回0
const calories = nutritionSummary.totalCalories || 0;
const protein = nutritionSummary.totalProtein || 0;
const carbohydrate = nutritionSummary.totalCarbohydrate || 0;
const fat = nutritionSummary.totalFat || 0;
const fiber = nutritionSummary.totalFiber || 0;
const sodium = nutritionSummary.totalSodium || 0;
return [ return [
Math.min(5, (nutritionSummary.totalCalories / recommendations.calories) * 5), calories > 0 ? Math.min(5, (calories / recommendations.calories) * 5) : 0,
Math.min(5, (nutritionSummary.totalProtein / recommendations.protein) * 5), protein > 0 ? Math.min(5, (protein / recommendations.protein) * 5) : 0,
Math.min(5, (nutritionSummary.totalCarbohydrate / recommendations.carbohydrate) * 5), carbohydrate > 0 ? Math.min(5, (carbohydrate / recommendations.carbohydrate) * 5) : 0,
Math.min(5, (nutritionSummary.totalFat / recommendations.fat) * 5), fat > 0 ? Math.min(5, (fat / recommendations.fat) * 5) : 0,
Math.min(5, (nutritionSummary.totalFiber / recommendations.fiber) * 5), fiber > 0 ? Math.min(5, (fiber / recommendations.fiber) * 5) : 0,
Math.min(5, Math.max(0, 5 - (nutritionSummary.totalSodium / recommendations.sodium) * 5)), // 钠含量越低越好 sodium > 0 ? Math.min(5, Math.max(0, 5 - (sodium / recommendations.sodium) * 5)) : 0, // 钠含量越低越好
]; ];
}, [nutritionSummary]); }, [nutritionSummary]);
@@ -56,6 +80,34 @@ export function NutritionRadarCard({ nutritionSummary }: NutritionRadarCardProps
]; ];
}, [nutritionSummary]); }, [nutritionSummary]);
// 计算还能吃的卡路里
const consumedCalories = nutritionSummary?.totalCalories || 0;
const remainingCalories = burnedCalories - consumedCalories - calorieDeficit;
// 餐次数据
const meals = [
{
type: 'breakfast' as const,
name: '早餐',
emoji: '🥚',
},
{
type: 'lunch' as const,
name: '午餐',
emoji: '🍔',
},
{
type: 'dinner' as const,
name: '晚餐',
emoji: '🥣',
},
{
type: 'snack' as const,
name: '加餐',
emoji: '🍎',
},
];
const handleNavigateToRecords = () => { const handleNavigateToRecords = () => {
router.push(ROUTES.NUTRITION_RECORDS); router.push(ROUTES.NUTRITION_RECORDS);
}; };
@@ -90,6 +142,74 @@ export function NutritionRadarCard({ nutritionSummary }: NutritionRadarCardProps
))} ))}
</View> </View>
</View> </View>
{/* 卡路里计算区域 */}
<View style={styles.calorieSection}>
<View style={styles.calorieContent}>
<Text style={styles.calorieSubtitle}>()</Text>
<View style={styles.calculationRow}>
<AnimatedNumber
value={remainingCalories}
resetToken={resetToken}
style={styles.mainValue}
format={(v) => Math.round(v).toString()}
/>
<Text style={styles.calculationText}> = </Text>
<View style={styles.calculationItem}>
<Ionicons name="flame" size={16} color="#FF6B6B" />
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={burnedCalories}
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
value={consumedCalories}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
/>
<Text style={styles.calculationText}> - </Text>
<View style={styles.calculationItem}>
<Ionicons name="trending-down" size={16} color="#95A5A6" />
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={calorieDeficit}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
/>
</View>
</View>
{/* 餐次选择区域 */}
{/* <View style={styles.mealsContainer}>
{meals.map((meal) => (
<TouchableOpacity
key={meal.type}
style={styles.mealItem}
onPress={() => onMealPress?.(meal.type)}
activeOpacity={0.7}
>
<View style={styles.mealIconContainer}>
<Text style={styles.mealEmoji}>{meal.emoji}</Text>
<View style={styles.addButton}>
<Ionicons name="add" size={12} color="#FFFFFF" />
</View>
</View>
<Text style={styles.mealName}>{meal.name}</Text>
</TouchableOpacity>
))}
</View> */}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
} }
@@ -99,7 +219,6 @@ const styles = StyleSheet.create({
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 22, borderRadius: 22,
padding: 18, padding: 18,
marginBottom: 16,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
@@ -168,4 +287,103 @@ const styles = StyleSheet.create({
color: '#192126', color: '#192126',
fontWeight: '700', fontWeight: '700',
}, },
// 卡路里相关样式
calorieSection: {
marginTop: 12,
},
calorieTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
calorieIcon: {
fontSize: 16,
marginRight: 8,
},
calorieTitle: {
fontSize: 16,
fontWeight: '800',
color: '#192126',
},
calorieContent: {
},
calorieSubtitle: {
fontSize: 10,
color: '#64748B',
marginBottom: 8,
fontWeight: '600',
},
calculationRow: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 4,
},
mainValue: {
fontSize: 24,
fontWeight: '800',
color: '#192126',
},
calculationText: {
fontSize: 14,
fontWeight: '600',
color: '#64748B',
},
calculationItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 2,
},
calculationLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
calculationValue: {
fontSize: 12,
fontWeight: '700',
color: '#192126',
},
mealsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
},
mealItem: {
alignItems: 'center',
flex: 1,
},
mealIconContainer: {
position: 'relative',
marginBottom: 6,
},
mealEmoji: {
fontSize: 24,
},
addButton: {
position: 'absolute',
top: -2,
right: -2,
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#10B981',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
mealName: {
fontSize: 10,
color: '#64748B',
fontWeight: '600',
},
}); });

View File

@@ -10,10 +10,6 @@
</array> </array>
<key>com.apple.developer.healthkit</key> <key>com.apple.developer.healthkit</key>
<true/> <true/>
<key>com.apple.developer.healthkit.access</key>
<array>
<string>health-records</string>
</array>
<key>com.apple.developer.healthkit.background-delivery</key> <key>com.apple.developer.healthkit.background-delivery</key>
<true/> <true/>
</dict> </dict>