688 lines
21 KiB
TypeScript
688 lines
21 KiB
TypeScript
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',
|
||
},
|
||
});
|