diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index 3c75b7e..2151d7f 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -14,17 +14,19 @@ import { WorkoutSummaryCard } from '@/components/WorkoutSummaryCard';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
-import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
import { syncHealthKitToServer } from '@/services/healthKitSync';
import { setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { updateUserProfile } from '@/store/userSlice';
import { fetchTodayWaterStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
-import { fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
+import { fetchHealthDataForDate } from '@/utils/health';
import { logger } from '@/utils/logger';
+import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
+import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
+import { useRouter } from 'expo-router';
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -63,8 +65,8 @@ export default function ExploreScreen() {
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
const userProfile = useAppSelector((s) => s.user.profile);
- const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
-
+ const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard();
+ const router = useRouter();
// 使用 dayjs:当月日期与默认选中"今天"
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
@@ -80,7 +82,11 @@ export default function ExploreScreen() {
return dayjs(currentSelectedDate).format('YYYY-MM-DD');
}, [currentSelectedDate]);
-
+ const handleOpenGallery = React.useCallback(async () => {
+ const ok = await ensureLoggedIn();
+ if (!ok) return;
+ router.push('/gallery');
+ }, [ensureLoggedIn, router]);
// 用于触发动画重置的 token(当日期或数据变化时更新)
const [animToken, setAnimToken] = useState(0);
@@ -384,42 +390,41 @@ export default function ExploreScreen() {
{/* 顶部信息栏 */}
- {/* 左边logo */}
-
+
+
- {/* 右边文字区域 */}
-
- {t('statistics.title')}
+ {/* 右边文字区域 */}
+
+ {t('statistics.title')}
+
- {/* 开发环境调试按钮 */}
- {__DEV__ && (
-
- {
- console.log('🔧 Manual background task test...');
- await BackgroundTaskManager.getInstance().triggerTaskForTesting();
- }}
- >
- 🔧
-
+
+
+ {isLiquidGlassAvailable() ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
- {
- console.log('🫀 Testing HRV data fetch...');
- await testHRVDataFetch();
- }}
- >
- 🫀
-
-
- )}
+
@@ -537,6 +542,7 @@ export default function ExploreScreen() {
{/* 围度数据卡片 - 占满底部一行 */}
+
);
}
@@ -585,6 +591,13 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
+ gap: 12,
+ },
+ headerLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ minWidth: 0,
},
logoImage: {
width: 28,
@@ -921,6 +934,53 @@ const styles = StyleSheet.create({
textAlign: 'left',
fontFamily: 'AliBold',
},
+ headerActions: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ },
+ reportButton: {
+ height: 36,
+ borderRadius: 18,
+ paddingHorizontal: 12,
+ backgroundColor: '#F6F7FB',
+ borderWidth: 1,
+ borderColor: '#E5E7EB',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ reportIconWrapper: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ reportButtonLabel: {
+ fontSize: 14,
+ fontFamily: 'AliBold',
+ color: '#0F172A',
+ },
+ // Liquid Glass 风格按钮
+ liquidGlassButton: {
+ height: 40,
+ width: 40,
+ borderRadius: 20,
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ },
+ liquidGlassFallback: {
+ backgroundColor: 'rgba(255, 255, 255, 0.6)',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.8)',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 4,
+ },
});
diff --git a/app/gallery/index.tsx b/app/gallery/index.tsx
new file mode 100644
index 0000000..f9b7988
--- /dev/null
+++ b/app/gallery/index.tsx
@@ -0,0 +1,687 @@
+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',
+ },
+});
diff --git a/i18n/en/health.ts b/i18n/en/health.ts
index ea6a8a0..3999c21 100644
--- a/i18n/en/health.ts
+++ b/i18n/en/health.ts
@@ -52,6 +52,32 @@ export const healthPermissions = {
export const statistics = {
title: 'Out Live',
+ aiReport: {
+ button: 'Report',
+ generating: 'Generating your AI health report, this may take 10–30s…',
+ generatingShort: 'Generating',
+ success: 'Report ready',
+ failed: 'Failed to generate report, please try again',
+ missing: 'Report is not ready yet, please try again',
+ permission: 'Media permission is required to save the report',
+ saved: 'Saved to Photos',
+ saveFailed: 'Save failed, please try again',
+ save: 'Save',
+ saving: 'Saving…',
+ share: 'Share',
+ sharing: 'Sharing…',
+ shareFailed: 'Share failed, please try again',
+ shareTitle: 'AI Health Report',
+ shareMessage: 'Here is my AI health report—take a look!',
+ close: 'Close',
+ galleryTitle: 'AI Report Gallery',
+ gallerySubtitle: 'Browse and keep your immersive reports',
+ bannerDesc: 'Tap generate on the top right, takes about 10–30s',
+ loadFailed: 'Failed to load report history',
+ emptyHistory: 'No reports yet',
+ emptyHistoryHint: 'Tap the top right to generate your first report',
+ generated: 'generated',
+ },
sections: {
bodyMetrics: 'Body Metrics',
},
diff --git a/i18n/zh/health.ts b/i18n/zh/health.ts
index 2178edd..9684117 100644
--- a/i18n/zh/health.ts
+++ b/i18n/zh/health.ts
@@ -52,6 +52,33 @@ export const healthPermissions = {
export const statistics = {
title: 'Out Live',
+ aiReport: {
+ button: '报告',
+ generating: '正在生成健康报告,预计 10~30 秒…',
+ generatingShort: '生成中',
+ success: '报告已生成',
+ failed: '生成报告失败,请稍后重试',
+ missing: '未获取到报告图片,请稍后重试',
+ permission: '需要相册权限才能保存图片',
+ saved: '已保存到相册',
+ saveFailed: '保存失败,请稍后重试',
+ save: '保存',
+ saving: '保存中…',
+ share: '分享',
+ sharing: '分享中…',
+ shareFailed: '分享失败,请稍后重试',
+ shareTitle: 'AI 健康报告',
+ shareMessage: '这是我的 AI 健康报告,分享给你看看!',
+ close: '收起',
+ galleryTitle: 'AI 报告画廊',
+ gallerySubtitle: '沉浸式浏览你的健康报告',
+ bannerTitle: '今日 AI 健康报告',
+ bannerDesc: '点击右上角生成,约 10~30 秒',
+ loadFailed: '加载报告历史失败',
+ emptyHistory: '暂无报告记录',
+ emptyHistoryHint: '点击右上角生成你的第一份报告',
+ generated: '生成',
+ },
sections: {
bodyMetrics: '身体指标',
},
diff --git a/services/aiReport.ts b/services/aiReport.ts
new file mode 100644
index 0000000..11f268d
--- /dev/null
+++ b/services/aiReport.ts
@@ -0,0 +1,68 @@
+import { api } from '@/services/api';
+
+export type GenerateAiReportParams = {
+ date?: string;
+};
+
+export type GenerateAiReportResponse = {
+ imageUrl: string;
+};
+
+/**
+ * 调用后端生成 AI 健康报告图片
+ */
+export async function generateAiReport(params: GenerateAiReportParams = {}): Promise {
+ return await api.post('/users/ai-report', params, {
+ // 确保请求体使用 JSON
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+}
+
+// 报告状态
+export type AiReportStatus = 'pending' | 'processing' | 'success' | 'failed';
+
+// 单条报告记录
+export type AiReportRecord = {
+ id: string;
+ reportDate: string;
+ imageUrl: string;
+ status: AiReportStatus;
+ createdAt: string;
+};
+
+// 历史列表请求参数
+export type GetAiReportHistoryParams = {
+ page?: number;
+ pageSize?: number;
+ startDate?: string;
+ endDate?: string;
+ status?: AiReportStatus;
+};
+
+// 历史列表响应
+export type GetAiReportHistoryResponse = {
+ records: AiReportRecord[];
+ total: number;
+ page: number;
+ pageSize: number;
+ totalPages: number;
+};
+
+/**
+ * 获取 AI 健康报告历史列表
+ */
+export async function getAiReportHistory(params: GetAiReportHistoryParams = {}): Promise {
+ const searchParams = new URLSearchParams();
+ if (params.page) searchParams.set('page', String(params.page));
+ if (params.pageSize) searchParams.set('pageSize', String(params.pageSize));
+ if (params.startDate) searchParams.set('startDate', params.startDate);
+ if (params.endDate) searchParams.set('endDate', params.endDate);
+ if (params.status) searchParams.set('status', params.status);
+
+ const query = searchParams.toString();
+ const path = `/users/ai-report/history${query ? `?${query}` : ''}`;
+
+ return await api.get(path);
+}