feat: 实现减脂体重管理App完整功能

- 实现拍照识别食物功能(集成大语言模型视觉能力)
- 实现智能对话功能(集成大语言模型流式输出)
- 实现食物记录和卡路里管理功能
- 实现体重记录和统计功能
- 实现健康数据管理页面
- 配置数据库表结构(用户、食物记录、体重记录)
- 实现Express后端API路由
- 配置Tab导航和前端页面
- 采用健康运动配色方案
This commit is contained in:
jaystar
2026-02-02 15:17:50 +08:00
commit 28c4d7b3b4
82 changed files with 21891 additions and 0 deletions

View 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>
);
}