import React, { useCallback, useEffect, useMemo, useState } from 'react';
import apiClient from '../api/client';
import {
getDinsarCatalogStatus,
getDinsarProductDetail,
getDinsarProductCleanupPlan,
listDinsarProductPairs,
} from '../api/dinsarProducts';
import {
DINSAR_ENGINE_ALL,
buildDinsarEngineOptions,
getDinsarEngineMeta,
} from '../utils/dinsarEngines';
import { formatSatelliteFamilyLabel, inferSatelliteFamilyFromResultLike } from '../utils/satelliteFamily';
const STATUS_TONE_MAP = {
READY: 'ready',
PARTIAL: 'warn',
QUARANTINED: 'error',
WARN: 'warn',
ERROR: 'error',
REBUILDING: 'info',
ready: 'ready',
missing: 'neutral',
failed: 'error',
blocked: 'warn',
legacy: 'neutral',
};
function formatDateTime(value) {
if (!value) return '-';
try {
return new Date(value).toLocaleString();
} catch {
return String(value);
}
}
function formatBytes(value) {
const size = Number(value || 0);
if (!Number.isFinite(size) || size <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let next = size;
let index = 0;
while (next >= 1024 && index < units.length - 1) {
next /= 1024;
index += 1;
}
return `${next.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
function getMessageTone(message) {
return /失败|error|Error|ERROR/.test(String(message || '')) ? 'error' : 'success';
}
function StatusPill({ label, tone = 'neutral' }) {
return {label};
}
function engineStatusTone(status) {
const normalized = String(status || '').toLowerCase();
if (normalized === 'ready') return 'ready';
if (normalized === 'failed') return 'error';
if (normalized === 'blocked') return 'warn';
if (normalized === 'running') return 'info';
return 'neutral';
}
function formatEngineResultStatus(status) {
const normalized = String(status || 'missing').toLowerCase();
if (normalized === 'ready') return '已生产';
if (normalized === 'failed') return '生产失败';
if (normalized === 'missing') return '未生产';
if (normalized === 'blocked') return '不适用';
if (normalized === 'running') return '生产中';
if (normalized === 'legacy') return '历史结果';
return status || '未生产';
}
function buildEngineResultTitle(result = {}) {
const parts = [formatEngineResultStatus(result.status)];
if (result.skip_reason === 'production_failed') {
parts.push('该引擎已有失败生产记录,不是单纯缺失结果。');
}
if (result.production_run_id) {
parts.push(`run=${result.production_run_id}`);
}
if (result.production_error) {
parts.push(String(result.production_error).replace(/\s+/g, ' ').slice(0, 220));
}
if (result.latest_log_path) {
parts.push(`log=${result.latest_log_path}`);
}
return parts.filter(Boolean).join('\n');
}
function MetaField({ label, value, multiline = false }) {
const displayValue = value === null || value === undefined || value === '' ? '-' : value;
return (
{label}
{displayValue}
);
}
export default function DinsarCatalogPanel({
readOnly = false,
compact = false,
}) {
const [catalogStatus, setCatalogStatus] = useState(null);
const [products, setProducts] = useState([]);
const [productPairs, setProductPairs] = useState([]);
const [selectedPairKey, setSelectedPairKey] = useState('');
const [selectedProductId, setSelectedProductId] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);
const [cleanupPlan, setCleanupPlan] = useState(null);
const [cleanupPlanLoading, setCleanupPlanLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
const [actionMessage, setActionMessage] = useState('');
const [engineFilter, setEngineFilter] = useState(DINSAR_ENGINE_ALL);
const [queryDraft, setQueryDraft] = useState('');
const [queryApplied, setQueryApplied] = useState('');
const listLimit = compact ? 8 : 18;
const previewBaseUrl = apiClient.defaults.baseURL || '/api';
const engineOptions = useMemo(
() => buildDinsarEngineOptions([], { includeKnown: true }),
[]
);
const selectedEngineMeta = useMemo(
() => (engineFilter === DINSAR_ENGINE_ALL ? null : getDinsarEngineMeta(engineFilter)),
[engineFilter]
);
useEffect(() => {
if (engineFilter === DINSAR_ENGINE_ALL) return;
if (!engineOptions.some((option) => option.value === engineFilter)) {
setEngineFilter(DINSAR_ENGINE_ALL);
}
}, [engineFilter, engineOptions]);
const loadCatalog = useCallback(async () => {
setLoading(true);
try {
const [statusData, productData] = await Promise.all([
getDinsarCatalogStatus(),
listDinsarProductPairs({
limit: listLimit,
offset: 0,
engine_code: engineFilter === DINSAR_ENGINE_ALL ? undefined : engineFilter,
query: queryApplied || undefined,
}),
]);
setCatalogStatus(statusData);
const nextPairs = Array.isArray(productData?.items) ? productData.items : [];
setProductPairs(nextPairs);
const nextItems = nextPairs
.map((item) => ({
id: item.primary_product_id,
engine_code: item.primary_engine_code,
pair_key: item.pair_key,
}))
.filter((item) => item.id);
setProducts(nextItems);
setSelectedPairKey((current) => {
if (current && nextPairs.some((item) => (item.pair_key || `pair:${item.primary_product_id}`) === current)) {
return current;
}
const first = nextPairs[0];
return first ? (first.pair_key || `pair:${first.primary_product_id}`) : '';
});
setSelectedProductId((current) => {
if (current && nextItems.some((item) => item.id === current)) {
return current;
}
return nextPairs[0]?.primary_product_id ?? null;
});
} catch (error) {
setActionMessage(`结果目录状态加载失败:${error?.response?.data?.detail || error.message}`);
setCatalogStatus(null);
setProducts([]);
setProductPairs([]);
setSelectedPairKey('');
setSelectedProductId(null);
} finally {
setLoading(false);
}
}, [engineFilter, listLimit, queryApplied]);
const loadProductDetail = useCallback(async (productId) => {
if (!productId) {
setSelectedProduct(null);
setCleanupPlan(null);
return;
}
setSelectedProduct(null);
setCleanupPlan(null);
setDetailLoading(true);
try {
const detail = await getDinsarProductDetail(productId);
setSelectedProduct(detail);
} catch (error) {
setSelectedProduct({
error: error?.response?.data?.detail || error.message || '结果详情加载失败',
});
} finally {
setDetailLoading(false);
}
}, []);
const handleSelectProduct = useCallback((pairKey, productId) => {
setSelectedPairKey(pairKey);
setSelectedProductId((current) => (current === productId ? current : productId || null));
}, []);
const loadCleanupPlan = useCallback(async () => {
if (!selectedProductId) return;
setCleanupPlanLoading(true);
try {
const plan = await getDinsarProductCleanupPlan(selectedProductId);
setCleanupPlan(plan);
} catch (error) {
setCleanupPlan({
error: error?.response?.data?.detail || error.message || '中间文件清理计划加载失败',
});
} finally {
setCleanupPlanLoading(false);
}
}, [selectedProductId]);
useEffect(() => {
loadCatalog();
}, [loadCatalog]);
useEffect(() => {
loadProductDetail(selectedProductId);
}, [loadProductDetail, selectedProductId]);
const handleApplyFilters = useCallback(() => {
setQueryApplied(queryDraft.trim());
}, [queryDraft]);
const handleResetFilters = useCallback(() => {
setEngineFilter(DINSAR_ENGINE_ALL);
setQueryDraft('');
setQueryApplied('');
}, []);
const catalogTone = STATUS_TONE_MAP[catalogStatus?.status] || 'neutral';
const actionTone = getMessageTone(actionMessage);
const selectedIssues = Array.isArray(selectedProduct?.issues) ? selectedProduct.issues : [];
const selectedAssets = Array.isArray(selectedProduct?.assets) ? selectedProduct.assets : [];
const selectedPairingTrace = selectedProduct?.pairing_trace || null;
const selectedPairingNetwork = selectedProduct?.pairing_network || null;
const selectedPairingRun = selectedPairingNetwork?.run || null;
const selectedPairingEdge = selectedPairingNetwork?.edge || null;
const selectedPairingMetric = selectedPairingNetwork?.metric || null;
const selectedProductEngine = getDinsarEngineMeta(selectedProduct?.engine_code);
const selectedStatusTone = STATUS_TONE_MAP[selectedProduct?.status] || 'neutral';
return (
{compact ? '结果目录状态' : '标准结果包目录'}
{compact
? '查看结果包目录与数据库索引是否一致。'
: '统一结果目录按 engine + pair + run 管理,便于同一对影像保留多套生产结果并行对比。'}
{selectedEngineMeta && (
{selectedEngineMeta.shortLabel}
)}
目录状态
{catalogStatus?.status || '未知'}
需要重建
{catalogStatus?.needs_rebuild ? '是' : '否'}
{catalogStatus?.needs_rebuild ? 'Manifest 与数据库存在漂移' : '目录登记正常'}
Manifest / 数据库
{catalogStatus?.manifest_count ?? 0} / {catalogStatus?.db_count ?? 0}
已登记结果包总量
问题数量
{catalogStatus?.issue_count ?? 0}
含缺失文件与健康异常
结果包根目录:{catalogStatus?.storage_root || '-'}
最近消息:{catalogStatus?.last_message || '-'}
最近全量重建:{formatDateTime(catalogStatus?.last_full_rebuild_at)}
{actionMessage && (
{actionMessage}
)}
结果包详情
查看选中结果的发布信息、配对溯源与资产健康
{!selectedProductId ? (
请选择一个已有结果的任务查看详情。
) : detailLoading || !selectedProduct ? (
正在加载详情...
) : selectedProduct?.error ? (
{selectedProduct.error}
) : (
{(() => {
const satelliteFamily = inferSatelliteFamilyFromResultLike(selectedProduct?.profile || selectedProduct);
return (
{selectedProduct.display_name || selectedProduct.product_id}
{selectedProduct.task_alias || selectedProduct.task_name || '未命名任务'}
{selectedProductEngine.shortLabel}
{satelliteFamily && (
{formatSatelliteFamilyLabel(satelliteFamily)}
)}
);
})()}
配对追踪
{!selectedPairingTrace?.network_run_id ? (
当前结果未携带完整的配对网络追踪信息。
) : (
)}
资产列表 ({selectedAssets.length})
{selectedAssets.length === 0 ? (
暂无资产记录。
) : (
{selectedAssets.map((asset) => (
{asset.asset_role}
{asset.exists_flag ? '文件存在' : '文件缺失'}
{asset.asset_name}
{asset.absolute_path}
))}
)}
问题列表 ({selectedIssues.length})
{selectedIssues.length === 0 ? (
当前没有登记问题。
) : (
{selectedIssues.map((issue) => (
{issue.issue_code}
{issue.severity}
{issue.message}
{issue.repair_action && (
建议修复动作:{issue.repair_action}
)}
))}
)}
中间文件清理计划
{!cleanupPlan ? (
尚未生成清理计划。
) : cleanupPlan.error ? (
{cleanupPlan.error}
) : (
{Array.isArray(cleanupPlan.blockers) && cleanupPlan.blockers.length > 0 && (
{cleanupPlan.blockers.map((blocker) => (
{blocker}
))}
)}
{(cleanupPlan.candidates || []).map((candidate) => (
{candidate.reason}
{candidate.exists ? formatBytes(candidate.size_bytes) : '不存在'}
{candidate.path}
))}
)}
)}
);
}