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