import { CircularRing } from '@/components/CircularRing'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { ROUTES } from '@/constants/Routes'; import { useAppSelector } from '@/hooks/redux'; import { addDietRecord, type CreateDietRecordDto, type MealType } from '@/services/dietRecords'; import { selectFoodRecognitionResult } from '@/store/foodRecognitionSlice'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, BackHandler, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import ImageViewing from 'react-native-image-viewing'; // 模拟食物摄入列表数据 const mockFoodItems = [ { id: '1', name: '每日豆奶', emoji: '🥛', amount: 190, unit: 'g', calories: 80, protein: 4.6, carbohydrate: 4.0, fat: 4.2, }, { id: '2', name: '全麦面包', emoji: '🍞', amount: 50, unit: 'g', calories: 120, protein: 4.0, carbohydrate: 22.0, fat: 2.0, }, { id: '3', name: '香蕉', emoji: '🍌', amount: 100, unit: 'g', calories: 89, protein: 1.1, carbohydrate: 23.0, fat: 0.3, } ]; // 餐次映射 const MEAL_TYPE_MAP = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }; export default function FoodAnalysisResultScreen() { const router = useRouter(); const params = useLocalSearchParams<{ imageUri?: string; mealType?: string; recognitionId?: string; hideRecordBar?: string; }>(); const [foodItems, setFoodItems] = useState(mockFoodItems); const [currentMealType, setCurrentMealType] = useState((params.mealType as MealType) || 'breakfast'); const [showMealSelector, setShowMealSelector] = useState(false); const [isRecording, setIsRecording] = useState(false); const [showImagePreview, setShowImagePreview] = useState(false); const [animationTrigger, setAnimationTrigger] = useState(0); const [editingFood, setEditingFood] = useState(null); const [editFormData, setEditFormData] = useState({ name: '', amount: '', calories: '' }); const { imageUri, recognitionId, hideRecordBar } = params; // 判断是否隐藏记录栏(默认显示) const shouldHideRecordBar = hideRecordBar === 'true'; // 从 Redux 获取识别结果 const recognitionResult = useAppSelector(recognitionId ? selectFoodRecognitionResult(recognitionId) : () => null); // 处理Android返回键关闭图片预览 useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { if (showImagePreview) { setShowImagePreview(false); return true; // 阻止默认返回行为 } return false; }); return () => backHandler.remove(); }, [showImagePreview]); // 处理识别结果数据 useEffect(() => { if (recognitionResult) { // 检查是否识别到食物 if (!recognitionResult.isFoodDetected) { // 非食物检测情况,清空食物列表 setFoodItems([]); return; } // 将识别结果转换为食物项格式 const convertedItems = recognitionResult.items.map((item, index) => ({ id: item.id || `item-${index}`, name: item.foodName, emoji: getRandomFoodEmoji(), // 使用随机emoji amount: parseInt(item.portion.match(/\d+/)?.[0] || '100'), unit: item.portion.includes('g') ? 'g' : item.portion.includes('ml') ? 'ml' : 'g', calories: item.calories, protein: item.nutritionData.proteinGrams || 0, carbohydrate: item.nutritionData.carbohydrateGrams || 0, fat: item.nutritionData.fatGrams || 0, })); setFoodItems(convertedItems); // 如果识别结果中有餐次信息,更新餐次 if (recognitionResult.items.length > 0 && recognitionResult.items[0].mealType) { const mealTypeFromApi = recognitionResult.items[0].mealType as MealType; if (['breakfast', 'lunch', 'dinner', 'snack'].includes(mealTypeFromApi)) { setCurrentMealType(mealTypeFromApi); } } // 触发营养圆环动画 setAnimationTrigger(prev => prev + 1); } }, [recognitionResult]); // 当食物项发生变化时也触发动画 useEffect(() => { if (foodItems.length > 0) { setAnimationTrigger(prev => prev + 1); } }, [foodItems]); const handleSaveToDiary = async () => { if (isRecording || foodItems.length === 0) return; setIsRecording(true); try { // 逐个记录所有食物 for (const item of foodItems) { const dietRecordData: CreateDietRecordDto = { mealType: currentMealType, foodName: item.name, foodDescription: `${item.amount}${item.unit}`, portionDescription: `${item.amount}${item.unit}`, estimatedCalories: item.calories, proteinGrams: item.protein, carbohydrateGrams: item.carbohydrate, fatGrams: item.fat, source: 'vision', mealTime: new Date().toISOString(), imageUrl: imageUri, }; await addDietRecord(dietRecordData); } router.replace(ROUTES.TAB_STATISTICS) } catch (error) { console.error('记录饮食失败:', error); } finally { setIsRecording(false); } }; // 计算所有食物的总营养数据 const totalCalories = foodItems.reduce((sum, item) => sum + item.calories, 0); const totalProtein = foodItems.reduce((sum, item) => sum + item.protein, 0); const totalCarbohydrate = foodItems.reduce((sum, item) => sum + item.carbohydrate, 0); const totalFat = foodItems.reduce((sum, item) => sum + item.fat, 0); // 计算营养比例(基于推荐日摄入量) const proteinPercentage = Math.round((totalProtein / 50) * 100); // 推荐50g const fatPercentage = Math.round((totalFat / 65) * 100); // 推荐65g const carbohydratePercentage = Math.round((totalCarbohydrate / 300) * 100); // 推荐300g // 删除食物项 const handleRemoveFood = (itemId: string) => { setFoodItems(prev => prev.filter(item => item.id !== itemId)); }; // 打开编辑弹窗 const handleEditFood = (item: typeof mockFoodItems[0]) => { setEditFormData({ name: item.name, amount: item.amount.toString(), calories: item.calories.toString(), }); setEditingFood(item.id); }; // 保存编辑 const handleSaveEdit = () => { if (!editingFood) return; const amount = parseFloat(editFormData.amount) || 0; const calories = parseFloat(editFormData.calories) || 0; setFoodItems(prev => prev.map(item => item.id === editingFood ? { ...item, name: editFormData.name, amount, calories } : item )); setEditingFood(null); setEditFormData({ name: '', amount: '', calories: '' }); }; // 关闭编辑弹窗 const handleCloseEdit = () => { setEditingFood(null); setEditFormData({ name: '', amount: '', calories: '' }); }; // 获取随机食物emoji function getRandomFoodEmoji(): string { const foodEmojis = ['🍎', '🍌', '🍞', '🥛', '🥗', '🍗', '🍖', '🥕', '🥦', '🥬', '🍅', '🥒', '🍇', '🥝', '🍓']; return foodEmojis[Math.floor(Math.random() * foodEmojis.length)]; } // 处理餐次选择 const handleMealTypeSelect = (selectedMealType: MealType) => { setCurrentMealType(selectedMealType); setShowMealSelector(false); }; // 餐次选择选项 const mealOptions = [ { key: 'breakfast' as const, label: '早餐', color: '#FF6B35' }, { key: 'lunch' as const, label: '午餐', color: '#4CAF50' }, { key: 'dinner' as const, label: '晚餐', color: '#2196F3' }, { key: 'snack' as const, label: '加餐', color: '#FF9800' }, ]; if (!imageUri && !recognitionResult) { return ( router.back()} /> 未找到图片或识别结果 ); } return ( {/* 背景渐变 */} router.back()} transparent={true} /> {/* 食物主图 */} {imageUri ? ( setShowImagePreview(true)} activeOpacity={0.9} > {/* 预览提示图标 */} ) : ( 营养记录 )} {/* 识别信息气泡 */} {recognitionResult ? `置信度: ${recognitionResult.confidence}%` : dayjs().format('YYYY年M月D日') } {/* 营养信息卡片 */} {/* 卡路里 */} {totalCalories} 千卡 {/* 营养圆环图 */} {/* 食物摄入部分 */} {recognitionResult ? '识别结果' : '食物摄入'} {recognitionResult && recognitionResult.analysisText && ( {recognitionResult.analysisText} )} {/* 非食物检测提示 */} {recognitionResult && !recognitionResult.isFoodDetected && ( 未识别到食物 {recognitionResult.nonFoodMessage || recognitionResult.analysisText} 建议: • 确保图片中包含食物 • 尝试更清晰的照片角度 • 避免过度模糊或光线不足 )} {/* 食物列表 */} {foodItems.length > 0 && foodItems.map((item, index) => ( 0 && { marginTop: 8 }]}> {item.emoji} {item.name} {item.amount}{item.unit} {item.calories}千卡 {shouldHideRecordBar ? null : handleEditFood(item)} > } {shouldHideRecordBar ? null : handleRemoveFood(item.id)} > } ))} {/* 底部餐次选择和记录按钮 */} {!shouldHideRecordBar && ( recognitionResult && !recognitionResult.isFoodDetected ? ( // 非食物检测情况显示重新拍照按钮 router.back()} activeOpacity={0.8} > 重新拍照 ) : ( // 正常食物识别情况 setShowMealSelector(true)} > option.key === currentMealType)?.color || '#FF6B35' } ]} /> {MEAL_TYPE_MAP[currentMealType as keyof typeof MEAL_TYPE_MAP]} {isRecording ? ( ) : ( 记录 )} ) )} {/* 餐次选择弹窗 */} setShowMealSelector(false)} > setShowMealSelector(false)} /> 选择餐次 setShowMealSelector(false)}> {mealOptions.map((option) => ( handleMealTypeSelect(option.key)} > {option.label} {currentMealType === option.key && ( )} ))} {/* 图片预览 */} { console.log('ImageViewing onRequestClose called'); setShowImagePreview(false); }} swipeToCloseEnabled={true} doubleTapToZoomEnabled={true} HeaderComponent={() => ( {recognitionResult ? `置信度: ${recognitionResult.confidence}%` : dayjs().format('YYYY年M月D日 HH:mm') } )} FooterComponent={() => ( setShowImagePreview(false)} > 关闭 )} /> {/* 编辑食物弹窗 */} ); } // 编辑食物弹窗组件 function FoodEditModal({ visible, formData, onFormDataChange, onClose, onSave }: { visible: boolean; formData: { name: string; amount: string; calories: string }; onFormDataChange: (data: { name: string; amount: string; calories: string }) => void; onClose: () => void; onSave: () => void; }) { const handleFieldChange = (field: string, value: string) => { onFormDataChange({ ...formData, [field]: value }); }; return ( 编辑食物信息 {/* 食物名称 */} 食物名称 handleFieldChange('name', value)} autoFocus /> {/* 重量/数量 */} 重量 (克) handleFieldChange('amount', value)} keyboardType="numeric" /> {/* 卡路里 */} 卡路里 (千卡) handleFieldChange('calories', value)} keyboardType="numeric" /> {/* 按钮区域 */} 取消 保存 ); } // 营养圆环组件 function NutritionRing({ label, value, unit, percentage, color, resetToken }: { label: string; value: string | number; unit?: string; percentage: number; color: string; resetToken?: unknown; }) { return ( {percentage}% {label} {value}{unit} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5e5fbff', }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, decorativeCircle1: { position: 'absolute', top: 120, right: 30, width: 80, height: 80, borderRadius: 40, backgroundColor: '#0EA5E9', opacity: 0.08, }, decorativeCircle2: { position: 'absolute', top: 250, left: -20, width: 60, height: 60, borderRadius: 30, backgroundColor: '#8B74F3', opacity: 0.06, }, decorativeCircle3: { position: 'absolute', bottom: 200, right: -15, width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', opacity: 0.05, }, scrollContainer: { flex: 1, }, imageContainer: { position: 'relative', height: 300, }, foodImage: { width: '100%', height: '100%', }, descriptionBubble: { position: 'absolute', top: 20, left: 20, backgroundColor: 'rgba(255, 255, 255, 0.9)', paddingHorizontal: 12, paddingVertical: 8, borderRadius: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, descriptionText: { fontSize: 14, color: Colors.light.text, fontWeight: '500', }, nutritionCard: { backgroundColor: Colors.light.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '100%', marginTop: -24, paddingTop: 24, paddingHorizontal: 20, paddingBottom: 40, shadowColor: '#000', shadowOffset: { width: 0, height: -4, }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8, }, calorieSection: { flexDirection: 'row', alignItems: 'baseline', marginBottom: 8, }, calorieValue: { fontSize: 32, fontWeight: '700', lineHeight: 32, }, calorieUnit: { fontSize: 16, color: Colors.light.textSecondary, marginLeft: 8, }, foodName: { fontSize: 20, fontWeight: '600', color: Colors.light.text, marginBottom: 24, }, nutritionHeader: { marginBottom: 16, }, nutritionTitle: { fontSize: 18, fontWeight: '600', color: Colors.light.text, textAlign: 'center', }, nutritionRings: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 32, paddingVertical: 20, }, nutritionRingContainer: { alignItems: 'center', }, ringWrapper: { position: 'relative', marginBottom: 8, }, ringCenter: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center', justifyContent: 'center', }, ringCenterText: { fontSize: 14, fontWeight: '600', color: Colors.light.text, }, nutritionRingLabel: { fontSize: 14, color: Colors.light.textSecondary, fontWeight: '500', marginBottom: 2, }, nutritionRingValue: { fontSize: 12, color: Colors.light.text, fontWeight: '600', }, foodIntakeSection: { marginBottom: 20, }, foodIntakeTitle: { fontSize: 18, fontWeight: '600', color: Colors.light.text, marginBottom: 12, }, foodIntakeRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, paddingHorizontal: 16, backgroundColor: '#F8F9FA', borderRadius: 12, }, foodIntakeInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, }, foodIntakeIcon: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#FFF', alignItems: 'center', justifyContent: 'center', marginRight: 12, }, foodIconEmoji: { fontSize: 20, }, foodIntakeDetails: { flex: 1, }, foodIntakeName: { fontSize: 16, fontWeight: '500', color: Colors.light.text, marginBottom: 2, }, foodIntakeAmount: { fontSize: 14, color: Colors.light.textSecondary, }, foodIntakeCalories: { flexDirection: 'row', alignItems: 'center', }, foodIntakeCaloriesValue: { fontSize: 16, fontWeight: '600', color: Colors.light.text, marginRight: 8, }, editButton: { padding: 4, marginRight: 4, }, deleteButton: { padding: 4, }, bottomContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: '#FFF', borderTopWidth: 1, borderTopColor: '#E5E5E5', }, mealSelector: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, backgroundColor: '#F8F9FA', borderRadius: 20, }, mealIndicator: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#FF6B35', marginRight: 8, }, mealText: { fontSize: 14, color: '#333', marginRight: 4, }, recordButton: { backgroundColor: Colors.light.primary, paddingHorizontal: 24, paddingVertical: 10, borderRadius: 20, }, recordButtonText: { fontSize: 16, color: '#FFF', fontWeight: '500', }, recordButtonDisabled: { backgroundColor: '#CCC', }, // 餐次选择弹窗样式 mealSelectorOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, mealSelectorBackdrop: { flex: 1, }, mealSelectorModal: { backgroundColor: '#FFF', borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: 34, }, mealSelectorHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: '#E5E5E5', }, mealSelectorTitle: { fontSize: 18, fontWeight: '600', color: '#333', }, mealOptionsContainer: { paddingHorizontal: 20, paddingVertical: 16, }, mealOption: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16, paddingHorizontal: 16, borderRadius: 12, marginBottom: 8, backgroundColor: '#F8F9FA', }, mealOptionActive: { backgroundColor: '#E8F5E8', borderWidth: 1, borderColor: '#4CAF50', }, mealOptionIndicator: { width: 12, height: 12, borderRadius: 6, marginRight: 12, }, mealOptionText: { flex: 1, fontSize: 16, color: '#333', fontWeight: '500', }, mealOptionTextActive: { color: '#4CAF50', }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, errorText: { fontSize: 16, color: Colors.light.textMuted, textAlign: 'center', }, analysisText: { fontSize: 14, color: Colors.light.textSecondary, marginBottom: 12, lineHeight: 20, }, // 非食物检测样式 nonFoodContainer: { backgroundColor: '#FFF8E1', borderRadius: 16, padding: 24, alignItems: 'center', marginVertical: 16, borderWidth: 1, borderColor: '#FFE0B2', }, nonFoodIcon: { marginBottom: 16, }, nonFoodTitle: { fontSize: 18, fontWeight: '600', color: '#E65100', marginBottom: 8, textAlign: 'center', }, nonFoodMessage: { fontSize: 16, color: Colors.light.text, textAlign: 'center', lineHeight: 24, marginBottom: 16, }, nonFoodSuggestions: { alignSelf: 'stretch', }, nonFoodSuggestionsTitle: { fontSize: 16, fontWeight: '600', color: Colors.light.text, marginBottom: 8, }, nonFoodSuggestionItem: { fontSize: 14, color: Colors.light.textSecondary, marginBottom: 4, lineHeight: 20, }, retakePhotoButton: { backgroundColor: Colors.light.primary, paddingVertical: 16, paddingHorizontal: 32, borderRadius: 28, alignItems: 'center', flexDirection: 'row', justifyContent: 'center', flex: 1, shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 6, }, retakePhotoButtonText: { color: Colors.light.onPrimary, fontSize: 16, fontWeight: '600', letterSpacing: 0.5, }, placeholderContainer: { width: '100%', height: '100%', backgroundColor: '#F5F5F5', alignItems: 'center', justifyContent: 'center', }, placeholderContent: { alignItems: 'center', }, placeholderText: { fontSize: 16, color: '#666', fontWeight: '500', marginTop: 8, }, // 预览提示图标样式 previewHint: { position: 'absolute', top: 16, right: 16, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 20, padding: 8, }, // ImageViewing 组件样式 imageViewerHeader: { position: 'absolute', top: 60, left: 20, right: 20, backgroundColor: 'rgba(0, 0, 0, 0.7)', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, zIndex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, imageViewerCloseButton: { padding: 4, }, imageViewerHeaderText: { color: '#FFF', fontSize: 14, fontWeight: '500', flex: 1, textAlign: 'center', marginLeft: -28, // 补偿关闭按钮的宽度,保持文字居中 }, imageViewerFooter: { position: 'absolute', bottom: 60, left: 20, right: 20, alignItems: 'center', zIndex: 1, }, imageViewerFooterButton: { backgroundColor: 'rgba(0, 0, 0, 0.7)', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 20, }, imageViewerFooterButtonText: { color: '#FFF', fontSize: 16, fontWeight: '500', }, modalBackdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)', }, // 编辑弹窗样式 editModalSheet: { position: 'absolute', left: 0, right: 0, bottom: 0, backgroundColor: '#FFFFFF', borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 20, paddingBottom: 40, paddingTop: 20, }, modalHandle: { width: 36, height: 4, backgroundColor: '#E0E0E0', borderRadius: 2, alignSelf: 'center', marginBottom: 20, }, modalTitle: { fontSize: 20, fontWeight: '600', color: '#333333', marginBottom: 24, textAlign: 'center', }, editFieldContainer: { marginBottom: 20, }, editFieldLabel: { fontSize: 16, fontWeight: '500', color: '#333333', marginBottom: 8, }, editInput: { height: 50, borderWidth: 1, borderColor: '#E0E0E0', borderRadius: 12, paddingHorizontal: 16, fontSize: 16, backgroundColor: '#FFFFFF', color: '#333333', }, modalButtons: { flexDirection: 'row', gap: 12, marginTop: 8, }, modalCancelBtn: { flex: 1, height: 50, backgroundColor: '#F0F0F0', borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, modalCancelText: { fontSize: 16, fontWeight: '600', color: '#666666', }, modalSaveBtn: { flex: 1, height: 50, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, modalSaveText: { fontSize: 16, fontWeight: '600', }, });