- 实现拍照识别食物功能(集成大语言模型视觉能力) - 实现智能对话功能(集成大语言模型流式输出) - 实现食物记录和卡路里管理功能 - 实现体重记录和统计功能 - 实现健康数据管理页面 - 配置数据库表结构(用户、食物记录、体重记录) - 实现Express后端API路由 - 配置Tab导航和前端页面 - 采用健康运动配色方案
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>
|
||
);
|
||
}
|