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 (
);
}
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;