feat(auth): 为未登录用户添加登录引导界面

为目标页面、营养记录、食物添加等功能添加登录状态检查和引导界面,确保用户在未登录状态下能够获得清晰的登录提示和指引。

- 在目标页面添加精美的未登录引导界面,包含渐变背景和登录按钮
- 为食物记录相关组件添加登录状态检查,未登录时自动跳转登录页面
- 重构血氧饱和度卡片为独立数据获取,移除对外部数据依赖
- 移除个人页面的实验性SwiftUI组件,统一使用原生TouchableOpacity
- 清理统计页面和营养记录页面的冗余代码和未使用变量
This commit is contained in:
richarjiang
2025-09-19 15:52:24 +08:00
parent ccfccca7bc
commit 9bcea25a2f
10 changed files with 220 additions and 194 deletions

View File

@@ -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 = [

View File

@@ -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}
>

View File

@@ -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<OxygenSaturationCardProps> = ({
resetToken,
style,
oxygenSaturation
selectedDate
}) => {
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(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 (
<HealthDataCard
title="血氧饱和度"
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'}
value={loading ? '--' : (oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--')}
unit="%"
style={style}
/>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default OxygenSaturationCard;

View File

@@ -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 (
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}>
<Image
source={require('@/assets/images/icons/icon-weight.png')}
style={styles.iconSquare}
/>
<Text style={styles.cardTitle}></Text>
</View>
<View style={styles.emptyContent}>
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyDescription}>
</Text>
<TouchableOpacity
style={styles.recordButton}
onPress={(e) => {
e.stopPropagation();
navigateToCoach();
}}
activeOpacity={0.8}
>
<Ionicons name="add" size={18} color="#192126" />
<Text style={styles.recordButtonText}></Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);
}
// 处理体重历史数据
const sortedHistory = [...weightHistory]
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.slice(-7); // 只显示最近7条记录
if (sortedHistory.length === 0) {
return (
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}></Text>
</View>
// return (
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
// <View style={styles.cardHeader}>
// <Text style={styles.cardTitle}>体重记录</Text>
// </View>
<View style={styles.emptyContent}>
<Text style={styles.emptyDescription}>
</Text>
<TouchableOpacity
style={styles.recordButton}
onPress={(e) => {
e.stopPropagation();
navigateToCoach();
}}
activeOpacity={0.8}
>
<Ionicons name="add" size={18} color="#FFFFFF" />
<Text style={styles.recordButtonText}></Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);
}
// <View style={styles.emptyContent}>
// <Text style={styles.emptyDescription}>
// 暂无体重记录,点击下方按钮开始记录
// </Text>
// <TouchableOpacity
// style={styles.recordButton}
// onPress={(e) => {
// e.stopPropagation();
// navigateToCoach();
// }}
// activeOpacity={0.8}
// >
// <Ionicons name="add" size={18} color="#FFFFFF" />
// <Text style={styles.recordButtonText}>记录体重</Text>
// </TouchableOpacity>
// </View>
// </TouchableOpacity>
// );
// }
// 生成图表数据
const weights = sortedHistory.map(item => parseFloat(item.weight));