import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { deleteNutritionAnalysisRecord, getNutritionAnalysisRecords, type GetNutritionRecordsParams, type NutritionAnalysisRecord, type NutritionItem } 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 { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Alert, BackHandler, FlatList, RefreshControl, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import ImageViewing from 'react-native-image-viewing'; export default function NutritionAnalysisHistoryScreen() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop(); const router = useRouter(); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [expandedItems, setExpandedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [total, setTotal] = useState(0); const [statusFilter, setStatusFilter] = useState(''); const [error, setError] = useState(null); const [showImagePreview, setShowImagePreview] = useState(false); const [previewImageUri, setPreviewImageUri] = useState(null); const [deletingId, setDeletingId] = useState(null); const isGlassAvailable = isLiquidGlassAvailable(); // 处理Android返回键关闭图片预览 useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { if (showImagePreview) { setShowImagePreview(false); return true; // 阻止默认返回行为 } return false; }); return () => backHandler.remove(); }, [showImagePreview]); // 获取历史记录 const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false, currentStatusFilter?: string) => { try { // 清除之前的错误 setError(null); const params: GetNutritionRecordsParams = { page, limit: 20, }; // 使用传入的筛选条件或当前状态 const filterToUse = currentStatusFilter !== undefined ? currentStatusFilter : statusFilter; if (filterToUse) { params.status = filterToUse; } const response = await getNutritionAnalysisRecords(params); console.log('response', JSON.stringify(response)); if (response.code === 0) { const newRecords = response.data.records; if (isRefresh || page === 1) { setRecords(newRecords); } else { setRecords(prev => [...prev, ...newRecords]); } setTotal(response.data.total); setHasMore(page < response.data.totalPages); setCurrentPage(page); } else { const errorMessage = response.message || t('nutritionAnalysisHistory.errors.fetchFailed'); setError(errorMessage); Alert.alert(t('nutritionAnalysisHistory.errors.error'), errorMessage); } } catch (error) { console.error('[HISTORY] 获取历史记录失败:', error); const errorMessage = t('nutritionAnalysisHistory.errors.fetchFailedRetry'); setError(errorMessage); Alert.alert(t('nutritionAnalysisHistory.errors.error'), errorMessage); } finally { setLoading(false); setRefreshing(false); setLoadingMore(false); } }, [statusFilter]); // 初始加载 - 只在组件挂载时执行一次 useEffect(() => { setLoading(true); fetchRecords(1, true); }, []); // 移除 fetchRecords 依赖,避免循环 // 筛选条件变化时的处理 useEffect(() => { // 只有在非初始加载时才执行 if (!loading) { setLoading(true); setCurrentPage(1); fetchRecords(1, true, statusFilter); } }, [statusFilter]); // 只依赖 statusFilter // 下拉刷新 const handleRefresh = useCallback(() => { setRefreshing(true); fetchRecords(1, true); }, []); // 移除 fetchRecords 依赖 // 加载更多 const handleLoadMore = useCallback(() => { if (!hasMore || loadingMore || loading || error) return; // 添加错误状态检查 setLoadingMore(true); fetchRecords(currentPage + 1, false); }, [hasMore, loadingMore, loading, currentPage, error]); // 移除 fetchRecords 依赖,添加 error 依赖 // 切换展开状态 const toggleExpanded = useCallback((id: number) => { triggerLightHaptic(); setExpandedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } return newSet; }); }, []); // 获取状态颜色 const getStatusColor = (status: string) => { switch (status) { case 'success': return '#4CAF50'; case 'failed': return '#F44336'; case 'processing': return '#FF9800'; default: return '#9E9E9E'; } }; // 获取状态文本 const getStatusText = (status: string) => { switch (status) { case 'success': return t('nutritionAnalysisHistory.status.success'); case 'failed': return t('nutritionAnalysisHistory.status.failed'); case 'processing': return t('nutritionAnalysisHistory.status.processing'); default: return t('nutritionAnalysisHistory.status.unknown'); } }; // 从营养数据中提取主要营养素的辅助函数 const getMainNutrients = (data: NutritionItem[]) => { const energy = data.find(item => item.key === 'energy_kcal'); const protein = data.find(item => item.key === 'protein'); const carbs = data.find(item => item.key === 'carbohydrate'); const fat = data.find(item => item.key === 'fat'); return { energy: energy?.value || '', protein: protein?.value || '', carbs: carbs?.value || '', fat: fat?.value || '' }; }; // 处理图片预览 const handleImagePreview = useCallback((imageUrl: string) => { triggerLightHaptic(); setPreviewImageUri(imageUrl); setShowImagePreview(true); }, []); // 处理删除记录 const handleDeleteRecord = useCallback((recordId: number) => { Alert.alert( t('nutritionAnalysisHistory.delete.confirmTitle'), t('nutritionAnalysisHistory.delete.confirmMessage'), [ { text: t('nutritionAnalysisHistory.delete.cancel'), style: 'cancel', }, { text: t('nutritionAnalysisHistory.delete.delete'), style: 'destructive', onPress: async () => { try { setDeletingId(recordId); await deleteNutritionAnalysisRecord(recordId); // 从本地状态中移除删除的记录 setRecords(prev => prev.filter(record => record.id !== recordId)); setTotal(prev => Math.max(0, prev - 1)); // 触发轻微震动反馈 triggerLightHaptic(); // 显示成功提示 Alert.alert(t('nutritionAnalysisHistory.delete.successTitle'), t('nutritionAnalysisHistory.delete.successMessage')); } catch (error) { console.error('[HISTORY] 删除记录失败:', error); Alert.alert(t('nutritionAnalysisHistory.errors.error'), t('nutritionAnalysisHistory.errors.deleteFailed')); } finally { setDeletingId(null); } }, }, ] ); }, []); // 渲染历史记录项 const renderRecordItem = useCallback(({ item }: { item: NutritionAnalysisRecord }) => { const isExpanded = expandedItems.has(item.id); const isSuccess = item.status === 'success'; return ( {/* 头部信息 */} {isSuccess && ( {t('nutritionAnalysisHistory.recognized', { count: item.nutritionCount })} )} {dayjs(item.createdAt).format(t('nutritionAnalysisHistory.dateFormat'))} {getStatusText(item.status)} {/* 删除按钮 */} handleDeleteRecord(item.id)} disabled={deletingId === item.id} activeOpacity={0.7} > {isGlassAvailable ? ( {deletingId === item.id ? ( ) : ( )} ) : ( {deletingId === item.id ? ( ) : ( )} )} {/* 图片预览 */} {item.imageUrl && ( handleImagePreview(item.imageUrl)} activeOpacity={0.9} > {/* 预览提示图标 */} )} {/* 分析结果摘要 */} {isSuccess && item.analysisResult && item.analysisResult.data && item.analysisResult.data.length > 0 && ( {(() => { const mainNutrients = getMainNutrients(item.analysisResult.data); return ( <> {mainNutrients.energy && ( {t('nutritionAnalysisHistory.nutrients.energy')} {mainNutrients.energy} )} {mainNutrients.protein && ( {t('nutritionAnalysisHistory.nutrients.protein')} {mainNutrients.protein} )} {mainNutrients.carbs && ( {t('nutritionAnalysisHistory.nutrients.carbs')} {mainNutrients.carbs} )} {mainNutrients.fat && ( {t('nutritionAnalysisHistory.nutrients.fat')} {mainNutrients.fat} )} ); })()} )} {/* 失败信息 */} {!isSuccess && ( {item.message} )} {/* 展开/收起按钮 */} toggleExpanded(item.id)} activeOpacity={0.7} > {isExpanded ? t('nutritionAnalysisHistory.actions.collapse') : t('nutritionAnalysisHistory.actions.expand')} {/* 详细信息 */} {isExpanded && isSuccess && item.analysisResult && item.analysisResult.data && ( {t('nutritionAnalysisHistory.details.title')} {item.analysisResult.data.map((nutritionItem: NutritionItem) => ( {nutritionItem.name} {nutritionItem.value} {nutritionItem.analysis && ( {nutritionItem.analysis} )} ))} {t('nutritionAnalysisHistory.details.aiModel')}: {item.aiModel} {t('nutritionAnalysisHistory.details.provider')}: {item.aiProvider} )} ); }, [expandedItems, toggleExpanded]); // 渲染空状态 const renderEmptyState = () => ( {t('nutritionAnalysisHistory.empty.title')} {t('nutritionAnalysisHistory.empty.subtitle')} ); // 渲染错误状态 const renderErrorState = () => ( {t('nutritionAnalysisHistory.errors.loadFailed')} {error || t('nutritionAnalysisHistory.errors.unknownError')} { setLoading(true); fetchRecords(1, true); }} > {t('nutritionAnalysisHistory.actions.retry')} ); // 渲染底部加载指示器 const renderFooter = () => { if (!loadingMore) return null; return ( {t('nutritionAnalysisHistory.loadingMore')} ); }; return ( {/* 背景渐变 */} router.back()} transparent={true} /> {/* 筛选按钮 */} { if (statusFilter !== '') { setStatusFilter(''); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, ''); } }} activeOpacity={0.7} > {t('nutritionAnalysisHistory.filter.all')} { if (statusFilter !== 'success') { setStatusFilter('success'); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, 'success'); } }} activeOpacity={0.7} > {t('nutritionAnalysisHistory.status.success')} { if (statusFilter !== 'failed') { setStatusFilter('failed'); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, 'failed'); } }} activeOpacity={0.7} > {t('nutritionAnalysisHistory.status.failed')} {/* 记录列表 */} {loading ? ( {t('nutritionAnalysisHistory.loading')} ) : ( item.id.toString()} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} refreshControl={ } onEndReached={handleLoadMore} onEndReachedThreshold={0.2} // 提高阈值,减少频繁触发 ListEmptyComponent={error ? renderErrorState : renderEmptyState} // 错误时显示错误状态 ListFooterComponent={renderFooter} /> )} {/* 图片预览 */} setShowImagePreview(false)} swipeToCloseEnabled={true} doubleTapToZoomEnabled={true} HeaderComponent={() => ( {dayjs().format(t('nutritionAnalysisHistory.dateFormat'))} )} FooterComponent={() => ( setShowImagePreview(false)} > {t('nutritionLabelAnalysis.actions.close')} )} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5e5fbff', }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, filterContainer: { flexDirection: 'row', paddingHorizontal: 16, paddingBottom: 12, gap: 8, }, filterButton: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.7)', borderWidth: 1, borderColor: '#E0E0E0', }, filterButtonActive: { backgroundColor: Colors.light.primary, borderColor: Colors.light.primary, }, filterButtonText: { fontSize: 14, fontWeight: '500', color: '#666', }, filterButtonTextActive: { color: '#FFF', }, listContainer: { paddingHorizontal: 16, paddingBottom: 20, }, recordItem: { backgroundColor: Colors.light.background, borderRadius: 16, padding: 16, marginBottom: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, recordHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, }, recordInfo: { flex: 1, }, recordTitle: { fontSize: 16, fontWeight: '600', color: Colors.light.text, marginBottom: 4, }, recordDate: { fontSize: 14, color: Colors.light.textSecondary, marginBottom: 4, }, statusBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, alignSelf: 'flex-start', }, statusText: { fontSize: 12, fontWeight: '500', color: '#FFF', }, glassDeleteButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, fallbackDeleteButton: { borderWidth: 1, borderColor: 'rgba(244, 67, 54, 0.3)', backgroundColor: 'rgba(244, 67, 54, 0.1)', }, imageContainer: { marginBottom: 12, position: 'relative', }, thumbnail: { width: '100%', height: 120, borderRadius: 12, }, previewHint: { position: 'absolute', top: 8, right: 8, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 16, padding: 6, }, summaryContainer: { marginBottom: 12, }, nutritionSummary: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, }, nutritionItem: { flex: 1, minWidth: '45%', backgroundColor: 'rgba(74, 144, 226, 0.1)', padding: 8, borderRadius: 8, }, nutritionLabel: { fontSize: 12, color: Colors.light.textSecondary, marginBottom: 2, }, nutritionValue: { fontSize: 14, fontWeight: '600', color: Colors.light.primary, }, errorContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(244, 67, 54, 0.1)', padding: 12, borderRadius: 8, marginBottom: 12, }, errorMessage: { fontSize: 14, color: '#F44336', marginLeft: 8, flex: 1, }, expandButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 8, }, expandButtonText: { fontSize: 14, color: Colors.light.primary, fontWeight: '500', marginRight: 4, }, detailsContainer: { borderTopWidth: 1, borderTopColor: '#F0F0F0', paddingTop: 16, marginTop: 8, }, detailsTitle: { fontSize: 16, fontWeight: '600', color: Colors.light.text, marginBottom: 12, }, detailItem: { paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F8F8F8', }, detailLabel: { fontSize: 14, color: Colors.light.text, }, nutritionInfo: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, }, detailValue: { fontSize: 14, fontWeight: '600', color: Colors.light.primary, }, analysisText: { fontSize: 12, color: Colors.light.textSecondary, lineHeight: 16, marginTop: 4, paddingHorizontal: 8, paddingVertical: 4, backgroundColor: 'rgba(74, 144, 226, 0.05)', borderRadius: 6, }, metaInfo: { marginTop: 12, paddingTop: 12, borderTopWidth: 1, borderTopColor: '#F0F0F0', }, metaText: { fontSize: 12, color: Colors.light.textSecondary, marginBottom: 4, }, emptyState: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, }, emptyStateText: { fontSize: 18, fontWeight: '600', color: Colors.light.text, marginTop: 16, }, emptyStateSubtext: { fontSize: 14, color: Colors.light.textSecondary, marginTop: 8, }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, loadingText: { fontSize: 16, color: Colors.light.textSecondary, marginTop: 12, }, loadingFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 20, }, loadingFooterText: { fontSize: 14, color: Colors.light.textSecondary, marginLeft: 8, }, errorState: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, }, errorStateText: { fontSize: 18, fontWeight: '600', color: Colors.light.text, marginTop: 16, }, errorStateSubtext: { fontSize: 14, color: Colors.light.textSecondary, marginTop: 8, textAlign: 'center', paddingHorizontal: 32, }, retryButton: { marginTop: 20, paddingHorizontal: 24, paddingVertical: 12, backgroundColor: Colors.light.primary, borderRadius: 24, }, retryButtonText: { color: '#FFF', fontSize: 16, fontWeight: '600', }, // 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', }, });