import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useMemo, useState } from 'react'; import { Alert, ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { usePersonalDataExport } from '../src/hooks'; import { colors } from '../src/theme'; import type { DataExportCategory, DataExportFormat, PersonalDataExport } from '../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 个人数据导出 ); } /* ===================== 子组件:功能说明卡 ===================== */ function IntroCard({ title, description }: { title: string; description: string }) { return ( {title} {description} ); } /* ===================== 子组件:数据分类选择 ===================== */ function CategoryRow({ category, checked, onToggle, }: { category: DataExportCategory; checked: boolean; onToggle: () => void; }) { return ( {checked ? : null} {category.label} {category.description} {category.size} ); } /* ===================== 子组件:导出格式选择 ===================== */ function FormatCard({ format, selected, onSelect, }: { format: DataExportFormat; selected: boolean; onSelect: () => void; }) { return ( {selected ? : null} {format.label} {format.description} ); } /* ===================== 子组件:时间范围选择 ===================== */ function TimeRangeRow({ label, selected, onSelect, }: { label: string; selected: boolean; onSelect: () => void; }) { return ( {label} {selected ? : null} ); } /* ===================== 子组件:导出预览 ===================== */ function PreviewCard({ categoryCount, totalSize, estimatedTime, validity, }: { categoryCount: number; totalSize: string; estimatedTime: string; validity: string; }) { return ( 导出数据预览 {categoryCount} 类 已选分类 {totalSize} 预估大小 {estimatedTime} 预计处理时间 {validity} 有效期 ); } /* ===================== 子组件:最近导出记录 ===================== */ function RecentExportRow({ record, onDownload, }: { record: PersonalDataExport['recentExports'][number]; onDownload: () => void; }) { const isCompleted = record.status === 'completed'; return ( {record.name} {record.date} {record.size} {isCompleted ? '已完成' : '已过期'} ); } /* ===================== 页面 ===================== */ export default function PersonalDataExportScreen() { const router = useRouter(); const { exportData, loading, error, refresh, submit, submitting } = usePersonalDataExport(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [formatId, setFormatId] = useState(null); const [timeRangeId, setTimeRangeId] = useState(null); // 初始化本地选择状态 const initialized = useMemo(() => { if (!exportData) return false; if (selectedIds.size === 0) { const initial = new Set(); exportData.categories.forEach((cat) => { if (cat.items.every((item) => item.checked)) { initial.add(cat.id); } }); setSelectedIds(initial); } if (formatId === null) setFormatId(exportData.selectedFormat); if (timeRangeId === null) setTimeRangeId(exportData.selectedTimeRange); return true; }, [exportData, selectedIds.size, formatId, timeRangeId]); const toggleCategory = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleSubmit = async () => { await submit(); }; if (loading) { return (
router.back()} /> ); } if (error || !exportData || !initialized) { return (
router.back()} /> 加载失败 重试 ); } const selectedCategories = exportData.categories.filter((cat) => selectedIds.has(cat.id)); const selectedCount = selectedCategories.length; return (
router.back()} /> {/* 选择要导出的数据 */} 选择要导出的数据 { const allIds = new Set(exportData.categories.map((cat) => cat.id)); setSelectedIds(allIds); }} > 全选 {exportData.categories.map((category, index) => { const isLast = index === exportData.categories.length - 1; return ( toggleCategory(category.id)} /> ); })} {/* 导出格式 */} 导出格式 {exportData.formats.map((format) => ( setFormatId(format.id)} /> ))} {/* 时间范围 */} 时间范围 {exportData.timeRanges.map((range, index) => { const isLast = index === exportData.timeRanges.length - 1; return ( setTimeRangeId(range.id)} /> ); })} {/* 导出预览 */} 导出数据预览 {/* 最近导出 */} 最近导出 {exportData.recentExports.map((record, index) => { const isLast = index === exportData.recentExports.length - 1; return ( {/* 下载需要后端给出文件地址(RecentExportRecord 无下载地址字段),故按项目 4.4 节先例 保留入口 + 如实说明,不假装成功 */} // affordance-pending-backend Alert.alert('暂未开放', '导出文件下载需后端支持,当前版本暂未开放') } /> ); })} {/* 原先这里挂着「查看更多历史导出」,但记录只有 mock 三条、也没有分页接口和历史页,已摘掉 */} {submitting ? ( ) : ( 申请导出 )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, height: 48, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, body: { padding: 8, paddingBottom: 100, }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { marginTop: 8, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { color: colors.white, fontSize: 13, fontWeight: '600' }, introCard: { flexDirection: 'row', backgroundColor: colors.primaryLight, borderRadius: 8, padding: 8, marginBottom: 8, }, introIconWrap: { width: 48, height: 48, borderRadius: 24, backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, introText: { flex: 1 }, introTitle: { fontSize: 16, fontWeight: '700', color: colors.primaryText, marginBottom: 4, }, introDesc: { fontSize: 12, lineHeight: 17, color: colors.textSecondary, }, section: { marginBottom: 20 }, sectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, }, sectionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, sectionAction: { fontSize: 13, color: colors.primary, fontWeight: '600', }, card: { backgroundColor: colors.white, borderRadius: 8, overflow: 'hidden', }, rowBorder: { borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, categoryRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 14, }, checkbox: { width: 20, height: 20, borderRadius: 4, borderWidth: 2, borderColor: colors.borderStrong, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, checkboxActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, categoryBody: { flex: 1 }, categoryLabel: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, }, categoryDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2, }, categorySize: { fontSize: 12, color: colors.textHint, }, formatRow: { flexDirection: 'row', gap: 8, }, formatCard: { flex: 1, backgroundColor: colors.white, borderRadius: 8, padding: 8, borderWidth: 1, borderColor: colors.borderLight, }, formatCardActive: { borderColor: colors.primary, backgroundColor: colors.primaryLight, }, formatTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, }, formatIconWrap: { width: 44, height: 44, borderRadius: 12, backgroundColor: colors.pageBg, alignItems: 'center', justifyContent: 'center', }, formatIconWrapActive: { backgroundColor: colors.white, }, formatLabel: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 4, }, formatLabelActive: { color: colors.primary, }, formatDesc: { fontSize: 12, color: colors.textSecondary, lineHeight: 17, }, timeRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 14, }, timeLabel: { fontSize: 15, color: colors.textPrimary, }, timeLabelActive: { fontWeight: '600', color: colors.primary, }, radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: colors.borderStrong, alignItems: 'center', justifyContent: 'center', }, radioActive: { borderColor: colors.primary, }, radioDot: { width: 10, height: 10, borderRadius: 5, backgroundColor: colors.primary, }, previewCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, }, previewTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: 12, }, previewGrid: { flexDirection: 'row', flexWrap: 'wrap', }, previewItem: { width: '50%', paddingVertical: 10, }, previewValue: { fontSize: 16, fontWeight: '700', color: colors.primary, marginBottom: 2, }, previewLabel: { fontSize: 12, color: colors.textSecondary, }, recentRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, }, recentIconWrap: { width: 40, height: 40, borderRadius: 10, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, recentBody: { flex: 1 }, recentName: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, marginBottom: 4, }, recentMeta: { flexDirection: 'row', alignItems: 'center', gap: 8, }, recentDate: { fontSize: 12, color: colors.textSecondary, }, recentSize: { fontSize: 12, color: colors.textHint, }, statusTag: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8, }, statusCompleted: { backgroundColor: colors.successLight, }, statusExpired: { backgroundColor: colors.borderLight, }, statusText: { fontSize: 11, fontWeight: '600', }, statusTextCompleted: { color: colors.successText, }, statusTextExpired: { color: colors.textHint, }, recentAction: { width: 36, alignItems: 'center', }, viewMoreRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, borderTopWidth: 1, borderTopColor: colors.borderLight, }, viewMoreText: { fontSize: 13, color: colors.textSecondary, marginRight: 4, }, footer: { position: 'absolute', left: 0, right: 0, bottom: 0, backgroundColor: colors.white, paddingHorizontal: 16, paddingTop: 12, paddingBottom: 24, borderTopWidth: 1, borderTopColor: colors.borderLight, }, submitBtn: { backgroundColor: colors.primaryBtn, borderRadius: 24, paddingVertical: 14, alignItems: 'center', }, submitBtnDisabled: { backgroundColor: colors.textDisabled, }, submitBtnText: { color: colors.white, fontSize: 16, fontWeight: '700', }, });