207 lines
7.5 KiB
TypeScript
207 lines
7.5 KiB
TypeScript
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|||
|
|
import { View, ScrollView, TouchableOpacity, RefreshControl } from 'react-native';
|
|||
|
|
import { useTheme } from '@/hooks/useTheme';
|
|||
|
|
import { Screen } from '@/components/Screen';
|
|||
|
|
import { ThemedText } from '@/components/ThemedText';
|
|||
|
|
import { ThemedView } from '@/components/ThemedView';
|
|||
|
|
import { FontAwesome6 } from '@expo/vector-icons';
|
|||
|
|
import { useSafeRouter } from '@/hooks/useSafeRouter';
|
|||
|
|
import { createStyles } from './styles';
|
|||
|
|
|
|||
|
|
// 模拟用户ID(实际应用中应该从用户认证系统获取)
|
|||
|
|
const MOCK_USER_ID = 'mock-user-001';
|
|||
|
|
|
|||
|
|
export default function HomeScreen() {
|
|||
|
|
const { theme, isDark } = useTheme();
|
|||
|
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|||
|
|
const router = useSafeRouter();
|
|||
|
|
|
|||
|
|
const [totalCalories, setTotalCalories] = useState(0);
|
|||
|
|
const [targetCalories] = useState(2000);
|
|||
|
|
const [currentWeight, setCurrentWeight] = useState<number | null>(null);
|
|||
|
|
const [targetWeight, setTargetWeight] = useState(65);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
|
|||
|
|
// 获取今日热量和体重数据
|
|||
|
|
const fetchData = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
// 获取今日总热量
|
|||
|
|
/**
|
|||
|
|
* 服务端文件:server/src/routes/food-records.ts
|
|||
|
|
* 接口:GET /api/v1/food-records/total-calories
|
|||
|
|
* Query 参数:userId: string, date?: string
|
|||
|
|
*/
|
|||
|
|
const caloriesRes = await fetch(
|
|||
|
|
`${process.env.EXPO_PUBLIC_BACKEND_BASE_URL}/api/v1/food-records/total-calories?userId=${MOCK_USER_ID}`
|
|||
|
|
);
|
|||
|
|
const caloriesData = await caloriesRes.json();
|
|||
|
|
if (caloriesData.success) {
|
|||
|
|
setTotalCalories(caloriesData.data.totalCalories);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取体重统计
|
|||
|
|
/**
|
|||
|
|
* 服务端文件:server/src/routes/weight-records.ts
|
|||
|
|
* 接口:GET /api/v1/weight-records/stats
|
|||
|
|
* Query 参数:userId: string
|
|||
|
|
*/
|
|||
|
|
const weightRes = await fetch(
|
|||
|
|
`${process.env.EXPO_PUBLIC_BACKEND_BASE_URL}/api/v1/weight-records/stats?userId=${MOCK_USER_ID}`
|
|||
|
|
);
|
|||
|
|
const weightData = await weightRes.json();
|
|||
|
|
if (weightData.success) {
|
|||
|
|
setCurrentWeight(weightData.data.currentWeight);
|
|||
|
|
if (weightData.data.targetWeight) {
|
|||
|
|
setTargetWeight(weightData.data.targetWeight);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch data:', error);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchData();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const caloriePercentage = Math.min((totalCalories / targetCalories) * 100, 100);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Screen backgroundColor={theme.backgroundRoot} statusBarStyle={isDark ? 'light' : 'dark'}>
|
|||
|
|
<ScrollView
|
|||
|
|
contentContainerStyle={styles.scrollContent}
|
|||
|
|
refreshControl={
|
|||
|
|
<RefreshControl refreshing={loading} onRefresh={fetchData} tintColor={theme.primary} />
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
{/* Header */}
|
|||
|
|
<ThemedView level="root" style={styles.header}>
|
|||
|
|
<ThemedText variant="h2" color={theme.textPrimary}>
|
|||
|
|
今日概览
|
|||
|
|
</ThemedText>
|
|||
|
|
<ThemedText variant="small" color={theme.textMuted}>
|
|||
|
|
坚持就是胜利 💪
|
|||
|
|
</ThemedText>
|
|||
|
|
</ThemedView>
|
|||
|
|
|
|||
|
|
{/* 热量卡片 */}
|
|||
|
|
<ThemedView level="default" style={styles.calorieCard}>
|
|||
|
|
<View style={styles.cardHeader}>
|
|||
|
|
<View style={styles.iconContainer}>
|
|||
|
|
<FontAwesome6 name="fire-flame-curved" size={24} color={theme.primary} />
|
|||
|
|
</View>
|
|||
|
|
<ThemedText variant="h4" color={theme.textPrimary}>
|
|||
|
|
今日热量
|
|||
|
|
</ThemedText>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<View style={styles.calorieContent}>
|
|||
|
|
<ThemedText variant="displayLarge" color={theme.primary}>
|
|||
|
|
{totalCalories}
|
|||
|
|
</ThemedText>
|
|||
|
|
<ThemedText variant="small" color={theme.textMuted}>
|
|||
|
|
/ {targetCalories} kcal
|
|||
|
|
</ThemedText>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<View style={styles.progressBar}>
|
|||
|
|
<View style={[styles.progressFill, { width: `${caloriePercentage}%` }]} />
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<ThemedText variant="small" color={theme.textMuted} style={styles.remainingText}>
|
|||
|
|
还可摄入 {Math.max(0, targetCalories - totalCalories)} kcal
|
|||
|
|
</ThemedText>
|
|||
|
|
</ThemedView>
|
|||
|
|
|
|||
|
|
{/* 体重卡片 */}
|
|||
|
|
<ThemedView level="default" style={styles.weightCard}>
|
|||
|
|
<View style={styles.cardHeader}>
|
|||
|
|
<View style={styles.iconContainer}>
|
|||
|
|
<FontAwesome6 name="weight-scale" size={24} color={theme.primary} />
|
|||
|
|
</View>
|
|||
|
|
<ThemedText variant="h4" color={theme.textPrimary}>
|
|||
|
|
当前体重
|
|||
|
|
</ThemedText>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<View style={styles.weightContent}>
|
|||
|
|
<ThemedText variant="displayLarge" color={theme.primary}>
|
|||
|
|
{currentWeight || '--'}
|
|||
|
|
</ThemedText>
|
|||
|
|
<ThemedText variant="small" color={theme.textMuted}>
|
|||
|
|
kg
|
|||
|
|
</ThemedText>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{currentWeight && (
|
|||
|
|
<ThemedText variant="small" color={theme.textSecondary}>
|
|||
|
|
目标体重:{targetWeight} kg
|
|||
|
|
{currentWeight > targetWeight ? ` (还需减 ${(currentWeight - targetWeight).toFixed(1)} kg)` : ' ✨'}
|
|||
|
|
</ThemedText>
|
|||
|
|
)}
|
|||
|
|
</ThemedView>
|
|||
|
|
|
|||
|
|
{/* 快捷操作 */}
|
|||
|
|
<View style={styles.quickActions}>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.actionButton, styles.cameraButton]}
|
|||
|
|
onPress={() => router.push('/record')}
|
|||
|
|
>
|
|||
|
|
<View style={styles.actionIconContainer}>
|
|||
|
|
<FontAwesome6 name="camera" size={28} color={theme.buttonPrimaryText} />
|
|||
|
|
</View>
|
|||
|
|
<ThemedText variant="smallMedium" color={theme.buttonPrimaryText}>
|
|||
|
|
拍照识别
|
|||
|
|
</ThemedText>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.actionButton, styles.chartButton]}
|
|||
|
|
onPress={() => router.push('/stats')}
|
|||
|
|
>
|
|||
|
|
<View style={styles.actionIconContainer}>
|
|||
|
|
<FontAwesome6 name="chart-line" size={28} color={theme.buttonPrimaryText} />
|
|||
|
|
</View>
|
|||
|
|
<ThemedText variant="smallMedium" color={theme.buttonPrimaryText}>
|
|||
|
|
数据统计
|
|||
|
|
</ThemedText>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.actionButton, styles.aiButton]}
|
|||
|
|
onPress={() => router.push('/chat')}
|
|||
|
|
>
|
|||
|
|
<View style={styles.actionIconContainer}>
|
|||
|
|
<FontAwesome6 name="robot" size={28} color={theme.buttonPrimaryText} />
|
|||
|
|
</View>
|
|||
|
|
<ThemedText variant="smallMedium" color={theme.buttonPrimaryText}>
|
|||
|
|
AI 助手
|
|||
|
|
</ThemedText>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 最近记录 */}
|
|||
|
|
<ThemedView level="root" style={styles.recentSection}>
|
|||
|
|
<View style={styles.sectionHeader}>
|
|||
|
|
<ThemedText variant="h4" color={theme.textPrimary}>
|
|||
|
|
最近记录
|
|||
|
|
</ThemedText>
|
|||
|
|
<TouchableOpacity onPress={() => router.push('/record')}>
|
|||
|
|
<ThemedText variant="smallMedium" color={theme.primary}>
|
|||
|
|
查看全部
|
|||
|
|
</ThemedText>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<ThemedText variant="small" color={theme.textMuted} style={styles.emptyText}>
|
|||
|
|
暂无记录,快去拍照识别吧!
|
|||
|
|
</ThemedText>
|
|||
|
|
</ThemedView>
|
|||
|
|
</ScrollView>
|
|||
|
|
</Screen>
|
|||
|
|
);
|
|||
|
|
}
|