import { HeaderBar } from '@/components/ui/HeaderBar'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useVipService } from '@/hooks/useVipService'; import { AiReportRecord, generateAiReport, getAiReportHistory } from '@/services/aiReport'; import { getAuthToken } from '@/services/api'; import { Toast } from '@/utils/toast.utils'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import * as FileSystem from 'expo-file-system/legacy'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image as ExpoImage } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import * as MediaLibrary from 'expo-media-library'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, Animated, Platform, Pressable, RefreshControl, ScrollView, Share, StatusBar, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function GalleryScreen() { const { t, i18n } = useTranslation(); const insets = useSafeAreaInsets(); const { ensureLoggedIn } = useAuthGuard(); const { checkServiceAccess } = useVipService(); const { openMembershipModal } = useMembershipModal(); const { width: screenWidth, height: screenHeight } = useWindowDimensions(); // 报告历史列表 const [reports, setReports] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [isGeneratingReport, setIsGeneratingReport] = useState(false); const [reportImageUrl, setReportImageUrl] = useState(null); const [reportLocalUri, setReportLocalUri] = useState(null); const [reportModalVisible, setReportModalVisible] = useState(false); const [isSavingReport, setIsSavingReport] = useState(false); const [isSharingReport, setIsSharingReport] = useState(false); const reportSpinAnim = useRef(new Animated.Value(0)).current; const reportIconSpin = reportSpinAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] }); const emptyImageHeight = useMemo(() => screenHeight / 1.5, [screenHeight]); const todayString = useMemo(() => dayjs().format('YYYY-MM-DD'), []); const reportImageSize = useMemo(() => { const maxWidth = Math.min(screenWidth - 40, 440); const maxHeight = screenHeight - 240; let width = maxWidth; let height = (maxWidth * 16) / 9; if (height > maxHeight) { height = maxHeight; width = (maxHeight * 9) / 16; } return { width, height }; }, [screenHeight, screenWidth]); // 加载报告历史 const loadReports = useCallback(async (pageNum: number, refresh = false) => { try { const response = await getAiReportHistory({ page: pageNum, pageSize: 10, status: 'success', }); if (refresh) { setReports(response.records); } else { setReports(prev => [...prev, ...response.records]); } setHasMore(pageNum < response.totalPages); setPage(pageNum); } catch (error: any) { console.error('load-ai-report-history-failed', error); if (refresh) { Toast.error(t('statistics.aiReport.loadFailed', '加载报告历史失败')); } } }, [t]); // 初始加载 useEffect(() => { const init = async () => { setIsLoading(true); await loadReports(1, true); setIsLoading(false); }; init(); }, [loadReports]); // 下拉刷新 const handleRefresh = useCallback(async () => { setIsRefreshing(true); await loadReports(1, true); setIsRefreshing(false); }, [loadReports]); // 加载更多 const handleLoadMore = useCallback(async () => { if (isLoadingMore || !hasMore) return; setIsLoadingMore(true); await loadReports(page + 1, false); setIsLoadingMore(false); }, [isLoadingMore, hasMore, page, loadReports]); useEffect(() => { if (!isGeneratingReport) { reportSpinAnim.stopAnimation(); return; } reportSpinAnim.setValue(0); const loop = Animated.loop( Animated.timing(reportSpinAnim, { toValue: 1, duration: 1400, useNativeDriver: true, }) ); loop.start(); return () => loop.stop(); }, [isGeneratingReport, reportSpinAnim]); const handleGenerateReport = useCallback(async () => { const ok = await ensureLoggedIn(); if (!ok || isGeneratingReport) return; // 检查 VIP 权限 const access = checkServiceAccess(); if (!access.canUseService) { openMembershipModal({ onPurchaseSuccess: () => { // 购买成功后自动触发生成 handleGenerateReport(); }, }); return; } setIsGeneratingReport(true); setReportLocalUri(null); Toast.info(t('statistics.aiReport.generating', '正在生成健康报告,预计 10~30 秒…')); try { const response = await generateAiReport({ date: todayString }); const imageUrl = (response as any)?.imageUrl ?? (response as any)?.url ?? (response as any)?.image_url; if (!imageUrl) { throw new Error(t('statistics.aiReport.missing', '未获取到报告图片,请稍后重试')); } setReportImageUrl(imageUrl); setReportModalVisible(true); Toast.success(t('statistics.aiReport.success', '报告已生成')); // 生成成功后刷新列表 handleRefresh(); } catch (error: any) { console.error('generate-ai-report-failed', error); Toast.error(error?.message ?? t('statistics.aiReport.failed', '生成报告失败,请稍后重试')); } finally { setIsGeneratingReport(false); } }, [ensureLoggedIn, isGeneratingReport, checkServiceAccess, openMembershipModal, t, todayString, handleRefresh]); const prepareLocalReportImage = useCallback(async () => { if (!reportImageUrl) { throw new Error(t('statistics.aiReport.missing', '未获取到报告图片,请稍后重试')); } if (reportLocalUri) { return reportLocalUri; } const fileUri = `${FileSystem.cacheDirectory}ai-report-${Date.now()}.jpg`; const token = await getAuthToken(); const download = await FileSystem.downloadAsync( reportImageUrl, fileUri, token ? { headers: { Authorization: `Bearer ${token}` } } : undefined, ); if (!download?.uri) { throw new Error(t('statistics.aiReport.missing', '未获取到报告图片,请稍后重试')); } setReportLocalUri(download.uri); return download.uri; }, [reportImageUrl, reportLocalUri, t]); const handleSaveReport = useCallback(async () => { if (isSavingReport) return; try { setIsSavingReport(true); const permission = await MediaLibrary.requestPermissionsAsync(); if (permission.status !== 'granted') { Toast.warning(t('statistics.aiReport.permission', '需要相册权限才能保存图片')); return; } const localUri = await prepareLocalReportImage(); await MediaLibrary.saveToLibraryAsync(localUri); Toast.success(t('statistics.aiReport.saved', '已保存到相册')); } catch (error: any) { console.error('save-ai-report-failed', error); Toast.error(error?.message ?? t('statistics.aiReport.saveFailed', '保存失败,请稍后重试')); } finally { setIsSavingReport(false); } }, [isSavingReport, prepareLocalReportImage, t]); const handleShareReport = useCallback(async () => { if (isSharingReport) return; try { setIsSharingReport(true); const localUri = await prepareLocalReportImage(); await Share.share({ message: t('statistics.aiReport.shareMessage', '这是我的 AI 健康报告,分享给你看看!'), url: Platform.OS === 'ios' ? localUri : `file://${localUri}`, title: t('statistics.aiReport.shareTitle', 'AI 健康报告') }); } catch (error: any) { console.error('share-ai-report-failed', error); Toast.error(error?.message ?? t('statistics.aiReport.shareFailed', '分享失败,请稍后重试')); } finally { setIsSharingReport(false); } }, [isSharingReport, prepareLocalReportImage, t]); // 点击卡片查看报告 const handleCardPress = useCallback((report: AiReportRecord) => { if (!report.imageUrl) return; setReportImageUrl(report.imageUrl); setReportLocalUri(null); setReportModalVisible(true); }, []); // 滚动到底部加载更多 const handleScroll = useCallback((event: any) => { const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; const paddingToBottom = 100; if (layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom) { handleLoadMore(); } }, [handleLoadMore]); const headerRight = isLiquidGlassAvailable() ? ( ) : ( ); const headerTitle = ( {t('statistics.aiReport.galleryTitle', 'AI 报告画廊')} {t('statistics.aiReport.gallerySubtitle', '沉浸式浏览你的健康报告')} ); return ( } onScroll={handleScroll} scrollEventThrottle={400} > {isLoading ? ( ) : reports.length === 0 ? ( { const imageUrl = i18n.language?.startsWith('en') ? 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/gallary/empty_en.jpg' : 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/gallary/empty_zh.jpg'; setReportImageUrl(imageUrl); setReportLocalUri(null); setReportModalVisible(true); }} > {t('statistics.aiReport.clickToPreview', '点击预览模板')} {t('statistics.aiReport.emptyHistory', '暂无报告记录')} {t('statistics.aiReport.emptyHistoryHint', '点击右上方按钮生成你的第一份报告')} ) : ( {reports.map((report) => ( [styles.card, pressed && styles.cardPressed]} onPress={() => handleCardPress(report)} > {dayjs(report.reportDate).format('YYYY年M月D日')} {dayjs(report.createdAt).format('HH:mm')} {t('statistics.aiReport.generated', '生成')} ))} {isLoadingMore && ( )} )} {reportModalVisible && ( setReportModalVisible(false)} /> {reportImageUrl ? ( ) : ( {t('statistics.aiReport.missing', '未获取到报告图片,请稍后重试')} )} {isSavingReport ? t('statistics.aiReport.saving', '保存中…') : t('statistics.aiReport.save', '保存')} {isSharingReport ? t('statistics.aiReport.sharing', '分享中…') : t('statistics.aiReport.share', '分享')} setReportModalVisible(false)}> {t('statistics.aiReport.close', '收起')} )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f7f8fb', }, headerCenter: { flex: 1, minWidth: 0, }, headerTitle: { fontSize: 18, fontFamily: 'AliBold', color: '#0F172A', textAlign: 'center', }, headerSubtitle: { marginTop: 2, color: '#6B7280', fontSize: 12, fontFamily: 'AliRegular', textAlign: 'center', }, reportButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, borderRadius: 18, overflow: 'hidden', }, reportButtonFallback: { backgroundColor: 'rgba(255, 255, 255, 0.5)', borderWidth: 1, borderColor: '#E5E7EB', }, reportIconWrapper: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#E0F2FE', alignItems: 'center', justifyContent: 'center', }, loadingContainer: { flex: 1, paddingTop: 100, alignItems: 'center', justifyContent: 'center', }, emptyContainer: { alignItems: 'center', gap: 24, }, emptyImageCard: { width: '100%', borderRadius: 20, overflow: 'hidden', shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 16, shadowOffset: { width: 0, height: 8 }, elevation: 6, }, emptyImage: { width: '100%', height: 380, }, emptyImageOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.15)', borderRadius: 20, }, previewHint: { position: 'absolute', top: 12, right: 12, flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: 'rgba(0, 0, 0, 0.5)', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 14, }, previewHintText: { fontSize: 12, fontFamily: 'AliRegular', color: '#fff', }, emptyContent: { alignItems: 'center', gap: 12, paddingHorizontal: 20, }, emptyTitle: { fontSize: 18, fontFamily: 'AliBold', color: '#1F2937', textAlign: 'center', }, emptySubtitle: { fontSize: 14, fontFamily: 'AliRegular', color: '#6B7280', textAlign: 'center', lineHeight: 20, }, emptyButton: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 28, paddingVertical: 14, backgroundColor: '#3B82F6', borderRadius: 28, marginTop: 8, shadowColor: '#3B82F6', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 4, }, emptyButtonText: { fontSize: 16, fontFamily: 'AliBold', color: '#fff', }, loadingMoreContainer: { paddingVertical: 20, alignItems: 'center', }, galleryGrid: { gap: 18, }, card: { backgroundColor: '#fff', borderRadius: 22, overflow: 'hidden', shadowColor: '#000', shadowOpacity: 0.08, shadowRadius: 12, shadowOffset: { width: 0, height: 8 }, elevation: 6, }, cardPressed: { transform: [{ scale: 0.99 }], }, cardImage: { width: '100%', height: 360, }, cardBody: { paddingHorizontal: 16, paddingVertical: 14, gap: 4, }, cardTitle: { fontSize: 16, fontFamily: 'AliBold', color: '#111827', }, cardSubtitle: { fontSize: 12, fontFamily: 'AliRegular', color: '#9CA3AF', }, modalOverlay: { position: 'absolute', inset: 0, backgroundColor: 'rgba(12, 18, 27, 0.78)', alignItems: 'center', justifyContent: 'center', padding: 16, }, modalCard: { backgroundColor: '#FDFDFE', borderRadius: 20, padding: 14, alignItems: 'center', gap: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.28, shadowRadius: 18, elevation: 16, }, reportImage: { borderRadius: 14, overflow: 'hidden', }, reportImageFallback: { borderRadius: 14, backgroundColor: '#F3F4F6', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, }, reportFallbackText: { textAlign: 'center', color: '#111827', fontFamily: 'AliRegular', }, modalActions: { flexDirection: 'row', gap: 10, }, modalButton: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, paddingVertical: 10, backgroundColor: '#E0F2FE', borderRadius: 12, borderWidth: 1, borderColor: '#BAE6FD', }, modalButtonDisabled: { opacity: 0.6, }, modalButtonText: { fontSize: 14, color: '#0F172A', fontFamily: 'AliBold', }, closeRow: { marginTop: 4, flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 8, paddingVertical: 6, }, closeLabel: { fontSize: 14, color: '#4B5563', fontFamily: 'AliRegular', }, });