import React, { useState, useEffect } from 'react'; import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title } from 'chart.js'; import { Pie, Bar } from 'react-chartjs-2'; import { statsApi } from './api'; ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title); const chartOptions = { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, font: { size: 16 } }, }, }; const num = (value) => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }; const StatisticsDashboard = ({ onClose }) => { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { const fetchStatistics = async () => { try { setLoading(true); const data = await statsApi.getStatistics(); setStats(data); setError(''); } catch (err) { setError('无法加载统计数据,请确认后端服务正常。'); console.error(err); } finally { setLoading(false); } }; fetchStatistics(); }, []); const generateChartData = (title, data, colors = null) => { const labels = Object.keys(data || {}); const values = Object.values(data || {}).map(num); const defaultBg = [ 'rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)', 'rgba(255, 159, 64, 0.7)', ]; const defaultBorder = [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)', ]; return { labels, datasets: [ { label: title, data: values, backgroundColor: colors?.backgroundColor || defaultBg, borderColor: colors?.borderColor || defaultBorder, borderWidth: 1, }, ], }; }; if (loading) { return (

正在加载统计数据...

); } if (error) { return (

{error}

); } if (!stats) return null; const { dinsar_results_overview = {}, source_data_overview = {}, by_satellite = {}, ai_quality_overview = {}, ai_prediction_overview = {}, dinsar_cache_consistency = {}, source_preview_consistency = {}, source_xml_consistency = {}, } = stats; const sourceDataOrbit = generateChartData( '源数据精轨状态', { 有精轨数据: num(source_data_overview.with_orbit_data_count), 无精轨数据: Math.max(0, num(source_data_overview.total_count) - num(source_data_overview.with_orbit_data_count)), }, { backgroundColor: ['rgba(75, 192, 192, 0.7)', 'rgba(201, 203, 207, 0.7)'], borderColor: ['rgba(75, 192, 192, 1)', 'rgba(201, 203, 207, 1)'], } ); const dinsarData = generateChartData( 'D-InSAR 缓存状态', { 已缓存: num(dinsar_results_overview.cached_count), 未缓存: num(dinsar_results_overview.uncached_count), }, { backgroundColor: ['rgba(75, 192, 192, 0.7)', 'rgba(255, 99, 132, 0.7)'], borderColor: ['rgba(75, 192, 192, 1)', 'rgba(255, 99, 132, 1)'], } ); const sourceDataEnvi = generateChartData( '源数据 ENVI 处理状态', { 有ENVI结果: num(source_data_overview.envi_processed_count), 无ENVI结果: Math.max(0, num(source_data_overview.total_count) - num(source_data_overview.envi_processed_count)), }, { backgroundColor: ['rgba(54, 162, 235, 0.7)', 'rgba(201, 203, 207, 0.7)'], borderColor: ['rgba(54, 162, 235, 1)', 'rgba(201, 203, 207, 1)'], } ); const aiQualityData = generateChartData( '人工标记质量分布', { 人工标记良好: num(ai_quality_overview.good_count), 人工标记欠佳: num(ai_quality_overview.bad_count), 未人工标记: num(ai_quality_overview.unlabeled_count), }, { backgroundColor: ['rgba(40, 167, 69, 0.7)', 'rgba(220, 53, 69, 0.7)', 'rgba(108, 117, 125, 0.7)'], borderColor: ['rgba(40, 167, 69, 1)', 'rgba(220, 53, 69, 1)', 'rgba(108, 117, 125, 1)'], } ); const aiPredictionData = generateChartData( 'AI 预测质量分布', { 'AI预测良好 (>=0.7)': num(ai_prediction_overview.good_count), 'AI预测欠佳 (<0.4)': num(ai_prediction_overview.bad_count), AI预测中等: num(ai_prediction_overview.medium_count), 未预测: num(ai_prediction_overview.unpredicted_count), }, { backgroundColor: ['rgba(40, 167, 69, 0.7)', 'rgba(220, 53, 69, 0.7)', 'rgba(255, 193, 7, 0.7)', 'rgba(108, 117, 125, 0.7)'], borderColor: ['rgba(40, 167, 69, 1)', 'rgba(220, 53, 69, 1)', 'rgba(255, 193, 7, 1)', 'rgba(108, 117, 125, 1)'], } ); const dinsarConsistencyData = generateChartData( 'D-InSAR 缓存一致性', { '库缓存且文件存在': num(dinsar_cache_consistency.db_cached_and_file_exists_count), '库缓存但文件缺失': num(dinsar_cache_consistency.db_cached_but_file_missing_count), '库未缓存但文件存在': num(dinsar_cache_consistency.db_uncached_but_file_exists_count), '库未缓存且文件缺失': num(dinsar_cache_consistency.db_uncached_and_file_missing_count), }, { backgroundColor: ['rgba(40, 167, 69, 0.7)', 'rgba(220, 53, 69, 0.7)', 'rgba(255, 193, 7, 0.7)', 'rgba(108, 117, 125, 0.7)'], borderColor: ['rgba(40, 167, 69, 1)', 'rgba(220, 53, 69, 1)', 'rgba(255, 193, 7, 1)', 'rgba(108, 117, 125, 1)'], } ); const sourcePreviewConsistencyData = generateChartData( '源影像预览一致性', { '存在预览缓存': num(source_preview_consistency.preview_exists_count), '预览缓存缺失': num(source_preview_consistency.preview_missing_count), 'DB READY且有缓存': num(source_preview_consistency.db_ready_and_cache_exists_count), 'DB READY但缺缓存': num(source_preview_consistency.db_ready_but_cache_missing_count), }, { backgroundColor: ['rgba(40, 167, 69, 0.7)', 'rgba(220, 53, 69, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 159, 64, 0.7)'], borderColor: ['rgba(40, 167, 69, 1)', 'rgba(220, 53, 69, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 159, 64, 1)'], } ); const sourceXmlConsistencyData = generateChartData( '源影像 XML 读取一致性', { '检测到XML并解析': num(source_xml_consistency.xml_parsed_ok_count), '检测到XML未解析': num(source_xml_consistency.xml_detected_but_unparsed_count), '缺少XML': num(source_xml_consistency.xml_missing_count), }, { backgroundColor: ['rgba(40, 167, 69, 0.7)', 'rgba(255, 193, 7, 0.7)', 'rgba(220, 53, 69, 0.7)'], borderColor: ['rgba(40, 167, 69, 1)', 'rgba(255, 193, 7, 1)', 'rgba(220, 53, 69, 1)'], } ); const issues = []; if (num(dinsar_cache_consistency.db_cached_but_file_missing_count) > 0) { issues.push({ level: 'error', text: `D-InSAR: 数据库标记已缓存但文件缺失 ${num(dinsar_cache_consistency.db_cached_but_file_missing_count)} 条` }); } if (num(dinsar_cache_consistency.db_uncached_but_file_exists_count) > 0) { issues.push({ level: 'warn', text: `D-InSAR: 数据库未标记缓存但文件已存在 ${num(dinsar_cache_consistency.db_uncached_but_file_exists_count)} 条` }); } if (num(dinsar_cache_consistency.manifest_missing_file_count) > 0) { issues.push({ level: 'warn', text: `D-InSAR: manifest 引用缺失文件 ${num(dinsar_cache_consistency.manifest_missing_file_count)} 条` }); } if (num(source_preview_consistency.db_ready_but_cache_missing_count) > 0) { issues.push({ level: 'error', text: `源影像: DB READY 但预览缓存缺失 ${num(source_preview_consistency.db_ready_but_cache_missing_count)} 条` }); } if (num(source_xml_consistency.xml_detected_but_unparsed_count) > 0) { issues.push({ level: 'warn', text: `源影像: 检测到 XML 但关键字段未入库 ${num(source_xml_consistency.xml_detected_but_unparsed_count)} 条` }); } if (num(source_xml_consistency.xml_missing_count) > 0) { issues.push({ level: 'warn', text: `源影像: 未检测到 XML ${num(source_xml_consistency.xml_missing_count)} 条` }); } return (

数据统计仪表盘

D-InSAR总数
{num(dinsar_results_overview.total_count)}
D-InSAR缓存文件存在
{num(dinsar_cache_consistency.cache_file_exists_count)}
源影像预览存在
{num(source_preview_consistency.preview_exists_count)}
XML解析成功
{num(source_xml_consistency.xml_parsed_ok_count)}
{issues.length === 0 ? (
一致性检查通过,当前未发现异常项。
) : ( issues.map((item, index) => (
{item.text}
)) )}
{num(source_data_overview.total_count) > 0 && }
{Object.keys(by_satellite).length > 0 && }
{num(source_data_overview.total_count) > 0 && }
); }; export default StatisticsDashboard;