import { useEffect, useMemo, useRef, useState } from 'react'; import { BarChart, HeatmapChart, LineChart, MapChart, ScatterChart, } from 'echarts/charts'; import { GeoComponent, GridComponent, LegendComponent, TooltipComponent, VisualMapComponent, } from 'echarts/components'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; import { getStatisticsDashboard } from './api/stats'; echarts.use([ BarChart, HeatmapChart, LineChart, MapChart, ScatterChart, GeoComponent, GridComponent, LegendComponent, TooltipComponent, VisualMapComponent, CanvasRenderer, ]); const CITY_COVERAGE_MAP_NAME = 'insar-city-coverage'; const FAMILY_COLORS = { 'LT-1': '#2563eb', 'Sentinel-1': '#0f766e', GF3: '#d97706', 未分类: '#64748b', }; const STATUS_COLORS = { READY: '#16a34a', OK: '#16a34a', COMPLETED: '#16a34a', SUCCESS: '#16a34a', RUNNING: '#0ea5e9', PENDING: '#f59e0b', FAILED: '#dc2626', ERROR: '#dc2626', WARNING: '#f59e0b', WARN: '#f59e0b', UNKNOWN: '#64748b', }; function formatNumber(value) { const number = Number(value); if (!Number.isFinite(number)) return '-'; return new Intl.NumberFormat('zh-CN').format(number); } function formatPercent(rate) { const number = Number(rate); if (!Number.isFinite(number)) return '-'; return `${(number * 100).toFixed(1)}%`; } function formatDuration(seconds) { const number = Number(seconds); if (!Number.isFinite(number) || number <= 0) return '-'; if (number < 60) return `${Math.round(number)} 秒`; if (number < 3600) return `${Math.round(number / 60)} 分钟`; return `${(number / 3600).toFixed(1)} 小时`; } function formatDateTime(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } function colorForStatus(status) { return STATUS_COLORS[String(status || '').toUpperCase()] || '#64748b'; } function colorForFamily(family) { return FAMILY_COLORS[family] || '#64748b'; } function colorForCoverageCount(count, maxCount) { const value = Number(count) || 0; const max = Math.max(Number(maxCount) || 1, 1); const rate = value / max; if (rate >= 0.82) return '#08306b'; if (rate >= 0.62) return '#1261b4'; if (rate >= 0.42) return '#2f7ed8'; if (rate >= 0.24) return '#5aa9f4'; if (rate >= 0.1) return '#9bd3ff'; return '#dbeeff'; } function isFeatureCollection(features) { return features?.type === 'FeatureCollection' && Array.isArray(features.features) && features.features.length > 0; } function registerCityCoverageMap(features) { if (!isFeatureCollection(features)) return false; echarts.registerMap(CITY_COVERAGE_MAP_NAME, features); return true; } function buildCityCoverageGrid(cityCoverage, mode = 'source') { const payload = cityCoverage || {}; const scope = mode === 'results' ? payload.results : payload.source; const features = Array.isArray(payload.features?.features) ? payload.features.features : []; const regions = Array.isArray(scope?.regions) ? scope.regions : []; if (!features.length || !regions.length) return null; const countByTree = new Map(regions.map((item) => [String(item.tree_id), item])); return { source_type: 'city_regions', total: scope.total, covered_count: scope.matched_count, cell_count: regions.length, max_count: scope.max_count, extent: {}, features: payload.features, cells: regions.map((region) => ({ tree_id: region.tree_id, name: region.name, count: region.count, lon: region.lon, lat: region.lat, breakdown: region.breakdown || [], meta: countByTree.get(String(region.tree_id)), })), }; } function Chart({ option, className = '', emptyText = '暂无数据' }) { const containerRef = useRef(null); const chartRef = useRef(null); const [renderError, setRenderError] = useState(''); useEffect(() => { if (!containerRef.current) return undefined; const chart = echarts.init(containerRef.current, null, { renderer: 'canvas' }); chartRef.current = chart; let resizeObserver = null; if (typeof ResizeObserver !== 'undefined') { resizeObserver = new ResizeObserver(() => chart.resize()); resizeObserver.observe(containerRef.current); } const onResize = () => chart.resize(); window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); resizeObserver?.disconnect(); chart.dispose(); chartRef.current = null; }; }, []); useEffect(() => { if (!chartRef.current) return; if (option) { try { chartRef.current.setOption(option, true); setRenderError(''); } catch (err) { console.error('Statistics chart render failed', err); chartRef.current.clear(); setRenderError(err?.message || 'chart render failed'); } } else { setRenderError(''); chartRef.current.clear(); } }, [option]); return (
{!option &&
{emptyText}
} {renderError &&
图表渲染失败,请刷新或检查统计数据
}
); } function buildSourceOption(rows) { const data = Array.isArray(rows) ? rows : []; if (!data.length) return null; return { animation: false, color: data.map((item) => colorForFamily(item.family)), grid: { left: 36, right: 12, top: 26, bottom: 28 }, tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' }, formatter: (params) => { const item = params?.[0]?.data || {}; return `${item.family}
资产:${formatNumber(item.count)} 景
解析可用:${formatPercent(item.ready_rate)}`; }, }, xAxis: { type: 'category', data: data.map((item) => item.family), axisTick: { show: false }, axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { color: '#475569', fontWeight: 700 }, }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#edf2f7' } }, axisLabel: { color: '#64748b' }, }, series: [ { type: 'bar', barMaxWidth: 42, data: data.map((item) => ({ ...item, value: item.count, itemStyle: { color: colorForFamily(item.family), borderRadius: [4, 4, 0, 0] }, })), }, ], }; } function buildTrendOption(rows, familyNames) { const data = Array.isArray(rows) ? rows : []; if (!data.length) return null; const families = familyNames?.length ? familyNames : Array.from(new Set(data.flatMap((item) => Object.keys(item.by_family || {})))); return { animation: false, color: families.map(colorForFamily), legend: { top: 0, right: 0, itemWidth: 12, itemHeight: 8, textStyle: { color: '#475569', fontSize: 11 }, }, grid: { left: 42, right: 12, top: 34, bottom: 28 }, tooltip: { trigger: 'axis', confine: true }, xAxis: { type: 'category', boundaryGap: false, data: data.map((item) => item.month), axisTick: { show: false }, axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { color: '#64748b' }, }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#edf2f7' } }, axisLabel: { color: '#64748b' }, }, series: families.map((family) => ({ name: family, type: 'line', smooth: true, symbolSize: 5, lineStyle: { width: 2 }, areaStyle: { opacity: 0.08 }, data: data.map((item) => item.by_family?.[family] || 0), })), }; } function buildPipelineOption(rows) { const data = Array.isArray(rows) ? rows : []; if (!data.length) return null; return { animation: false, grid: { left: 82, right: 38, top: 8, bottom: 10 }, tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' }, formatter: (params) => { const item = params?.[0]?.data || {}; const denominator = Number(item.denominator); const denominatorText = Number.isFinite(denominator) && denominator > 0 ? `
适用总数:${formatNumber(denominator)}` : ''; return `${item.label}
数量:${formatNumber(item.value)}${denominatorText}
比例:${formatPercent(item.rate)}`; }, }, xAxis: { type: 'value', max: Math.max(...data.map((item) => Number(item.value) || 0), 1), splitLine: { lineStyle: { color: '#edf2f7' } }, axisLabel: { color: '#64748b' }, }, yAxis: { type: 'category', inverse: true, data: data.map((item) => item.label), axisTick: { show: false }, axisLine: { show: false }, axisLabel: { color: '#334155', fontWeight: 700 }, }, series: [ { type: 'bar', barWidth: 14, data: data.map((item, index) => ({ ...item, itemStyle: { color: ['#2563eb', '#0f766e', '#0891b2', '#16a34a', '#d97706'][index % 5], borderRadius: [0, 5, 5, 0], }, })), label: { show: true, position: 'right', color: '#475569', formatter: (params) => { const denominator = Number(params.data.denominator); const totalText = Number.isFinite(denominator) && denominator > 0 ? `/${formatNumber(denominator)}` : ''; return `${formatNumber(params.data.value)}${totalText} / ${formatPercent(params.data.rate)}`; }, }, }, ], }; } function buildLegacyPointCoverageGrid(points, extent) { const source = Array.isArray(points) ? points .map((item) => ({ ...item, lon: Number(item.lon), lat: Number(item.lat), })) .filter((item) => Number.isFinite(item.lon) && Number.isFinite(item.lat)) : []; if (!source.length) return null; const extentMinLon = Number(extent?.min_lon); const extentMaxLon = Number(extent?.max_lon); const extentMinLat = Number(extent?.min_lat); const extentMaxLat = Number(extent?.max_lat); const minLon = Number.isFinite(extentMinLon) ? extentMinLon : Math.min(...source.map((item) => item.lon)); const maxLon = Number.isFinite(extentMaxLon) ? extentMaxLon : Math.max(...source.map((item) => item.lon)); const minLat = Number.isFinite(extentMinLat) ? extentMinLat : Math.min(...source.map((item) => item.lat)); const maxLat = Number.isFinite(extentMaxLat) ? extentMaxLat : Math.max(...source.map((item) => item.lat)); const lonSpan = Math.max(maxLon - minLon, 0.01); const latSpan = Math.max(maxLat - minLat, 0.01); const columns = 42; const rows = Math.max(14, Math.min(34, Math.round((columns * latSpan) / lonSpan))); const cellLon = lonSpan / columns; const cellLat = latSpan / rows; const buckets = new Map(); source.forEach((item) => { const col = Math.min(columns - 1, Math.max(0, Math.floor(((item.lon - minLon) / lonSpan) * columns))); const row = Math.min(rows - 1, Math.max(0, Math.floor(((item.lat - minLat) / latSpan) * rows))); const key = `${col}:${row}`; const family = item.family || '未分类'; const bucket = buckets.get(key) || { col, row, count: 0, families: {}, examples: [] }; bucket.count += 1; bucket.families[family] = (bucket.families[family] || 0) + 1; if (bucket.examples.length < 4) { bucket.examples.push({ family, label: item.satellite || item.source_format, date: item.date, }); } buckets.set(key, bucket); }); const cells = Array.from(buckets.values()).map((bucket) => { const dominantFamily = Object.entries(bucket.families).sort((a, b) => b[1] - a[1])[0]?.[0] || '未分类'; return { col: bucket.col, row: bucket.row, count: bucket.count, lon_min: minLon + bucket.col * cellLon, lon_max: minLon + (bucket.col + 1) * cellLon, lat_min: minLat + bucket.row * cellLat, lat_max: minLat + (bucket.row + 1) * cellLat, lon: minLon + (bucket.col + 0.5) * cellLon, lat: minLat + (bucket.row + 0.5) * cellLat, dominant_family: dominantFamily, families: Object.entries(bucket.families).map(([name, count]) => ({ name, count })), examples: bucket.examples, }; }); return { source_type: 'legacy_points', total: source.length, covered_count: source.length, cell_count: cells.length, max_count: Math.max(...cells.map((cell) => cell.count), 1), columns, rows, extent: { min_lon: minLon, min_lat: minLat, max_lon: maxLon, max_lat: maxLat }, cells, }; } function buildCoverageOption(grid, mode = 'source') { if (grid?.source_type === 'city_regions') { if (!registerCityCoverageMap(grid.features)) return null; const unit = mode === 'results' ? '项成果' : '景源数据'; const ownerLabel = mode === 'results' ? '成果类型' : '数据源'; const maxCount = Math.max(Number(grid?.max_count) || 0, ...grid.cells.map((item) => Number(item.count) || 0), 1); const heatData = grid.cells .map((region) => { const lon = Number(region.lon); const lat = Number(region.lat); const count = Number(region.count) || 0; if (!Number.isFinite(lon) || !Number.isFinite(lat) || count <= 0) return null; return { name: region.name, value: [lon, lat, count], tree_id: region.tree_id, breakdown: region.breakdown || [], }; }) .filter(Boolean); return { animation: false, tooltip: { trigger: 'item', confine: true, formatter: ({ data }) => { const value = Array.isArray(data?.value) ? Number(data.value[2]) : Number(data?.value); if (!data || !Number.isFinite(value) || value <= 0) { return `${data?.name || '未命中行政区'}
暂无${unit}`; } const breakdown = Array.isArray(data.breakdown) && data.breakdown.length ? `
${ownerLabel}:${data.breakdown.map((item) => `${item.name} ${formatNumber(item.count)}`).join(',')}` : ''; return `${data.name}
${formatNumber(value)} ${unit}${breakdown}`; }, }, visualMap: { show: true, type: 'continuous', min: 0, max: maxCount, orient: 'vertical', right: 8, bottom: 10, calculable: false, itemWidth: 12, itemHeight: 78, textStyle: { color: '#475569', fontSize: 11 }, inRange: { color: ['#dbeafe', '#93c5fd', '#38bdf8', '#22c55e', '#facc15', '#f97316', '#dc2626'], }, }, geo: { map: CITY_COVERAGE_MAP_NAME, roam: false, silent: true, layoutCenter: ['50%', '50%'], layoutSize: '96%', itemStyle: { areaColor: '#f8fafc', borderColor: '#cbd5e1', borderWidth: 0.8, }, emphasis: { disabled: true, }, }, series: [ { name: mode === 'results' ? '成果热度' : '源数据热度', type: 'heatmap', coordinateSystem: 'geo', pointSize: mode === 'results' ? 34 : 28, blurSize: mode === 'results' ? 42 : 36, minOpacity: 0.18, maxOpacity: 0.92, data: heatData, }, { name: mode === 'results' ? '成果命中市' : '源数据命中市', type: 'scatter', coordinateSystem: 'geo', symbolSize: (value) => { const count = Number(value?.[2]) || 0; return Math.max(5, Math.min(15, 5 + (count / maxCount) * 10)); }, itemStyle: { color: 'rgba(15, 23, 42, 0.72)', borderColor: '#ffffff', borderWidth: 1, }, label: { show: true, position: 'right', color: '#0f172a', fontSize: 11, fontWeight: 800, formatter: ({ data }) => (Number(data?.value?.[2]) >= maxCount * 0.28 ? data.name : ''), }, emphasis: { label: { show: true }, itemStyle: { color: '#0f172a' }, }, data: heatData, }, ], }; } const cells = Array.isArray(grid?.cells) ? grid.cells : []; const extent = grid?.extent || {}; if (!cells.length) return null; const minLon = Number(extent.min_lon); const maxLon = Number(extent.max_lon); const minLat = Number(extent.min_lat); const maxLat = Number(extent.max_lat); if (![minLon, maxLon, minLat, maxLat].every(Number.isFinite)) return null; const lonSpan = Math.max(maxLon - minLon, 0.01); const latSpan = Math.max(maxLat - minLat, 0.01); const lonPadding = lonSpan * 0.04; const latPadding = latSpan * 0.04; const maxCount = Math.max(Number(grid?.max_count) || 0, ...cells.map((item) => Number(item.count) || 0), 1); const unit = mode === 'results' ? '项成果' : '景源数据'; const ownerLabel = mode === 'results' ? '主成果类型' : '主数据源'; const data = cells.map((cell) => ({ value: [ Number(cell.lon_min), Number(cell.lat_min), Number(cell.lon_max), Number(cell.lat_max), Number(cell.count) || 0, ], meta: cell, })); return { animation: false, grid: { left: 8, right: 8, top: 8, bottom: 8 }, tooltip: { trigger: 'item', confine: true, formatter: ({ data: item }) => { const meta = item?.meta; if (!meta) return ''; const owner = mode === 'results' ? meta.dominant_catalog : meta.dominant_family; return [ `${formatNumber(meta.count)} ${unit}`, owner ? `${ownerLabel}:${owner}` : '', `经度:${Number(meta.lon_min).toFixed(3)} - ${Number(meta.lon_max).toFixed(3)}`, `纬度:${Number(meta.lat_min).toFixed(3)} - ${Number(meta.lat_max).toFixed(3)}`, meta.examples.length ? `样例:${meta.examples.map((example) => `${example.family || example.catalog || ''} ${example.date || ''}`.trim()).filter(Boolean).join(',')}` : '', ].filter(Boolean).join('
'); }, }, xAxis: { type: 'value', min: minLon - lonPadding, max: maxLon + lonPadding, show: false, }, yAxis: { type: 'value', min: minLat - latPadding, max: maxLat + latPadding, show: false, scale: true, }, series: [ { name: '覆盖密度', type: 'custom', renderItem: (params, api) => { const lonMin = api.value(0); const latMin = api.value(1); const lonMax = api.value(2); const latMax = api.value(3); const count = api.value(4); const topLeft = api.coord([lonMin, latMax]); const bottomRight = api.coord([lonMax, latMin]); const rect = echarts.graphic.clipRectByRect( { x: topLeft[0], y: topLeft[1], width: Math.max(bottomRight[0] - topLeft[0], 1.5), height: Math.max(bottomRight[1] - topLeft[1], 1.5), }, { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height, }, ); if (!rect) return null; return { type: 'rect', shape: rect, style: { fill: colorForCoverageCount(count, maxCount), stroke: 'rgba(255,255,255,0.72)', lineWidth: 0.6, }, emphasis: { style: { stroke: '#0f172a', lineWidth: 1.2, }, }, }; }, data, }, ], }; } function buildStatusOption(rows, key = 'status') { const data = Array.isArray(rows) ? rows : []; if (!data.length) return null; return { animation: false, grid: { left: 80, right: 26, top: 8, bottom: 12 }, tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, xAxis: { type: 'value', splitLine: { lineStyle: { color: '#edf2f7' } }, axisLabel: { color: '#64748b' }, }, yAxis: { type: 'category', inverse: true, data: data.map((item) => item[key]), axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: '#334155', fontWeight: 700 }, }, series: [ { type: 'bar', barWidth: 14, data: data.map((item) => ({ value: item.count, itemStyle: { color: colorForStatus(item[key]), borderRadius: [0, 5, 5, 0] }, })), label: { show: true, position: 'right', color: '#475569' }, }, ], }; } function buildResultTrendOption(rows) { const data = Array.isArray(rows) ? rows : []; if (!data.length) return null; const catalogs = Array.from(new Set(data.flatMap((item) => Object.keys(item.by_catalog || {})))); const colors = ['#2563eb', '#16a34a', '#d97706', '#7c3aed']; return { animation: false, color: colors, legend: { top: 0, right: 0, itemWidth: 12, itemHeight: 8, textStyle: { color: '#475569', fontSize: 11 }, }, grid: { left: 42, right: 12, top: 34, bottom: 28 }, tooltip: { trigger: 'axis', confine: true }, xAxis: { type: 'category', data: data.map((item) => item.month), axisTick: { show: false }, axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { color: '#64748b' }, }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#edf2f7' } }, axisLabel: { color: '#64748b' }, }, series: catalogs.map((catalog) => ({ name: catalog === 'sbas_insar' ? 'SBAS' : catalog.toUpperCase(), type: 'bar', stack: 'result', barMaxWidth: 28, data: data.map((item) => item.by_catalog?.[catalog] || 0), })), }; } function KpiCard({ item }) { return (
{item.label}
{formatNumber(item.value)} {item.unit}
{item.note}
); } function Section({ title, subtitle, className = '', children }) { return (

{title}

{subtitle && {subtitle}}
{children}
); } function FamilyLegend({ rows }) { const data = Array.isArray(rows) ? rows : []; return (
{data.map((item) => (
{item.family} {formatNumber(item.count)} {formatPercent(item.ready_rate)} 可用
))}
); } function formatYmd(value) { const text = String(value || '').trim(); if (/^\d{8}$/.test(text)) { return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`; } return text || '-'; } function MissingOrbitSummary({ summary }) { const affectedSceneCount = Number(summary?.affected_scene_count || 0); const missingDateCount = Number(summary?.missing_orbit_date_count || 0); const bySatellite = Array.isArray(summary?.by_satellite) ? summary.by_satellite : []; if (!affectedSceneCount) { return (
精轨缺口 当前没有开放的缺精轨场景。
); } return (
精轨缺口说明 缺轨按受影响影像景数统计;补齐时按卫星日期准备 LT-1 TXT 精轨。
受影响景数 {formatNumber(affectedSceneCount)}
缺失卫星日期 {formatNumber(missingDateCount)}
{bySatellite.map((item) => { const months = Array.isArray(item.months) ? item.months : []; const dates = Array.isArray(item.dates) ? item.dates : []; return (
{item.satellite || 'UNKNOWN'} {formatNumber(item.affected_scene_count)} 景 / {formatNumber(item.missing_orbit_date_count)} 个日期 {item.first_missing_date && item.last_missing_date ? ` / ${formatYmd(item.first_missing_date)} 至 ${formatYmd(item.last_missing_date)}` : ''}
{months.map((month) => ( {month.month} {formatNumber(month.affected_scene_count)}景 {formatNumber(month.missing_orbit_date_count)}日 ))}
{dates.map((date) => ( {formatYmd(date.date)} {formatNumber(date.affected_scene_count)} ))}
); })}
); } function ProductionRunList({ rows }) { const data = Array.isArray(rows) ? rows : []; if (!data.length) { return
暂无生产运行记录
; } return (
{data.map((item) => (
{item.run_id} {item.engine_code} · {formatDateTime(item.created_at)}
{item.status} {formatNumber(item.completed_items)}/{formatNumber(item.total_items)}
))}
); } function InventoryStateList({ rows }) { const data = Array.isArray(rows) ? rows : []; if (!data.length) { return
暂无扫描状态记录
; } return (
{data.slice(0, 6).map((item) => (
{item.inventory_type || '未分类扫描'} {formatDateTime(item.last_scan_finished_at || item.last_scan_started_at)}
{item.status} {formatNumber(item.last_asset_count)} 项
))}
); } export default function StatisticsDashboard() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [lastLoadedAt, setLastLoadedAt] = useState(null); const [coverageMode, setCoverageMode] = useState('source'); const loadDashboard = async ({ fresh = false } = {}) => { setLoading(true); setError(''); try { let payload; try { payload = await getStatisticsDashboard(fresh); } catch (err) { if (!fresh) throw err; payload = await getStatisticsDashboard(false); } setData(payload); setLastLoadedAt(new Date()); } catch (err) { setError(err.response?.data?.detail || err.message || '统计数据获取失败'); } finally { setLoading(false); } }; useEffect(() => { void loadDashboard(); }, []); const families = useMemo( () => (data?.asset?.source_by_family || []).map((item) => item.family), [data], ); const sourceOption = useMemo( () => buildSourceOption(data?.asset?.source_by_family), [data], ); const sourceTrendOption = useMemo( () => buildTrendOption(data?.asset?.source_by_month, families), [data, families], ); const pipelineOption = useMemo( () => buildPipelineOption(data?.asset?.pipeline), [data], ); const coverageOption = useMemo( () => buildCoverageOption( buildCityCoverageGrid(data?.coverage?.city_regions, coverageMode) || (coverageMode === 'results' ? data?.coverage?.results : data?.coverage?.source), coverageMode, ), [data, coverageMode], ); const productionStatusOption = useMemo( () => buildStatusOption(data?.production?.run_status), [data], ); const taskStatusOption = useMemo( () => buildStatusOption(data?.production?.dinsar_task_status), [data], ); const resultTrendOption = useMemo( () => buildResultTrendOption(data?.results?.results_by_month), [data], ); const issueCodeOption = useMemo( () => buildStatusOption(data?.issues?.by_code, 'code'), [data], ); const activeCoverage = buildCityCoverageGrid(data?.coverage?.city_regions, coverageMode) || (coverageMode === 'results' ? data?.coverage?.results : data?.coverage?.source); const coverageSubtitle = coverageMode === 'results' ? `按市级行政区统计,命中 ${formatNumber(activeCoverage?.cell_count)} 个市,已定位 ${formatNumber(activeCoverage?.covered_count)} / ${formatNumber(activeCoverage?.total)} 项` : `按市级行政区统计,命中 ${formatNumber(activeCoverage?.cell_count)} 个市,已定位 ${formatNumber(activeCoverage?.covered_count)} / ${formatNumber(activeCoverage?.total || data?.coverage?.point_total)} 景`; return (

InSAR 数据与生产统计

汇总源数据接入、元数据解析、精轨保障、生产运行和成果健康状态,用于判断系统工程能力和待处理风险。

{data?.generated_at ? `统计时间 ${formatDateTime(data.generated_at)}` : '等待统计数据'} {data?.cache_meta?.enabled && ( {data.cache_meta.hit ? '缓存命中' : '已重算'} {data.cache_meta.ttl_seconds ? ` · ${data.cache_meta.ttl_seconds}秒缓存` : ''} )}
{error && (
{error}
)} {loading && !data ? (
{Array.from({ length: 8 }).map((_, index) => (
))}
) : ( <>
{(data?.kpis || []).map((item) => ( ))}

{coverageMode === 'results' ? '成果市级覆盖热力' : '源数据市级覆盖热力'}

{coverageSubtitle}
{coverageMode === 'source' && }
精轨资产 {formatNumber(data?.orbit?.orbit_total)}
绑定覆盖率 {formatPercent(data?.orbit?.selected_rate)}
匹配记录 {formatNumber(data?.orbit?.matched_bindings)}
{(data?.orbit?.orbit_by_family || []).map((item) => (
{item.family} {formatNumber(item.count)}
))}
页面不自动轮询,避免统计聚合影响生产服务。 {lastLoadedAt && 本地刷新时间 {formatDateTime(lastLoadedAt.toISOString())}}
)}
); }