import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useCosUpload } from '@/hooks/useCosUpload'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { analyzeNutritionImage, type NutritionAnalysisResponse } from '@/services/nutritionLabelAnalysis'; import { triggerLightHaptic } from '@/utils/haptics'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import * as ImagePicker from 'expo-image-picker'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useCallback, useRef, useState } from 'react'; import { ActivityIndicator, Alert, BackHandler, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import ImageViewing from 'react-native-image-viewing'; export default function NutritionLabelAnalysisScreen() { const safeAreaTop = useSafeAreaTop(); const router = useRouter(); const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); const { upload, uploading: uploadingToCos, progress: uploadProgress } = useCosUpload({ prefix: 'nutrition-labels' }); const [imageUri, setImageUri] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [showImagePreview, setShowImagePreview] = useState(false); const [newAnalysisResult, setNewAnalysisResult] = useState(null); const [isUploading, setIsUploading] = useState(false); // 流式请求相关引用 const streamAbortRef = useRef<{ abort: () => void } | null>(null); const scrollViewRef = useRef(null); // 处理Android返回键关闭图片预览 React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { if (showImagePreview) { setShowImagePreview(false); return true; // 阻止默认返回行为 } return false; }); return () => backHandler.remove(); }, [showImagePreview]); // 组件卸载时清理流式请求 React.useEffect(() => { return () => { try { if (streamAbortRef.current) { streamAbortRef.current.abort(); streamAbortRef.current = null; } } catch (error) { console.warn('[NUTRITION_ANALYSIS] Error aborting stream on unmount:', error); } }; }, []); // 请求相机权限 const requestCameraPermission = async () => { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== 'granted') { Alert.alert('权限不足', '需要相机权限才能拍摄成分表'); return false; } return true; }; // 拍照 const takePhoto = async () => { const hasPermission = await requestCameraPermission(); if (!hasPermission) return; triggerLightHaptic(); const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [4, 3], quality: 0.8, }); if (!result.canceled && result.assets[0]) { setImageUri(result.assets[0].uri); setNewAnalysisResult(null); // 清除之前的分析结果 } }; // 从相册选择 const pickImage = async () => { triggerLightHaptic(); const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [4, 3], quality: 0.8, }); if (!result.canceled && result.assets[0]) { setImageUri(result.assets[0].uri); setNewAnalysisResult(null); // 清除之前的分析结果 } }; // 新的分析函数:先上传图片到COS,然后调用新API const startNewAnalysis = useCallback(async (uri: string) => { if (isAnalyzing || isUploading) return; setIsUploading(true); setNewAnalysisResult(null); // 延迟滚动到分析结果区域,给UI一些时间更新 setTimeout(() => { scrollViewRef.current?.scrollTo({ y: 350, animated: true }); }, 300); try { // 第一步:上传图片到COS console.log('[NUTRITION_ANALYSIS] 开始上传图片到COS...'); const uploadResult = await upload(uri); console.log('[NUTRITION_ANALYSIS] 图片上传成功:', uploadResult.url); setIsUploading(false); setIsAnalyzing(true); // 第二步:调用新的营养成分分析API console.log('[NUTRITION_ANALYSIS] 开始调用营养成分分析API...'); const analysisResponse = await analyzeNutritionImage({ imageUrl: uploadResult.url }); console.log('[NUTRITION_ANALYSIS] API响应:', analysisResponse); if (analysisResponse.success && analysisResponse.data) { // 直接使用服务端返回的数据,不做任何转换 setNewAnalysisResult(analysisResponse); } else { throw new Error(analysisResponse.message || '分析失败'); } } catch (error: any) { console.error('[NUTRITION_ANALYSIS] 新API分析失败:', error); setIsUploading(false); setIsAnalyzing(false); // 显示错误提示 Alert.alert( '分析失败', error.message || '无法识别成分表,请尝试拍摄更清晰的照片' ); } finally { setIsUploading(false); setIsAnalyzing(false); } }, [isAnalyzing, isUploading, upload]); return ( {/* 背景渐变 */} router.back()} transparent={true} right={ isLiquidGlassAvailable() ? ( pushIfAuthedElseLogin('/food/nutrition-analysis-history')} activeOpacity={0.7} > ) : ( pushIfAuthedElseLogin('/food/nutrition-analysis-history')} style={[styles.historyButton, styles.fallbackBackground]} activeOpacity={0.7} > ) } /> {/* 图片区域 */} {imageUri ? ( setShowImagePreview(true)} activeOpacity={0.9} > {/* 预览提示图标 */} {/* 开始分析按钮 */} {!isAnalyzing && !isUploading && !newAnalysisResult && ( { // 先验证登录状态 const isLoggedIn = await ensureLoggedIn(); if (isLoggedIn) { startNewAnalysis(imageUri); } }} activeOpacity={0.8} > 开始分析 )} {/* 删除图片按钮 */} { setImageUri(null); setNewAnalysisResult(null); triggerLightHaptic(); }} activeOpacity={0.8} > ) : ( 拍摄或选择成分表照片 {/* 操作按钮区域 */} 拍摄 相册 )} {/* 新API营养成分详细分析结果 */} {newAnalysisResult && newAnalysisResult.success && newAnalysisResult.data && ( 营养成分详细分析 {newAnalysisResult.data.map((item, index) => ( {item.name} {item.value} {item.analysis} ))} )} {/* 上传状态 */} {isUploading && ( 正在上传图片... {uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ''} )} {/* 加载状态 */} {isAnalyzing && !newAnalysisResult && !isUploading && ( 正在分析成分表... )} {/* 图片预览 */} setShowImagePreview(false)} swipeToCloseEnabled={true} doubleTapToZoomEnabled={true} HeaderComponent={() => ( {dayjs().format('YYYY年M月D日 HH:mm')} )} FooterComponent={() => ( setShowImagePreview(false)} > 关闭 )} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5e5fbff', }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, scrollContainer: { flex: 1, }, imageContainer: { position: 'relative', height: 300, marginHorizontal: 16, marginTop: 16, borderRadius: 20, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, foodImage: { width: '100%', height: '100%', borderRadius: 20, }, previewHint: { position: 'absolute', top: 16, right: 16, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 20, padding: 8, }, deleteImageButton: { position: 'absolute', top: 16, left: 16, backgroundColor: 'rgba(255, 59, 48, 0.9)', borderRadius: 20, padding: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, placeholderContainer: { width: '100%', height: '100%', backgroundColor: '#FFFFFF', alignItems: 'center', justifyContent: 'center', borderRadius: 20, shadowColor: 'rgba(0, 0, 0, 0.1)', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.15, shadowRadius: 8, elevation: 5, }, placeholderContent: { alignItems: 'center', marginBottom: 40, }, placeholderText: { fontSize: 16, color: '#666', fontWeight: '500', marginTop: 8, }, imageActionButtonsContainer: { flexDirection: 'row', gap: 12, paddingHorizontal: 20, }, imageActionButton: { flex: 1, backgroundColor: Colors.light.primary, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 16, alignItems: 'center', flexDirection: 'row', justifyContent: 'center', shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, imageActionButtonSecondary: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: Colors.light.primary, shadowOpacity: 0, elevation: 0, }, imageActionButtonText: { color: Colors.light.onPrimary, fontSize: 14, fontWeight: '600', marginLeft: 6, }, resultCard: { backgroundColor: Colors.light.background, margin: 16, borderRadius: 16, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, resultHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, resultTitle: { fontSize: 18, fontWeight: '600', color: Colors.light.text, }, confidenceContainer: { backgroundColor: '#E8F5E8', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, }, confidenceText: { fontSize: 12, color: '#4CAF50', fontWeight: '500', }, foodInfoContainer: { marginBottom: 12, }, foodNameLabel: { fontSize: 14, color: Colors.light.textSecondary, marginBottom: 4, }, foodNameValue: { fontSize: 16, color: Colors.light.text, fontWeight: '500', }, nutritionGrid: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8, }, nutritionItem: { width: '50%', marginBottom: 12, paddingRight: 8, }, nutritionLabel: { fontSize: 12, color: Colors.light.textSecondary, marginBottom: 2, }, nutritionValue: { fontSize: 16, color: Colors.light.text, fontWeight: '600', }, loadingContainer: { alignItems: 'center', justifyContent: 'center', padding: 40, }, loadingText: { fontSize: 16, color: Colors.light.textSecondary, marginTop: 12, }, // 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, }, imageViewerHeaderText: { color: '#FFF', fontSize: 14, fontWeight: '500', textAlign: 'center', }, 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', }, // 开始分析按钮样式 analyzeButton: { position: 'absolute', bottom: 16, right: 16, backgroundColor: Colors.light.primary, paddingHorizontal: 16, paddingVertical: 12, borderRadius: 20, alignItems: 'center', flexDirection: 'row', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, analyzeButtonText: { color: Colors.light.onPrimary, fontSize: 14, fontWeight: '600', marginLeft: 6, }, // 营养成分详细分析卡片样式 analysisSection: { marginHorizontal: 16, marginTop: 28, marginBottom: 20, }, analysisSectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 14, }, analysisSectionHeaderIcon: { width: 32, height: 32, borderRadius: 12, backgroundColor: 'rgba(107, 110, 214, 0.12)', alignItems: 'center', justifyContent: 'center', }, analysisSectionTitle: { marginLeft: 10, fontSize: 18, fontWeight: '600', color: Colors.light.text, }, analysisCardsWrapper: { backgroundColor: '#FFFFFF', borderRadius: 28, padding: 18, shadowColor: 'rgba(107, 110, 214, 0.25)', shadowOffset: { width: 0, height: 10, }, shadowOpacity: 0.2, shadowRadius: 20, elevation: 6, }, analysisCardItem: { flexDirection: 'row', alignItems: 'flex-start', padding: 16, backgroundColor: 'rgba(127, 119, 255, 0.1)', borderRadius: 22, shadowColor: 'rgba(127, 119, 255, 0.28)', shadowOffset: { width: 0, height: 6, }, shadowOpacity: 0.16, shadowRadius: 12, elevation: 4, marginBottom: 12, }, analysisCardItemLast: { marginBottom: 0, }, analysisItemIconGradient: { width: 52, height: 52, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 14, }, analysisItemContent: { flex: 1, }, analysisItemHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, analysisItemName: { flex: 1, fontSize: 16, fontWeight: '600', color: '#272753', }, analysisItemValue: { fontSize: 16, fontWeight: '700', color: Colors.light.primary, }, analysisItemDescriptionRow: { flexDirection: 'row', alignItems: 'flex-start', }, analysisItemDescriptionIcon: { marginRight: 6, marginTop: 2, }, analysisItemDescription: { flex: 1, fontSize: 13, lineHeight: 18, color: 'rgba(39, 39, 83, 0.72)', }, // 历史记录按钮样式 historyButton: { width: 38, height: 38, alignItems: 'center', justifyContent: 'center', borderRadius: 19, overflow: 'hidden', }, fallbackBackground: { backgroundColor: 'rgba(255, 255, 255, 0.7)', shadowColor: '#000', shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, }, });