import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; 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 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 || '获取历史记录失败'; setError(errorMessage); Alert.alert('错误', errorMessage); } } catch (error) { console.error('[HISTORY] 获取历史记录失败:', error); const errorMessage = '获取历史记录失败,请稍后重试'; setError(errorMessage); Alert.alert('错误', 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 '成功'; case 'failed': return '失败'; case 'processing': return '处理中'; default: return '未知'; } }; // 从营养数据中提取主要营养素的辅助函数 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( '确认删除', '确定要删除这条营养分析记录吗?此操作无法撤销。', [ { text: '取消', style: 'cancel', }, { text: '删除', 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('成功', '记录已删除'); } catch (error) { console.error('[HISTORY] 删除记录失败:', error); Alert.alert('错误', '删除失败,请稍后重试'); } finally { setDeletingId(null); } }, }, ] ); }, []); // 渲染历史记录项 const renderRecordItem = useCallback(({ item }: { item: NutritionAnalysisRecord }) => { const isExpanded = expandedItems.has(item.id); const isSuccess = item.status === 'success'; return ( {/* 头部信息 */} {isSuccess && ( 识别 {item.nutritionCount} 项营养素 )} {dayjs(item.createdAt).format('YYYY年M月D日 HH:mm')} {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 && ( 热量 {mainNutrients.energy} )} {mainNutrients.protein && ( 蛋白质 {mainNutrients.protein} )} {mainNutrients.carbs && ( 碳水 {mainNutrients.carbs} )} {mainNutrients.fat && ( 脂肪 {mainNutrients.fat} )} ); })()} )} {/* 失败信息 */} {!isSuccess && ( {item.message} )} {/* 展开/收起按钮 */} toggleExpanded(item.id)} activeOpacity={0.7} > {isExpanded ? '收起详情' : '展开详情'} {/* 详细信息 */} {isExpanded && isSuccess && item.analysisResult && item.analysisResult.data && ( 详细营养成分 {item.analysisResult.data.map((nutritionItem: NutritionItem) => ( {nutritionItem.name} {nutritionItem.value} {nutritionItem.analysis && ( {nutritionItem.analysis} )} ))} AI 模型: {item.aiModel} 服务提供商: {item.aiProvider} )} ); }, [expandedItems, toggleExpanded]); // 渲染空状态 const renderEmptyState = () => ( 暂无历史记录 开始识别营养成分表吧 ); // 渲染错误状态 const renderErrorState = () => ( 加载失败 {error || '未知错误'} { setLoading(true); fetchRecords(1, true); }} > 重试 ); // 渲染底部加载指示器 const renderFooter = () => { if (!loadingMore) return null; return ( 加载更多... ); }; return ( {/* 背景渐变 */} router.back()} transparent={true} /> {/* 筛选按钮 */} { if (statusFilter !== '') { setStatusFilter(''); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, ''); } }} activeOpacity={0.7} > 全部 { if (statusFilter !== 'success') { setStatusFilter('success'); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, 'success'); } }} activeOpacity={0.7} > 成功 { if (statusFilter !== 'failed') { setStatusFilter('failed'); setCurrentPage(1); // 直接调用数据获取,不依赖 useEffect setLoading(true); fetchRecords(1, true, 'failed'); } }} activeOpacity={0.7} > 失败 {/* 记录列表 */} {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('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, }, 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', }, });