import React, { useState, useMemo } from 'react'; import { View, ScrollView, TouchableOpacity, Modal, TextInput, Alert, Image } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; 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 { createFormDataFile } from '@/utils'; import { createStyles } from './styles'; const MOCK_USER_ID = 'mock-user-001'; type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack'; interface RecognizedFood { foodName: string; weight: number; calories: number; imageUrl: string; imageKey: string; } export default function RecordScreen() { const { theme, isDark } = useTheme(); const styles = useMemo(() => createStyles(theme), [theme]); const [modalVisible, setModalVisible] = useState(false); const [recognizedFood, setRecognizedFood] = useState(null); const [manualFood, setManualFood] = useState({ name: '', calories: '', weight: '', mealType: 'breakfast' as MealType, }); const [recognizing, setRecognizing] = useState(false); const [saving, setSaving] = useState(false); const [imageUri, setImageUri] = useState(null); // 请求相机权限 const requestCameraPermission = async () => { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== 'granted') { Alert.alert('权限提示', '需要相机权限才能拍照识别食物'); return false; } return true; }; // 拍照 const takePicture = async () => { const hasPermission = await requestCameraPermission(); if (!hasPermission) return; try { const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], allowsEditing: false, quality: 0.8, }); if (!result.canceled && result.assets[0]) { setImageUri(result.assets[0].uri); await recognizeFood(result.assets[0].uri); } } catch (error) { console.error('Camera error:', error); Alert.alert('错误', '拍照失败,请重试'); } }; // 从相册选择 const pickImage = async () => { try { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: false, quality: 0.8, }); if (!result.canceled && result.assets[0]) { setImageUri(result.assets[0].uri); await recognizeFood(result.assets[0].uri); } } catch (error) { console.error('Image picker error:', error); Alert.alert('错误', '选择图片失败,请重试'); } }; // 识别食物 const recognizeFood = async (uri: string) => { setRecognizing(true); try { const formData = new FormData(); const file = await createFormDataFile(uri, 'food_photo.jpg', 'image/jpeg'); formData.append('image', file as any); /** * 服务端文件:server/src/routes/food-records.ts * 接口:POST /api/v1/food-records/recognize * Body 参数:image: File (FormData) */ const response = await fetch( `${process.env.EXPO_PUBLIC_BACKEND_BASE_URL}/api/v1/food-records/recognize`, { method: 'POST', body: formData, } ); const data = await response.json(); if (data.success) { setRecognizedFood(data.data); setManualFood({ name: data.data.foodName, calories: data.data.calories.toString(), weight: data.data.weight.toString(), mealType: 'breakfast', }); setModalVisible(true); } else { Alert.alert('识别失败', data.error || '无法识别食物,请重试'); } } catch (error) { console.error('Recognition error:', error); Alert.alert('错误', '识别失败,请检查网络连接'); } finally { setRecognizing(false); } }; // 保存记录 const saveRecord = async () => { if (!manualFood.name || !manualFood.calories || !manualFood.weight) { Alert.alert('提示', '请填写完整的食物信息'); return; } setSaving(true); try { const recordData = { userId: MOCK_USER_ID, foodName: manualFood.name, calories: parseInt(manualFood.calories), weight: parseFloat(manualFood.weight), mealType: manualFood.mealType, recordedAt: new Date().toISOString(), imageUrl: recognizedFood?.imageUrl, }; /** * 服务端文件:server/src/routes/food-records.ts * 接口:POST /api/v1/food-records * Body 参数:userId: string, foodName: string, calories: number, weight: number, mealType: string, recordedAt: string, imageUrl?: string */ const response = await fetch( `${process.env.EXPO_PUBLIC_BACKEND_BASE_URL}/api/v1/food-records`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(recordData), } ); const data = await response.json(); if (data.success) { Alert.alert('成功', '记录已保存', [ { text: '确定', onPress: () => resetForm() }, ]); } else { Alert.alert('失败', data.error || '保存失败,请重试'); } } catch (error) { console.error('Save error:', error); Alert.alert('错误', '保存失败,请检查网络连接'); } finally { setSaving(false); } }; const resetForm = () => { setModalVisible(false); setRecognizedFood(null); setImageUri(null); setManualFood({ name: '', calories: '', weight: '', mealType: 'breakfast', }); }; const mealTypes: { key: MealType; label: string }[] = [ { key: 'breakfast', label: '早餐' }, { key: 'lunch', label: '午餐' }, { key: 'dinner', label: '晚餐' }, { key: 'snack', label: '加餐' }, ]; return ( 记录食物 {/* 识别方式 */} {recognizing ? '识别中...' : '拍照识别'} {recognizing ? '识别中...' : '相册选择'} {/* 手动添加 */} 手动添加 食物名称 setManualFood({ ...manualFood, name: text })} /> 热量 (kcal) setManualFood({ ...manualFood, calories: text })} keyboardType="numeric" /> 重量 (g) setManualFood({ ...manualFood, weight: text })} keyboardType="numeric" /> 餐次 {mealTypes.map((type) => ( setManualFood({ ...manualFood, mealType: type.key })} > {type.label} ))} {saving ? '保存中...' : '保存记录'} {/* 识别结果 Modal */} {recognizedFood ? '识别结果' : '确认信息'} {imageUri && ( )} 食物名称 setManualFood({ ...manualFood, name: text })} /> 热量 setManualFood({ ...manualFood, calories: text })} keyboardType="numeric" /> 重量 (g) setManualFood({ ...manualFood, weight: text })} keyboardType="numeric" /> 取消 {saving ? '保存中...' : '确认保存'} ); }