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

@@ -109,6 +109,8 @@ export default function GoalsScreen() {
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
try { try {
if (!isLoggedIn) return
await loadTasks(); await loadTasks();
} finally { } finally {
setRefreshing(false); setRefreshing(false);
@@ -117,6 +119,8 @@ export default function GoalsScreen() {
// 加载更多任务 // 加载更多任务
const handleLoadMoreTasks = async () => { const handleLoadMoreTasks = async () => {
if (!isLoggedIn) return
if (tasksPagination.hasMore && !tasksLoading) { if (tasksPagination.hasMore && !tasksLoading) {
try { try {
await dispatch(loadMoreTasks()).unwrap(); await dispatch(loadMoreTasks()).unwrap();
@@ -319,6 +323,61 @@ export default function GoalsScreen() {
// 渲染空状态 // 渲染空状态
const renderEmptyState = () => { const renderEmptyState = () => {
// 未登录状态下的引导
if (!isLoggedIn) {
return (
<View style={styles.emptyStateLogin}>
<LinearGradient
colors={['#F0F9FF', '#FEFEFE', '#F0F9FF']}
style={styles.emptyStateLoginBackground}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
<View style={styles.emptyStateLoginContent}>
{/* 清新的图标设计 */}
<View style={styles.emptyStateLoginIconContainer}>
<LinearGradient
colors={[colorTokens.primary, '#9B8AFB']}
style={styles.emptyStateLoginIconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<MaterialIcons name="person-outline" size={32} color="#FFFFFF" />
</LinearGradient>
</View>
{/* 主标题 */}
<Text style={[styles.emptyStateLoginTitle, { color: colorTokens.text }]}>
</Text>
{/* 副标题 */}
<Text style={[styles.emptyStateLoginSubtitle, { color: colorTokens.textSecondary }]}>
</Text>
{/* 登录按钮 */}
<TouchableOpacity
style={[styles.emptyStateLoginButton, { backgroundColor: colorTokens.primary }]}
onPress={() => pushIfAuthedElseLogin('/goals')}
>
<LinearGradient
colors={[colorTokens.primary, '#9B8AFB']}
style={styles.emptyStateLoginButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={styles.emptyStateLoginButtonText}></Text>
<MaterialIcons name="arrow-forward" size={18} color="#FFFFFF" />
</LinearGradient>
</TouchableOpacity>
</View>
</View>
);
}
// 已登录但无任务的状态
let title = '暂无任务'; let title = '暂无任务';
let subtitle = '创建目标后,系统会自动生成相应的任务'; let subtitle = '创建目标后,系统会自动生成相应的任务';
@@ -710,6 +769,80 @@ const styles = StyleSheet.create({
textAlign: 'center', textAlign: 'center',
lineHeight: 20, 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: { loadMoreContainer: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 20, paddingVertical: 20,

View File

@@ -9,8 +9,6 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto
import { getItem, setItem } from '@/utils/kvStore'; import { getItem, setItem } from '@/utils/kvStore';
import { log } from '@/utils/logger'; import { log } from '@/utils/logger';
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences'; 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 { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { isLiquidGlassAvailable } from 'expo-glass-effect'; import { isLiquidGlassAvailable } from 'expo-glass-effect';
@@ -215,32 +213,9 @@ export default function PersonalScreen() {
<Text style={styles.userName}>{displayName}</Text> <Text style={styles.userName}>{displayName}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{isLgAvaliable ? <Host style={{ <TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
marginRight: 18,
}}>
<Button
variant='default'
onPress={() => {
pushIfAuthedElseLogin('/profile/edit')
}}
modifiers={[
frame({
width: 60,
height: 30,
}),
glassEffect({
glass: {
variant: 'regular',
interactive: true
}
})
]} >
<SwiftText size={14} color='black' weight={'medium'}>{isLoggedIn ? '编辑' : '登录'}</SwiftText>
</Button>
</Host> : <TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={styles.editButtonText}>{isLoggedIn ? '编辑' : '登录'}</Text> <Text style={styles.editButtonText}>{isLoggedIn ? '编辑' : '登录'}</Text>
</TouchableOpacity>} </TouchableOpacity>
</View> </View>

View File

@@ -20,7 +20,6 @@ import { fetchTodayWaterStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health'; import { fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData'; import { getTestHealthData } from '@/utils/mockHealthData';
import { calculateNutritionGoals } from '@/utils/nutrition';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
@@ -92,8 +91,6 @@ export default function ExploreScreen() {
const activeCalories = useMockData ? (mockData?.activeEnergyBurned ?? null) : (healthData?.activeEnergyBurned ?? null); const activeCalories = useMockData ? (mockData?.activeEnergyBurned ?? null) : (healthData?.activeEnergyBurned ?? null);
const basalMetabolism: number | null = useMockData ? (mockData?.basalEnergyBurned ?? null) : (healthData?.basalEnergyBurned ?? null); const basalMetabolism: number | null = useMockData ? (mockData?.basalEnergyBurned ?? null) : (healthData?.basalEnergyBurned ?? null);
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
// 用于触发动画重置的 token当日期或数据变化时更新 // 用于触发动画重置的 token当日期或数据变化时更新
@@ -102,15 +99,6 @@ export default function ExploreScreen() {
// 从 Redux 获取营养数据 // 从 Redux 获取营养数据
const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString)); 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(); const dispatch = useAppDispatch();
@@ -226,13 +214,6 @@ export default function ExploreScreen() {
loadingRef.current.health = true; loadingRef.current.health = true;
console.log('=== 开始HealthKit初始化流程 ==='); console.log('=== 开始HealthKit初始化流程 ===');
// const ok = await ensureHealthPermissions();
// if (!ok) {
// const errorMsg = '无法获取健康权限请确保在真实iOS设备上运行并授权应用访问健康数据';
// console.warn(errorMsg);
// return;
// }
latestRequestKeyRef.current = requestKey; latestRequestKeyRef.current = requestKey;
console.log('权限获取成功,开始获取健康数据...', derivedDate); console.log('权限获取成功,开始获取健康数据...', derivedDate);
@@ -251,7 +232,6 @@ export default function ExploreScreen() {
activeCalories: data.activeEnergyBurned, activeCalories: data.activeEnergyBurned,
basalEnergyBurned: data.basalEnergyBurned, basalEnergyBurned: data.basalEnergyBurned,
hrv: data.hrv, hrv: data.hrv,
oxygenSaturation: data.oxygenSaturation,
heartRate: data.heartRate, heartRate: data.heartRate,
activeEnergyBurned: data.activeEnergyBurned, activeEnergyBurned: data.activeEnergyBurned,
activeCaloriesGoal: data.activeCaloriesGoal, activeCaloriesGoal: data.activeCaloriesGoal,
@@ -358,14 +338,6 @@ export default function ExploreScreen() {
loadAllData(currentSelectedDate); loadAllData(currentSelectedDate);
}, []) }, [])
// 页面聚焦时的数据加载逻辑
// useFocusEffect(
// React.useCallback(() => {
// // 页面聚焦时加载数据,使用缓存机制避免频繁请求
// console.log('页面聚焦,检查是否需要刷新数据...');
// loadAllData(currentSelectedDate);
// }, [loadAllData, currentSelectedDate])
// );
// AppState 监听:应用从后台返回前台时的处理 // AppState 监听:应用从后台返回前台时的处理
useEffect(() => { useEffect(() => {
@@ -487,16 +459,10 @@ export default function ExploreScreen() {
{/* 营养摄入雷达图卡片 */} {/* 营养摄入雷达图卡片 */}
<NutritionRadarCard <NutritionRadarCard
nutritionSummary={nutritionSummary} nutritionSummary={nutritionSummary}
nutritionGoals={nutritionGoals}
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)} burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
basalMetabolism={basalMetabolism || 0} basalMetabolism={basalMetabolism || 0}
activeCalories={activeCalories || 0} activeCalories={activeCalories || 0}
resetToken={animToken} resetToken={animToken}
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
console.log('选择餐次:', mealType);
// 这里可以导航到营养记录页面
pushIfAuthedElseLogin('/nutrition/records');
}}
/> />
<WeightHistoryCard /> <WeightHistoryCard />
@@ -575,9 +541,7 @@ export default function ExploreScreen() {
{/* 血氧饱和度卡片 */} {/* 血氧饱和度卡片 */}
<FloatingCard style={styles.masonryCard} delay={1750}> <FloatingCard style={styles.masonryCard} delay={1750}>
<OxygenSaturationCard <OxygenSaturationCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride} style={styles.basalMetabolismCardOverride}
oxygenSaturation={oxygenSaturation}
/> />
</FloatingCard> </FloatingCard>

View File

@@ -300,42 +300,6 @@ export default function NutritionRecordsScreen() {
}); });
}; };
// 渲染视图模式切换器
const renderViewModeToggle = () => (
<View style={[styles.viewModeContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<Text style={[styles.monthTitle, { color: colorTokens.text }]}>{monthTitle}</Text>
<View style={[styles.toggleContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<TouchableOpacity
style={[
styles.toggleButton,
viewMode === 'daily' && { backgroundColor: colorTokens.primary }
]}
onPress={() => setViewMode('daily')}
>
<Text style={[
styles.toggleText,
{ color: viewMode === 'daily' ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.toggleButton,
viewMode === 'all' && { backgroundColor: colorTokens.primary }
]}
onPress={() => setViewMode('all')}
>
<Text style={[
styles.toggleText,
{ color: viewMode === 'all' ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
</Text>
</TouchableOpacity>
</View>
</View>
);
// 渲染日期选择器(仅在按天查看模式下显示) // 渲染日期选择器(仅在按天查看模式下显示)
const renderDateSelector = () => { const renderDateSelector = () => {

View File

@@ -1,4 +1,5 @@
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
@@ -20,19 +21,21 @@ interface FloatingFoodOverlayProps {
export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) { export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) {
const router = useRouter(); const router = useRouter();
const { pushIfAuthedElseLogin } = useAuthGuard()
const handleFoodLibrary = () => { const handleFoodLibrary = () => {
onClose(); onClose();
router.push(`${ROUTES.FOOD_LIBRARY}?mealType=${mealType}`); pushIfAuthedElseLogin(`${ROUTES.FOOD_LIBRARY}?mealType=${mealType}`);
}; };
const handlePhotoRecognition = () => { const handlePhotoRecognition = () => {
onClose(); onClose();
router.push(`/food/camera?mealType=${mealType}`); pushIfAuthedElseLogin(`/food/camera?mealType=${mealType}`);
}; };
const handleVoiceRecord = () => { const handleVoiceRecord = () => {
onClose(); onClose();
router.push(`${ROUTES.VOICE_RECORD}?mealType=${mealType}`); pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${mealType}`);
}; };
const menuItems = [ const menuItems = [

View File

@@ -1,8 +1,9 @@
import { AnimatedNumber } from '@/components/AnimatedNumber'; import { AnimatedNumber } from '@/components/AnimatedNumber';
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { NutritionSummary } from '@/services/dietRecords'; import { NutritionSummary } from '@/services/dietRecords';
import { triggerLightHaptic } from '@/utils/haptics'; import { triggerLightHaptic } from '@/utils/haptics';
import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition'; import { calculateRemainingCalories } from '@/utils/nutrition';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { router } from 'expo-router'; import { router } from 'expo-router';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
@@ -14,8 +15,6 @@ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type NutritionRadarCardProps = { export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null; nutritionSummary: NutritionSummary | null;
/** 营养目标 */
nutritionGoals?: NutritionGoals;
/** 基础代谢消耗的卡路里 */ /** 基础代谢消耗的卡路里 */
burnedCalories?: number; burnedCalories?: number;
/** 基础代谢率 */ /** 基础代谢率 */
@@ -25,8 +24,6 @@ export type NutritionRadarCardProps = {
/** 动画重置令牌 */ /** 动画重置令牌 */
resetToken?: number; resetToken?: number;
/** 餐次点击回调 */
onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void;
}; };
// 简化的圆环进度组件 // 简化的圆环进度组件
@@ -97,16 +94,15 @@ const SimpleRingProgress = ({
export function NutritionRadarCard({ export function NutritionRadarCard({
nutritionSummary, nutritionSummary,
nutritionGoals,
burnedCalories = 1618, burnedCalories = 1618,
basalMetabolism, basalMetabolism,
activeCalories, activeCalories,
resetToken, resetToken,
onMealPress
}: NutritionRadarCardProps) { }: NutritionRadarCardProps) {
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
const { pushIfAuthedElseLogin } = useAuthGuard()
const nutritionStats = useMemo(() => { const nutritionStats = useMemo(() => {
return [ return [
{ label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' }, { label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' },
@@ -225,7 +221,7 @@ export function NutritionRadarCard({
style={styles.foodOptionItem} style={styles.foodOptionItem}
onPress={() => { onPress={() => {
triggerLightHaptic(); triggerLightHaptic();
router.push(`/food/camera?mealType=${currentMealType}`); pushIfAuthedElseLogin(`/food/camera?mealType=${currentMealType}`);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >
@@ -242,7 +238,7 @@ export function NutritionRadarCard({
style={styles.foodOptionItem} style={styles.foodOptionItem}
onPress={() => { onPress={() => {
triggerLightHaptic(); triggerLightHaptic();
router.push(`${ROUTES.FOOD_LIBRARY}?mealType=${currentMealType}`); pushIfAuthedElseLogin(`${ROUTES.FOOD_LIBRARY}?mealType=${currentMealType}`);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >
@@ -259,7 +255,7 @@ export function NutritionRadarCard({
style={styles.foodOptionItem} style={styles.foodOptionItem}
onPress={() => { onPress={() => {
triggerLightHaptic(); triggerLightHaptic();
router.push(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`); pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >

View File

@@ -1,32 +1,63 @@
import React from 'react'; import React, { useState, useCallback, useRef } from 'react';
import { StyleSheet } from 'react-native'; import { useFocusEffect } from '@react-navigation/native';
import HealthDataCard from './HealthDataCard'; import HealthDataCard from './HealthDataCard';
import { fetchOxygenSaturation } from '@/utils/health';
import dayjs from 'dayjs';
interface OxygenSaturationCardProps { interface OxygenSaturationCardProps {
resetToken: number;
style?: object; style?: object;
oxygenSaturation?: number | null; selectedDate?: Date;
} }
const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
resetToken,
style, 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 ( return (
<HealthDataCard <HealthDataCard
title="血氧饱和度" title="血氧饱和度"
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'} value={loading ? '--' : (oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--')}
unit="%" unit="%"
style={style} style={style}
/> />
); );
}; };
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default OxygenSaturationCard; export default OxygenSaturationCard;

View File

@@ -43,7 +43,9 @@ export function WeightHistoryCard() {
useEffect(() => { useEffect(() => {
if (isLoggedIn) {
loadWeightHistory(); loadWeightHistory();
}
}, [userProfile?.weight, isLoggedIn]); }, [userProfile?.weight, isLoggedIn]);
const loadWeightHistory = async () => { 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] const sortedHistory = [...weightHistory]
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.slice(-7); // 只显示最近7条记录 .slice(-7); // 只显示最近7条记录
if (sortedHistory.length === 0) { // return (
return ( // <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}> // <View style={styles.cardHeader}>
<View style={styles.cardHeader}> // <Text style={styles.cardTitle}>体重记录</Text>
<Text style={styles.cardTitle}></Text> // </View>
</View>
<View style={styles.emptyContent}> // <View style={styles.emptyContent}>
<Text style={styles.emptyDescription}> // <Text style={styles.emptyDescription}>
// 暂无体重记录,点击下方按钮开始记录
</Text> // </Text>
<TouchableOpacity // <TouchableOpacity
style={styles.recordButton} // style={styles.recordButton}
onPress={(e) => { // onPress={(e) => {
e.stopPropagation(); // e.stopPropagation();
navigateToCoach(); // navigateToCoach();
}} // }}
activeOpacity={0.8} // activeOpacity={0.8}
> // >
<Ionicons name="add" size={18} color="#FFFFFF" /> // <Ionicons name="add" size={18} color="#FFFFFF" />
<Text style={styles.recordButtonText}></Text> // <Text style={styles.recordButtonText}>记录体重</Text>
</TouchableOpacity> // </TouchableOpacity>
</View> // </View>
</TouchableOpacity> // </TouchableOpacity>
); // );
} // }
// 生成图表数据 // 生成图表数据
const weights = sortedHistory.map(item => parseFloat(item.weight)); const weights = sortedHistory.map(item => parseFloat(item.weight));

View File

@@ -15,7 +15,6 @@ export interface HealthData {
activeCalories: number | null; activeCalories: number | null;
basalEnergyBurned: number | null; basalEnergyBurned: number | null;
hrv: number | null; hrv: number | null;
oxygenSaturation: number | null;
heartRate: number | null; heartRate: number | null;
activeEnergyBurned: number; activeEnergyBurned: number;
activeCaloriesGoal: number; activeCaloriesGoal: number;

View File

@@ -216,8 +216,6 @@ export type TodayHealthData = {
exerciseMinutesGoal: number; exerciseMinutesGoal: number;
standHours: number; standHours: number;
standHoursGoal: number; standHoursGoal: number;
// 新增血氧饱和度和心率数据
oxygenSaturation: number | null;
heartRate: number | null; heartRate: number | null;
}; };
@@ -529,7 +527,7 @@ async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthA
} }
} }
async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> { export async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> {
try { try {
const result = await HealthKitManager.getOxygenSaturationSamples(options); const result = await HealthKitManager.getOxygenSaturationSamples(options);
@@ -618,7 +616,6 @@ function getDefaultHealthData(): TodayHealthData {
exerciseMinutesGoal: 30, exerciseMinutesGoal: 30,
standHours: 0, standHours: 0,
standHoursGoal: 12, standHoursGoal: 12,
oxygenSaturation: null,
heartRate: null, heartRate: null,
}; };
} }
@@ -636,14 +633,12 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
basalEnergyBurned, basalEnergyBurned,
hrv, hrv,
activitySummary, activitySummary,
oxygenSaturation,
heartRate heartRate
] = await Promise.all([ ] = await Promise.all([
fetchActiveEnergyBurned(options), fetchActiveEnergyBurned(options),
fetchBasalEnergyBurned(options), fetchBasalEnergyBurned(options),
fetchHeartRateVariability(options), fetchHeartRateVariability(options),
fetchActivitySummary(options), fetchActivitySummary(options),
fetchOxygenSaturation(options),
fetchHeartRate(options) fetchHeartRate(options)
]); ]);
@@ -657,7 +652,6 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30), exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30),
standHours: Math.round(activitySummary?.appleStandHours || 0), standHours: Math.round(activitySummary?.appleStandHours || 0),
standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12), standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12),
oxygenSaturation,
heartRate heartRate
}; };
} catch (error) { } catch (error) {