feat(ai报告): 新增AI健康报告画廊功能,支持报告生成、保存与分享
This commit is contained in:
687
app/gallery/index.tsx
Normal file
687
app/gallery/index.tsx
Normal file
@@ -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<AiReportRecord[]>([]);
|
||||
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<string | null>(null);
|
||||
const [reportLocalUri, setReportLocalUri] = useState<string | null>(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() ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleGenerateReport}
|
||||
disabled={isGeneratingReport}
|
||||
>
|
||||
<GlassView
|
||||
style={styles.reportButton}
|
||||
glassEffectStyle="clear"
|
||||
isInteractive
|
||||
>
|
||||
<Animated.View style={[styles.reportIconWrapper, isGeneratingReport && { transform: [{ rotate: reportIconSpin }] }]}>
|
||||
<Ionicons name="sparkles-outline" size={18} color="#0F172A" />
|
||||
</Animated.View>
|
||||
</GlassView>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleGenerateReport}
|
||||
style={[styles.reportButton, styles.reportButtonFallback]}
|
||||
disabled={isGeneratingReport}
|
||||
>
|
||||
<Animated.View style={[styles.reportIconWrapper, isGeneratingReport && { transform: [{ rotate: reportIconSpin }] }]}>
|
||||
<Ionicons name="sparkles-outline" size={18} color="#0F172A" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const headerTitle = (
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.headerTitle}>{t('statistics.aiReport.galleryTitle', 'AI 报告画廊')}</Text>
|
||||
<Text style={styles.headerSubtitle}>{t('statistics.aiReport.gallerySubtitle', '沉浸式浏览你的健康报告')}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<LinearGradient
|
||||
colors={['#f0f4ff', '#fdf8ff', '#f6f8fa']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
/>
|
||||
<HeaderBar
|
||||
title={headerTitle}
|
||||
right={headerRight}
|
||||
tone="light"
|
||||
transparent
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{
|
||||
paddingTop: insets.top + 56,
|
||||
paddingBottom: 40,
|
||||
paddingHorizontal: 16,
|
||||
...(reports.length === 0 && !isLoading ? { flexGrow: 1, justifyContent: 'center' } : {})
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#6B7280"
|
||||
/>
|
||||
}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={400}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#3B82F6" />
|
||||
</View>
|
||||
) : reports.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Pressable
|
||||
style={styles.emptyImageCard}
|
||||
onPress={() => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<ExpoImage
|
||||
source={{
|
||||
uri: 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'
|
||||
}}
|
||||
style={[styles.emptyImage, { height: emptyImageHeight }]}
|
||||
contentFit="contain"
|
||||
transition={300}
|
||||
/>
|
||||
<View style={styles.emptyImageOverlay}>
|
||||
<View style={styles.previewHint}>
|
||||
<Ionicons name="expand-outline" size={14} color="#fff" />
|
||||
<Text style={styles.previewHintText}>{t('statistics.aiReport.clickToPreview', '点击预览模板')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
<View style={styles.emptyContent}>
|
||||
<Text style={styles.emptyTitle}>{t('statistics.aiReport.emptyHistory', '暂无报告记录')}</Text>
|
||||
<Text style={styles.emptySubtitle}>{t('statistics.aiReport.emptyHistoryHint', '点击右上方按钮生成你的第一份报告')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.galleryGrid}>
|
||||
{reports.map((report) => (
|
||||
<Pressable
|
||||
key={report.id}
|
||||
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
|
||||
onPress={() => handleCardPress(report)}
|
||||
>
|
||||
<ExpoImage
|
||||
source={{ uri: report.imageUrl }}
|
||||
style={styles.cardImage}
|
||||
contentFit="cover"
|
||||
transition={250}
|
||||
/>
|
||||
<View style={styles.cardBody}>
|
||||
<Text numberOfLines={1} style={styles.cardTitle}>
|
||||
{dayjs(report.reportDate).format('YYYY年M月D日')}
|
||||
</Text>
|
||||
<Text style={styles.cardSubtitle}>
|
||||
{dayjs(report.createdAt).format('HH:mm')} {t('statistics.aiReport.generated', '生成')}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
{isLoadingMore && (
|
||||
<View style={styles.loadingMoreContainer}>
|
||||
<ActivityIndicator size="small" color="#6B7280" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{reportModalVisible && (
|
||||
<View style={styles.modalOverlay}>
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={() => setReportModalVisible(false)} />
|
||||
<View style={styles.modalCard}>
|
||||
{reportImageUrl ? (
|
||||
<ExpoImage
|
||||
source={{ uri: reportImageUrl }}
|
||||
style={[styles.reportImage, { width: reportImageSize.width, height: reportImageSize.height }]}
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.reportImageFallback, { width: reportImageSize.width, height: reportImageSize.height }]}>
|
||||
<Text style={styles.reportFallbackText}>{t('statistics.aiReport.missing', '未获取到报告图片,请稍后重试')}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalButton, isSavingReport && styles.modalButtonDisabled]}
|
||||
onPress={handleSaveReport}
|
||||
disabled={isSavingReport}
|
||||
>
|
||||
<Ionicons name="download-outline" size={18} color="#0F172A" />
|
||||
<Text style={styles.modalButtonText}>
|
||||
{isSavingReport ? t('statistics.aiReport.saving', '保存中…') : t('statistics.aiReport.save', '保存')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalButton, isSharingReport && styles.modalButtonDisabled]}
|
||||
onPress={handleShareReport}
|
||||
disabled={isSharingReport}
|
||||
>
|
||||
<Ionicons name="share-social-outline" size={18} color="#0F172A" />
|
||||
<Text style={styles.modalButtonText}>
|
||||
{isSharingReport ? t('statistics.aiReport.sharing', '分享中…') : t('statistics.aiReport.share', '分享')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Pressable style={styles.closeRow} onPress={() => setReportModalVisible(false)}>
|
||||
<Ionicons name="close" size={18} color="#4B5563" />
|
||||
<Text style={styles.closeLabel}>{t('statistics.aiReport.close', '收起')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user