import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Line, Path, Rect, Text as SvgText } from 'react-native-svg'; import { useAuditReport } from '../src/hooks'; import { colors } from '../src/theme'; import type { AuditKPI, AuditTrendPoint, RiskDistributionItem } from '../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 数据审计报告 ); } /* ===================== 子组件:等级卡 ===================== */ function GradeCard({ grade, desc, todayBlocked }: { grade: string; desc: string; todayBlocked: number }) { return ( {grade} 风控审计报告 {desc} 今日拦截异常 {todayBlocked} 次 ); } /* ===================== 子组件:核心指标 ===================== */ function KPICard({ kpi }: { kpi: AuditKPI }) { const isPositive = kpi.change >= 0; return ( {kpi.value} {kpi.unit ?? ''} {kpi.label} {isPositive ? '+' : ''} {kpi.change}% ); } /* ===================== 子组件:威胁趋势图 ===================== */ function ThreatTrendChart({ data }: { data: AuditTrendPoint[] }) { const width = 320; const height = 160; const padding = { top: 20, right: 20, bottom: 28, left: 32 }; const chartW = width - padding.left - padding.right; const chartH = height - padding.top - padding.bottom; const maxRisk = Math.max(...data.map((d) => d.riskCount), 1); const maxHandled = Math.max(...data.map((d) => d.handledCount), 1); const maxY = Math.max(maxRisk, maxHandled); const getX = (i: number) => padding.left + (i / (data.length - 1)) * chartW; const getY = (v: number) => padding.top + chartH - (v / maxY) * chartH; const riskPath = data .map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.riskCount)}`) .join(' '); const handledPath = data .map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.handledCount)}`) .join(' '); return ( 威胁趋势 风险事件 已处理 {/* 网格线 */} {[0, 0.5, 1].map((ratio, i) => { const y = padding.top + chartH * ratio; return ( ); })} {/* Y 轴标签 */} {maxY} {Math.round(maxY / 2)} 0 {/* 折线 */} {/* 数据点 */} {data.map((d, i) => ( ))} {/* X 轴标签 */} {data.map((d, i) => ( {d.date} ))} ); } /* ===================== 子组件:风险类型分布 ===================== */ function RiskDistribution({ items }: { items: RiskDistributionItem[] }) { return ( 风险类型分布 {items.length} 类型 {items.map((item) => ( {item.label} {item.percent}% ))} ); } /* ===================== 页面 ===================== */ export default function AuditReportScreen() { const router = useRouter(); const { report, loading, error, refresh } = useAuditReport(); if (loading) { return (
router.back()} /> ); } if (error || !report) { return (
router.back()} /> 加载失败 重试 ); } return (
router.back()} /> 核心数据看板 {report.period} {report.kpis.map((kpi) => ( ))} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg }, header: { height: 44, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, backgroundColor: colors.white, }, headerBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, scroll: { flex: 1 }, content: { padding: 12 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { marginTop: 12, paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, gradeCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.success, borderRadius: 16, padding: 20, }, gradeCircle: { width: 72, height: 72, borderRadius: 36, backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', }, gradeText: { fontSize: 36, fontWeight: '800', color: colors.success }, gradeInfo: { flex: 1, marginLeft: 16 }, gradeTitle: { fontSize: 18, fontWeight: '700', color: colors.textInverse }, gradeDesc: { fontSize: 13, color: colors.textInverse, opacity: 0.9, marginTop: 4, lineHeight: 18 }, gradeSub: { fontSize: 12, color: colors.textInverse, opacity: 0.8, marginTop: 8 }, sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 24, marginBottom: 12, }, sectionBar: { width: 4, height: 16, borderRadius: 2, backgroundColor: colors.primary, marginRight: 8 }, sectionTitle: { flex: 1, fontSize: 16, fontWeight: '700', color: colors.textPrimary }, sectionPeriod: { fontSize: 12, color: colors.textHint }, kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, kpiCard: { width: '48%', backgroundColor: colors.white, borderRadius: 12, padding: 16, marginBottom: 12, }, kpiIconWrap: { width: 36, height: 36, borderRadius: 10, alignItems: 'center', justifyContent: 'center', marginBottom: 10, }, kpiValue: { fontSize: 24, fontWeight: '700', color: colors.textPrimary }, kpiUnit: { fontSize: 13, fontWeight: '600', color: colors.textSecondary }, kpiLabel: { fontSize: 13, color: colors.textSecondary, marginTop: 4 }, kpiChangeRow: { flexDirection: 'row', alignItems: 'center', marginTop: 8 }, kpiChange: { fontSize: 12, fontWeight: '600', marginLeft: 3 }, chartCard: { backgroundColor: colors.white, borderRadius: 12, padding: 16, }, chartGap: { height: 12 }, chartHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, }, chartTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, legendRow: { flexDirection: 'row' }, legendItem: { flexDirection: 'row', alignItems: 'center', marginLeft: 12 }, legendDot: { width: 8, height: 8, borderRadius: 4 }, legendText: { marginLeft: 4, fontSize: 11, color: colors.textSecondary }, distributionWrap: { flexDirection: 'row', alignItems: 'center', marginTop: 8 }, piePlaceholder: { width: 120, height: 120, borderRadius: 60, backgroundColor: colors.border, alignItems: 'center', justifyContent: 'center', }, pieCenter: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', }, pieTotal: { fontSize: 22, fontWeight: '700', color: colors.textPrimary }, pieTotalLabel: { fontSize: 11, color: colors.textSecondary }, legendCol: { flex: 1, marginLeft: 20 }, distLegendItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 6, }, distLabel: { flex: 1, marginLeft: 8, fontSize: 13, color: colors.textPrimary }, distPercent: { fontSize: 13, fontWeight: '700', color: colors.textPrimary }, bottomPad: { height: 24 }, });