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

207 lines
7.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}