feat: 实现减脂体重管理App完整功能
- 实现拍照识别食物功能(集成大语言模型视觉能力) - 实现智能对话功能(集成大语言模型流式输出) - 实现食物记录和卡路里管理功能 - 实现体重记录和统计功能 - 实现健康数据管理页面 - 配置数据库表结构(用户、食物记录、体重记录) - 实现Express后端API路由 - 配置Tab导航和前端页面 - 采用健康运动配色方案
This commit is contained in:
206
client/screens/home/index.tsx
Normal file
206
client/screens/home/index.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user