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); +}