diff --git a/app/(tabs)/goals.tsx b/app/(tabs)/goals.tsx index 79427a9..972804f 100644 --- a/app/(tabs)/goals.tsx +++ b/app/(tabs)/goals.tsx @@ -109,6 +109,8 @@ export default function GoalsScreen() { const onRefresh = async () => { setRefreshing(true); try { + if (!isLoggedIn) return + await loadTasks(); } finally { setRefreshing(false); @@ -117,6 +119,8 @@ export default function GoalsScreen() { // 加载更多任务 const handleLoadMoreTasks = async () => { + if (!isLoggedIn) return + if (tasksPagination.hasMore && !tasksLoading) { try { await dispatch(loadMoreTasks()).unwrap(); @@ -319,6 +323,61 @@ export default function GoalsScreen() { // 渲染空状态 const renderEmptyState = () => { + // 未登录状态下的引导 + if (!isLoggedIn) { + return ( + + + + + {/* 清新的图标设计 */} + + + + + + + {/* 主标题 */} + + 开启您的健康之旅 + + + {/* 副标题 */} + + 登录后即可创建个人目标,让我们一起建立健康的生活习惯 + + + {/* 登录按钮 */} + pushIfAuthedElseLogin('/goals')} + > + + 立即登录 + + + + + + ); + } + + // 已登录但无任务的状态 let title = '暂无任务'; let subtitle = '创建目标后,系统会自动生成相应的任务'; @@ -710,6 +769,80 @@ const styles = StyleSheet.create({ textAlign: 'center', lineHeight: 20, }, + // 未登录空状态样式 + emptyStateLogin: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 24, + paddingVertical: 80, + position: 'relative', + }, + emptyStateLoginBackground: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + borderRadius: 24, + }, + emptyStateLoginContent: { + alignItems: 'center', + justifyContent: 'center', + zIndex: 1, + }, + emptyStateLoginIconContainer: { + marginBottom: 24, + shadowColor: '#7A5AF8', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.15, + shadowRadius: 16, + elevation: 8, + }, + emptyStateLoginIconGradient: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + }, + emptyStateLoginTitle: { + fontSize: 24, + fontWeight: '700', + marginBottom: 12, + textAlign: 'center', + letterSpacing: -0.5, + }, + emptyStateLoginSubtitle: { + fontSize: 16, + lineHeight: 24, + textAlign: 'center', + marginBottom: 32, + paddingHorizontal: 8, + }, + emptyStateLoginButton: { + borderRadius: 28, + shadowColor: '#7A5AF8', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 6, + }, + emptyStateLoginButtonGradient: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + paddingVertical: 16, + borderRadius: 28, + gap: 8, + }, + emptyStateLoginButtonText: { + color: '#FFFFFF', + fontSize: 17, + fontWeight: '600', + letterSpacing: -0.2, + }, loadMoreContainer: { alignItems: 'center', paddingVertical: 20, diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index 96b6237..61d90ee 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -9,8 +9,6 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto import { getItem, setItem } from '@/utils/kvStore'; import { log } from '@/utils/logger'; import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences'; -import { Button, Host, Text as SwiftText } from '@expo/ui/swift-ui'; -import { frame, glassEffect } from '@expo/ui/swift-ui/modifiers'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { isLiquidGlassAvailable } from 'expo-glass-effect'; @@ -215,32 +213,9 @@ export default function PersonalScreen() { {displayName} - {isLgAvaliable ? - - : pushIfAuthedElseLogin('/profile/edit')}> + pushIfAuthedElseLogin('/profile/edit')}> {isLoggedIn ? '编辑' : '登录'} - } - + diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 413a464..6be9e8e 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -20,7 +20,6 @@ import { fetchTodayWaterStats } from '@/store/waterSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health'; import { getTestHealthData } from '@/utils/mockHealthData'; -import { calculateNutritionGoals } from '@/utils/nutrition'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { debounce } from 'lodash'; @@ -92,8 +91,6 @@ export default function ExploreScreen() { const activeCalories = useMockData ? (mockData?.activeEnergyBurned ?? null) : (healthData?.activeEnergyBurned ?? null); const basalMetabolism: number | null = useMockData ? (mockData?.basalEnergyBurned ?? null) : (healthData?.basalEnergyBurned ?? null); - const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null); - // 用于触发动画重置的 token(当日期或数据变化时更新) @@ -102,15 +99,6 @@ export default function ExploreScreen() { // 从 Redux 获取营养数据 const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString)); - // 计算用户的营养目标 - const nutritionGoals = useMemo(() => { - return calculateNutritionGoals({ - weight: userProfile.weight, - height: userProfile.height, - birthDate: userProfile?.birthDate ? new Date(userProfile?.birthDate) : undefined, - gender: userProfile?.gender || undefined, - }); - }, [userProfile]); // 心情相关状态 const dispatch = useAppDispatch(); @@ -226,13 +214,6 @@ export default function ExploreScreen() { loadingRef.current.health = true; console.log('=== 开始HealthKit初始化流程 ==='); - // const ok = await ensureHealthPermissions(); - // if (!ok) { - // const errorMsg = '无法获取健康权限,请确保在真实iOS设备上运行并授权应用访问健康数据'; - // console.warn(errorMsg); - // return; - // } - latestRequestKeyRef.current = requestKey; console.log('权限获取成功,开始获取健康数据...', derivedDate); @@ -251,7 +232,6 @@ export default function ExploreScreen() { activeCalories: data.activeEnergyBurned, basalEnergyBurned: data.basalEnergyBurned, hrv: data.hrv, - oxygenSaturation: data.oxygenSaturation, heartRate: data.heartRate, activeEnergyBurned: data.activeEnergyBurned, activeCaloriesGoal: data.activeCaloriesGoal, @@ -358,14 +338,6 @@ export default function ExploreScreen() { loadAllData(currentSelectedDate); }, []) - // 页面聚焦时的数据加载逻辑 - // useFocusEffect( - // React.useCallback(() => { - // // 页面聚焦时加载数据,使用缓存机制避免频繁请求 - // console.log('页面聚焦,检查是否需要刷新数据...'); - // loadAllData(currentSelectedDate); - // }, [loadAllData, currentSelectedDate]) - // ); // AppState 监听:应用从后台返回前台时的处理 useEffect(() => { @@ -487,16 +459,10 @@ export default function ExploreScreen() { {/* 营养摄入雷达图卡片 */} { - console.log('选择餐次:', mealType); - // 这里可以导航到营养记录页面 - pushIfAuthedElseLogin('/nutrition/records'); - }} /> @@ -575,9 +541,7 @@ export default function ExploreScreen() { {/* 血氧饱和度卡片 */} diff --git a/app/nutrition/records.tsx b/app/nutrition/records.tsx index 6cbb26a..4d4ff75 100644 --- a/app/nutrition/records.tsx +++ b/app/nutrition/records.tsx @@ -300,42 +300,6 @@ export default function NutritionRecordsScreen() { }); }; - // 渲染视图模式切换器 - const renderViewModeToggle = () => ( - - {monthTitle} - - setViewMode('daily')} - > - - 按天查看 - - - setViewMode('all')} - > - - 全部记录 - - - - - ); // 渲染日期选择器(仅在按天查看模式下显示) const renderDateSelector = () => { diff --git a/components/FloatingFoodOverlay.tsx b/components/FloatingFoodOverlay.tsx index cf80291..f565dea 100644 --- a/components/FloatingFoodOverlay.tsx +++ b/components/FloatingFoodOverlay.tsx @@ -1,4 +1,5 @@ import { ROUTES } from '@/constants/Routes'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { Ionicons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import { useRouter } from 'expo-router'; @@ -20,19 +21,21 @@ interface FloatingFoodOverlayProps { export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) { const router = useRouter(); + const { pushIfAuthedElseLogin } = useAuthGuard() + const handleFoodLibrary = () => { onClose(); - router.push(`${ROUTES.FOOD_LIBRARY}?mealType=${mealType}`); + pushIfAuthedElseLogin(`${ROUTES.FOOD_LIBRARY}?mealType=${mealType}`); }; const handlePhotoRecognition = () => { onClose(); - router.push(`/food/camera?mealType=${mealType}`); + pushIfAuthedElseLogin(`/food/camera?mealType=${mealType}`); }; const handleVoiceRecord = () => { onClose(); - router.push(`${ROUTES.VOICE_RECORD}?mealType=${mealType}`); + pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${mealType}`); }; const menuItems = [ diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 2c4f297..bc3ed29 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -1,8 +1,9 @@ import { AnimatedNumber } from '@/components/AnimatedNumber'; import { ROUTES } from '@/constants/Routes'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { NutritionSummary } from '@/services/dietRecords'; import { triggerLightHaptic } from '@/utils/haptics'; -import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition'; +import { calculateRemainingCalories } from '@/utils/nutrition'; import dayjs from 'dayjs'; import { router } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -14,8 +15,6 @@ const AnimatedCircle = Animated.createAnimatedComponent(Circle); export type NutritionRadarCardProps = { nutritionSummary: NutritionSummary | null; - /** 营养目标 */ - nutritionGoals?: NutritionGoals; /** 基础代谢消耗的卡路里 */ burnedCalories?: number; /** 基础代谢率 */ @@ -25,8 +24,6 @@ export type NutritionRadarCardProps = { /** 动画重置令牌 */ resetToken?: number; - /** 餐次点击回调 */ - onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void; }; // 简化的圆环进度组件 @@ -97,16 +94,15 @@ const SimpleRingProgress = ({ export function NutritionRadarCard({ nutritionSummary, - nutritionGoals, burnedCalories = 1618, basalMetabolism, activeCalories, - resetToken, - onMealPress }: NutritionRadarCardProps) { const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); + const { pushIfAuthedElseLogin } = useAuthGuard() + const nutritionStats = useMemo(() => { return [ { label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' }, @@ -225,7 +221,7 @@ export function NutritionRadarCard({ style={styles.foodOptionItem} onPress={() => { triggerLightHaptic(); - router.push(`/food/camera?mealType=${currentMealType}`); + pushIfAuthedElseLogin(`/food/camera?mealType=${currentMealType}`); }} activeOpacity={0.7} > @@ -242,7 +238,7 @@ export function NutritionRadarCard({ style={styles.foodOptionItem} onPress={() => { triggerLightHaptic(); - router.push(`${ROUTES.FOOD_LIBRARY}?mealType=${currentMealType}`); + pushIfAuthedElseLogin(`${ROUTES.FOOD_LIBRARY}?mealType=${currentMealType}`); }} activeOpacity={0.7} > @@ -259,7 +255,7 @@ export function NutritionRadarCard({ style={styles.foodOptionItem} onPress={() => { triggerLightHaptic(); - router.push(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`); + pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`); }} activeOpacity={0.7} > diff --git a/components/statistic/OxygenSaturationCard.tsx b/components/statistic/OxygenSaturationCard.tsx index 57b1028..b619fa2 100644 --- a/components/statistic/OxygenSaturationCard.tsx +++ b/components/statistic/OxygenSaturationCard.tsx @@ -1,32 +1,63 @@ -import React from 'react'; -import { StyleSheet } from 'react-native'; +import React, { useState, useCallback, useRef } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; import HealthDataCard from './HealthDataCard'; +import { fetchOxygenSaturation } from '@/utils/health'; +import dayjs from 'dayjs'; interface OxygenSaturationCardProps { - resetToken: number; style?: object; - oxygenSaturation?: number | null; + selectedDate?: Date; } const OxygenSaturationCard: React.FC = ({ - resetToken, style, - oxygenSaturation + selectedDate }) => { + const [oxygenSaturation, setOxygenSaturation] = useState(null); + const [loading, setLoading] = useState(false); + const loadingRef = useRef(false); + + // 获取血氧饱和度数据 - 在页面聚焦、日期变化时触发 + useFocusEffect( + useCallback(() => { + const loadOxygenSaturationData = async () => { + const dateToUse = selectedDate || new Date(); + + // 防止重复请求 + if (loadingRef.current) return; + + try { + loadingRef.current = true; + setLoading(true); + + const options = { + startDate: dayjs(dateToUse).startOf('day').toDate().toISOString(), + endDate: dayjs(dateToUse).endOf('day').toDate().toISOString() + }; + + const data = await fetchOxygenSaturation(options); + setOxygenSaturation(data); + } catch (error) { + console.error('OxygenSaturationCard: 获取血氧饱和度数据失败:', error); + setOxygenSaturation(null); + } finally { + setLoading(false); + loadingRef.current = false; + } + }; + + loadOxygenSaturationData(); + }, [selectedDate]) + ); + return ( ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); - export default OxygenSaturationCard; \ No newline at end of file diff --git a/components/weight/WeightHistoryCard.tsx b/components/weight/WeightHistoryCard.tsx index e61aebb..8b80057 100644 --- a/components/weight/WeightHistoryCard.tsx +++ b/components/weight/WeightHistoryCard.tsx @@ -43,7 +43,9 @@ export function WeightHistoryCard() { useEffect(() => { - loadWeightHistory(); + if (isLoggedIn) { + loadWeightHistory(); + } }, [userProfile?.weight, isLoggedIn]); const loadWeightHistory = async () => { @@ -67,71 +69,36 @@ export function WeightHistoryCard() { }; - - // 如果没有体重数据,显示引导卡片 - if (!hasWeight) { - return ( - - - - 体重记录 - - - - 开始记录你的体重变化 - - 记录体重变化,追踪你的健康进展 - - { - e.stopPropagation(); - navigateToCoach(); - }} - activeOpacity={0.8} - > - - 记录 - - - - ); - } - // 处理体重历史数据 const sortedHistory = [...weightHistory] .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) .slice(-7); // 只显示最近7条记录 - if (sortedHistory.length === 0) { - return ( - - - 体重记录 - + // return ( + // + // + // 体重记录 + // - - - 暂无体重记录,点击下方按钮开始记录 - - { - e.stopPropagation(); - navigateToCoach(); - }} - activeOpacity={0.8} - > - - 记录体重 - - - - ); - } + // + // + // 暂无体重记录,点击下方按钮开始记录 + // + // { + // e.stopPropagation(); + // navigateToCoach(); + // }} + // activeOpacity={0.8} + // > + // + // 记录体重 + // + // + // + // ); + // } // 生成图表数据 const weights = sortedHistory.map(item => parseFloat(item.weight)); diff --git a/store/healthSlice.ts b/store/healthSlice.ts index 121899a..6208095 100644 --- a/store/healthSlice.ts +++ b/store/healthSlice.ts @@ -15,7 +15,6 @@ export interface HealthData { activeCalories: number | null; basalEnergyBurned: number | null; hrv: number | null; - oxygenSaturation: number | null; heartRate: number | null; activeEnergyBurned: number; activeCaloriesGoal: number; diff --git a/utils/health.ts b/utils/health.ts index 3476003..21ce83c 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -216,8 +216,6 @@ export type TodayHealthData = { exerciseMinutesGoal: number; standHours: number; standHoursGoal: number; - // 新增血氧饱和度和心率数据 - oxygenSaturation: number | null; heartRate: number | null; }; @@ -529,7 +527,7 @@ async function fetchActivitySummary(options: HealthDataOptions): Promise { +export async function fetchOxygenSaturation(options: HealthDataOptions): Promise { try { const result = await HealthKitManager.getOxygenSaturationSamples(options); @@ -618,7 +616,6 @@ function getDefaultHealthData(): TodayHealthData { exerciseMinutesGoal: 30, standHours: 0, standHoursGoal: 12, - oxygenSaturation: null, heartRate: null, }; } @@ -636,14 +633,12 @@ export async function fetchHealthDataForDate(date: Date): Promise