Improve pairing planning and statistics visibility

This commit is contained in:
2026-07-07 01:04:47 +08:00
parent 2da2121829
commit 36a406ae49
22 changed files with 2085 additions and 459 deletions
+133 -27
View File
@@ -295,7 +295,11 @@ function buildPipelineOption(rows) {
axisPointer: { type: 'shadow' },
formatter: (params) => {
const item = params?.[0]?.data || {};
return `${item.label}<br/>数量:${formatNumber(item.value)}<br/>比例:${formatPercent(item.rate)}`;
const denominator = Number(item.denominator);
const denominatorText = Number.isFinite(denominator) && denominator > 0
? `<br/>适用总数:${formatNumber(denominator)}`
: '';
return `${item.label}<br/>数量:${formatNumber(item.value)}${denominatorText}<br/>比例:${formatPercent(item.rate)}`;
},
},
xAxis: {
@@ -327,7 +331,13 @@ function buildPipelineOption(rows) {
show: true,
position: 'right',
color: '#475569',
formatter: (params) => `${formatNumber(params.data.value)} / ${formatPercent(params.data.rate)}`,
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)}`;
},
},
},
],
@@ -743,6 +753,87 @@ function FamilyLegend({ rows }) {
);
}
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 (
<div className="statistics-orbit-gap statistics-orbit-gap--ok">
<strong>精轨缺口</strong>
<span>当前没有开放的缺精轨场景。</span>
</div>
);
}
return (
<div className="statistics-orbit-gap">
<div className="statistics-orbit-gap-head">
<div>
<strong>精轨缺口说明</strong>
<span>缺轨按受影响影像景数统计;补齐时按卫星日期准备 LT-1 TXT 精轨。</span>
</div>
<div className="statistics-orbit-gap-metrics">
<div>
<span>受影响景数</span>
<b>{formatNumber(affectedSceneCount)}</b>
</div>
<div>
<span>缺失卫星日期</span>
<b>{formatNumber(missingDateCount)}</b>
</div>
</div>
</div>
<div className="statistics-orbit-gap-list">
{bySatellite.map((item) => {
const months = Array.isArray(item.months) ? item.months : [];
const dates = Array.isArray(item.dates) ? item.dates : [];
return (
<div className="statistics-orbit-gap-satellite" key={item.satellite}>
<div className="statistics-orbit-gap-satellite-head">
<strong>{item.satellite || 'UNKNOWN'}</strong>
<span>
{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)}`
: ''}
</span>
</div>
<div className="statistics-orbit-month-grid">
{months.map((month) => (
<span key={`${item.satellite}-${month.month}`}>
{month.month}
<b>{formatNumber(month.affected_scene_count)}景</b>
<em>{formatNumber(month.missing_orbit_date_count)}日</em>
</span>
))}
</div>
<div className="statistics-orbit-date-row" aria-label={`${item.satellite} 缺失精轨日期`}>
{dates.map((date) => (
<span key={`${item.satellite}-${date.date}`} title={`${formatNumber(date.affected_scene_count)} 景受影响`}>
{formatYmd(date.date)}
<b>{formatNumber(date.affected_scene_count)}</b>
</span>
))}
</div>
</div>
);
})}
</div>
</div>
);
}
function ProductionRunList({ rows }) {
const data = Array.isArray(rows) ? rows : [];
if (!data.length) {
@@ -792,11 +883,17 @@ export default function StatisticsDashboard() {
const [lastLoadedAt, setLastLoadedAt] = useState(null);
const [coverageMode, setCoverageMode] = useState('source');
const loadDashboard = async () => {
const loadDashboard = async ({ fresh = false } = {}) => {
setLoading(true);
setError('');
try {
const payload = await getStatisticsDashboard();
let payload;
try {
payload = await getStatisticsDashboard(fresh);
} catch (err) {
if (!fresh) throw err;
payload = await getStatisticsDashboard(false);
}
setData(payload);
setLastLoadedAt(new Date());
} catch (err) {
@@ -875,7 +972,7 @@ export default function StatisticsDashboard() {
{data.cache_meta.ttl_seconds ? ` · ${data.cache_meta.ttl_seconds}秒缓存` : ''}
</span>
)}
<button type="button" onClick={loadDashboard} disabled={loading}>
<button type="button" onClick={() => loadDashboard({ fresh: true })} disabled={loading}>
{loading ? '刷新中' : '刷新'}
</button>
</div>
@@ -948,29 +1045,38 @@ export default function StatisticsDashboard() {
<Chart option={sourceTrendOption} className="statistics-chart-md" />
</Section>
<Section title="精轨保障" subtitle={`${formatNumber(data?.orbit?.selected_bindings)} 景已选中`}>
<div className="statistics-orbit-summary">
<div>
<span>精轨资产</span>
<strong>{formatNumber(data?.orbit?.orbit_total)}</strong>
</div>
<div>
<span>绑定覆盖率</span>
<strong>{formatPercent(data?.orbit?.selected_rate)}</strong>
</div>
<div>
<span>匹配记录</span>
<strong>{formatNumber(data?.orbit?.matched_bindings)}</strong>
</div>
</div>
<div className="statistics-mini-bars">
{(data?.orbit?.orbit_by_family || []).map((item) => (
<div key={item.family}>
<span>{item.family}</span>
<b>{formatNumber(item.count)}</b>
<i style={{ width: `${Math.min(100, Math.max(2, item.ok_rate * 100))}%` }} />
<Section
title="精轨保障"
subtitle={`${formatNumber(data?.orbit?.selected_bindings)} 景已选中,${formatNumber(data?.orbit?.missing_summary?.affected_scene_count || data?.orbit?.missing_scene_count)} 景待补`}
className="statistics-section--orbit"
>
<div className="statistics-orbit-layout">
<div className="statistics-orbit-overview">
<div className="statistics-orbit-summary">
<div>
<span>精轨资产</span>
<strong>{formatNumber(data?.orbit?.orbit_total)}</strong>
</div>
<div>
<span>绑定覆盖率</span>
<strong>{formatPercent(data?.orbit?.selected_rate)}</strong>
</div>
<div>
<span>匹配记录</span>
<strong>{formatNumber(data?.orbit?.matched_bindings)}</strong>
</div>
</div>
))}
<div className="statistics-mini-bars">
{(data?.orbit?.orbit_by_family || []).map((item) => (
<div key={item.family}>
<span>{item.family}</span>
<b>{formatNumber(item.count)}</b>
<i style={{ width: `${Math.min(100, Math.max(2, item.ok_rate * 100))}%` }} />
</div>
))}
</div>
</div>
<MissingOrbitSummary summary={data?.orbit?.missing_summary} />
</div>
</Section>