import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; import { useCosUpload } from '@/hooks/useCosUpload'; import { recognizeFood } from '@/services/foodRecognition'; import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice'; import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Alert, Animated, Easing, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; export default function FoodRecognitionScreen() { const router = useRouter(); const params = useLocalSearchParams<{ imageUri?: string; mealType?: string; }>(); const { imageUri, mealType } = params; const { upload } = useCosUpload(); const [showRecognitionProcess, setShowRecognitionProcess] = useState(false); const [recognitionLogs, setRecognitionLogs] = useState([]); const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle'); const dispatch = useAppDispatch(); // 动画引用 const scaleAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(50)).current; const progressAnim = useRef(new Animated.Value(0)).current; const pulseAnim = useRef(new Animated.Value(1)).current; // 启动动画效果 useEffect(() => { if (showRecognitionProcess) { // 进入动画 Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 600, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: 0, duration: 600, easing: Easing.out(Easing.cubic), useNativeDriver: true, }) ]).start(); // 启动进度条动画 if (currentStep === 'uploading' || currentStep === 'recognizing') { Animated.timing(progressAnim, { toValue: currentStep === 'uploading' ? 0.5 : 1, duration: 2000, easing: Easing.inOut(Easing.ease), useNativeDriver: false, }).start(); } // 脉冲动画 const pulseAnimation = Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.1, duration: 800, easing: Easing.inOut(Easing.sin), useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 800, easing: Easing.inOut(Easing.sin), useNativeDriver: true, }) ]) ); if (currentStep === 'uploading' || currentStep === 'recognizing') { pulseAnimation.start(); } else { pulseAnimation.stop(); pulseAnim.setValue(1); } } else { fadeAnim.setValue(0); slideAnim.setValue(50); progressAnim.setValue(0); } }, [showRecognitionProcess, currentStep]); const addLog = (message: string) => { setRecognitionLogs(prev => [...prev, message]); }; const handleConfirm = async () => { if (!imageUri) return; // 按钮动画效果 Animated.sequence([ Animated.timing(scaleAnim, { toValue: 0.95, duration: 100, useNativeDriver: true, }), Animated.timing(scaleAnim, { toValue: 1, duration: 100, useNativeDriver: true, }) ]).start(); try { setShowRecognitionProcess(true); setRecognitionLogs([]); setCurrentStep('uploading'); dispatch(setLoading(true)); addLog('📤 正在上传图片到云端...'); // 上传图片到 COS const { url } = await upload( { uri: imageUri, name: 'food-image.jpg', type: 'image/jpeg' }, { prefix: 'food-images/' } ); addLog('✅ 图片上传完成'); addLog('🤖 AI大模型分析中...'); setCurrentStep('recognizing'); // 调用食物识别 API const recognitionResult = await recognizeFood({ imageUrls: [url] }); console.log('食物识别结果:', recognitionResult); if (!recognitionResult.isFoodDetected) { addLog('❌ 识别失败:未检测到食物'); addLog(`💭 ${recognitionResult.nonFoodMessage || recognitionResult.analysisText}`); setCurrentStep('failed'); return; } addLog('✅ AI分析完成'); addLog(`🎯 识别置信度: ${recognitionResult.confidence}%`); addLog(`🍽️ 识别到 ${recognitionResult.items.length} 种食物`); setCurrentStep('completed'); // 生成唯一的识别ID const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; // 保存识别结果到 Redux dispatch(saveRecognitionResult({ id: recognitionId, result: recognitionResult })); // 延迟跳转,让用户看到完成状态 setTimeout(() => { router.replace(`/food/analysis-result?imageUri=${encodeURIComponent(url)}&mealType=${mealType}&recognitionId=${recognitionId}`); }, 1500); } catch (error) { console.warn('食物识别失败', error); addLog('❌ 识别过程出错'); addLog(`💥 ${error instanceof Error ? error.message : '未知错误'}`); setCurrentStep('failed'); dispatch(setError('食物识别失败,请重试')); } finally { dispatch(setLoading(false)); } }; const handleRetry = () => { setShowRecognitionProcess(false); setCurrentStep('idle'); setRecognitionLogs([]); dispatch(setError(null)); router.back() }; const handleGoBack = () => { if (showRecognitionProcess && currentStep !== 'failed') { Alert.alert( '正在识别中', '识别过程尚未完成,确定要返回吗?', [ { text: '继续识别', style: 'cancel' }, { text: '返回', style: 'destructive', onPress: () => router.back() } ] ); } else { router.back(); } }; if (!imageUri) { return ( router.back()} /> 未找到图片 ); } return ( {/* 主要内容区域 */} {!showRecognitionProcess ? ( // 确认界面 <> {/* 照片卡片 */} {/* 餐次标签叠加 */} {mealType && ( {getMealTypeLabel(mealType)} )} {/* AI 识别说明卡片 */} 智能食物识别 AI 将分析您的照片,识别食物种类、估算营养成分,为您生成详细的营养分析报告 {/* 底部按钮区域 */} 开始智能识别 ) : ( // 识别过程界面 {/* 照片缩略图卡片 */} {mealType && ( {getMealTypeLabel(mealType)} )} AI 识别中... {/* 进度指示卡片 */} {currentStep === 'uploading' || currentStep === 'recognizing' ? ( ) : currentStep === 'completed' ? ( ) : currentStep === 'failed' ? ( ) : null} { currentStep === 'idle' ? '准备中' : currentStep === 'uploading' ? '上传图片中' : currentStep === 'recognizing' ? 'AI 分析中' : currentStep === 'completed' ? '识别完成' : currentStep === 'failed' ? '识别失败' : '' } { currentStep === 'uploading' ? '正在将图片上传到云端处理...' : currentStep === 'recognizing' ? '智能模型正在分析食物成分...' : currentStep === 'completed' ? '即将跳转到分析结果页面' : currentStep === 'failed' ? '请检查网络连接或重新拍照' : '' } {/* 进度条 */} {(currentStep === 'uploading' || currentStep === 'recognizing') && ( )} {/* 识别日志卡片 */} 进度 {recognitionLogs.map((log, index) => ( {log} ))} {recognitionLogs.length === 0 && ( 等待处理开始... )} {/* 重试按钮 */} {currentStep === 'failed' && ( 返回重新拍照 )} )} ); } // 获取餐次标签 function getMealTypeLabel(mealType: string): string { const mealTypeMap: Record = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐', }; return mealTypeMap[mealType] || '未知餐次'; } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: Colors.light.pageBackgroundEmphasis, }, contentContainer: { flex: 1, paddingHorizontal: 16, paddingTop: 16, }, // 照片卡片样式 photoCard: { backgroundColor: Colors.light.card, borderRadius: 24, padding: 20, marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, shadowRadius: 20, elevation: 8, }, photoFrame: { width: '100%', aspectRatio: 1, borderRadius: 20, overflow: 'hidden', backgroundColor: Colors.light.neutral100, position: 'relative', }, photoImage: { width: '100%', height: '100%', }, mealTypeBadge: { position: 'absolute', top: 16, right: 16, paddingHorizontal: 14, paddingVertical: 8, backgroundColor: 'rgba(122, 90, 248, 0.95)', borderRadius: 20, backdropFilter: 'blur(10px)', }, mealTypeBadgeText: { color: Colors.light.onPrimary, fontSize: 13, fontWeight: '700', letterSpacing: 0.3, }, // 信息卡片样式 infoCard: { backgroundColor: Colors.light.card, borderRadius: 20, padding: 20, marginBottom: 24, shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 16, elevation: 4, borderWidth: 1, borderColor: Colors.light.heroSurfaceTint, }, infoHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, aiIconContainer: { width: 32, height: 32, borderRadius: 16, backgroundColor: Colors.light.heroSurfaceTint, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, infoTitle: { fontSize: 18, fontWeight: '700', color: Colors.light.text, letterSpacing: 0.2, }, infoDescription: { fontSize: 14, color: Colors.light.textSecondary, lineHeight: 22, letterSpacing: 0.1, }, // 按钮样式 bottomContainer: { paddingBottom: 40, paddingTop: 8, }, confirmButtonContainer: {}, confirmButton: { backgroundColor: Colors.light.primary, paddingVertical: 18, paddingHorizontal: 32, borderRadius: 28, alignItems: 'center', shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.35, shadowRadius: 20, elevation: 8, }, confirmButtonContent: { flexDirection: 'row', alignItems: 'center', }, confirmButtonText: { color: Colors.light.onPrimary, fontSize: 16, fontWeight: '700', letterSpacing: 0.4, }, // 识别过程容器 recognitionContainer: { flex: 1, }, // 缩略图卡片 thumbnailCard: { backgroundColor: Colors.light.card, borderRadius: 20, padding: 16, marginBottom: 16, flexDirection: 'row', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12, elevation: 4, }, thumbnailImage: { width: 70, height: 70, borderRadius: 16, }, thumbnailInfo: { flex: 1, marginLeft: 16, }, thumbnailMealType: { alignSelf: 'flex-start', paddingHorizontal: 12, paddingVertical: 6, backgroundColor: Colors.light.primary, borderRadius: 16, marginBottom: 6, }, thumbnailMealTypeText: { color: Colors.light.onPrimary, fontSize: 11, fontWeight: '700', letterSpacing: 0.2, }, thumbnailTitle: { fontSize: 16, fontWeight: '600', color: Colors.light.text, letterSpacing: 0.1, }, // 进度卡片 progressCard: { backgroundColor: Colors.light.card, borderRadius: 20, padding: 20, marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.08, shadowRadius: 16, elevation: 6, }, progressHeader: { flexDirection: 'row', alignItems: 'flex-start', }, statusIconAnimated: {}, statusIcon: { width: 48, height: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', marginRight: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, progressInfo: { flex: 1, justifyContent: 'center', }, statusText: { fontSize: 18, fontWeight: '700', color: Colors.light.text, marginBottom: 4, letterSpacing: 0.2, }, statusSubtext: { fontSize: 14, color: Colors.light.textSecondary, lineHeight: 20, letterSpacing: 0.1, }, // 进度条 progressBarContainer: { marginTop: 20, }, progressBarBackground: { width: '100%', height: 8, backgroundColor: Colors.light.neutral100, borderRadius: 4, overflow: 'hidden', }, progressBarFill: { height: '100%', backgroundColor: Colors.light.primary, borderRadius: 4, }, // 日志卡片 logCard: { backgroundColor: Colors.light.card, borderRadius: 20, padding: 20, marginBottom: 16, flex: 1, minHeight: 200, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12, elevation: 4, }, logHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, logTitle: { fontSize: 16, fontWeight: '700', color: Colors.light.text, marginLeft: 8, letterSpacing: 0.2, }, logScrollView: { flex: 1, backgroundColor: Colors.light.heroSurfaceTint, borderRadius: 16, paddingHorizontal: 16, paddingVertical: 12, }, logContent: { flexGrow: 1, }, logItem: { paddingVertical: 6, paddingHorizontal: 4, }, logText: { fontSize: 14, color: Colors.light.text, lineHeight: 22, letterSpacing: 0.1, }, logPlaceholder: { fontSize: 14, color: Colors.light.textMuted, fontStyle: 'italic', textAlign: 'center', marginTop: 40, }, // 重试按钮 retryButton: { backgroundColor: Colors.light.primary, paddingVertical: 18, paddingHorizontal: 32, borderRadius: 28, alignItems: 'center', flexDirection: 'row', justifyContent: 'center', shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.35, shadowRadius: 20, elevation: 8, }, retryButtonText: { color: Colors.light.onPrimary, fontSize: 16, fontWeight: '700', letterSpacing: 0.4, }, // 通用样式 errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, errorText: { fontSize: 16, color: Colors.light.textMuted, textAlign: 'center', }, });