// dashboard/src/components/observation/ObservationResultCard.tsx // // 观测数据下载结果卡片 —— 可复用渲染 ObservationBatch // // 用途: // 1. 观测数据面板的"检索下载"视图在下载完成后展示结果 // 2. (可选)SpecialToolRenderers 的 FindObservationCard 可改为薄封装调用本组件 // // 视觉与 SpecialToolRenderers::FindObservationCard 保持一致,统一数据源主题色。 import { useState } from 'react'; import { Activity, CheckCircle2, AlertTriangle, Download, LineChart, Loader2, } from 'lucide-react'; import { SOURCE_THEME, PRODUCT_LABEL, formatFileSize, canPreview, type ObservationBatchResult, } from './constants'; import { ObservationPreviewRenderer } from './ObservationPreviewRenderer'; import { useObservationPreview } from './useObservationPreview'; interface ObservationResultCardProps { result: ObservationBatchResult; } export function ObservationResultCard({ result }: ObservationResultCardProps) { const { source, product, ra, dec, radius_deg, matched_count, products = [], failures = [], } = result; const theme = SOURCE_THEME[source] ?? SOURCE_THEME.lamost; const productLabel = PRODUCT_LABEL[product.product] ?? product.product; const hasResult = products.length > 0 || failures.length > 0; // 预览:自包含 hook,面板流 + Agent 流共用(两者都渲染本卡片) const { fetchPreview, getPreview } = useObservationPreview(); const [expanded, setExpanded] = useState>({}); return (
{/* 标题栏:数据源 + 产品类型双标签 */}
观测数据下载
{theme.label} {productLabel} {product.subtype ? ` · ${product.subtype}` : ''}
{/* 查询信息 */}
{ra !== undefined && dec !== undefined && ( ra={ra.toFixed(4)} dec={dec.toFixed(4)} {radius_deg !== undefined ? ` r=${radius_deg}°` : ''} )} 命中 {matched_count} 条 {products.length > 0 && ( 下载 {products.length} )} {failures.length > 0 && ( 失败 {failures.length} )}
{/* 产物列表(每个 product 含 1~N 个 artifact) */} {products.map((p, i) => { const allCached = p.artifacts.length > 0 && p.artifacts.every((a) => a.cached); return (
{p.source_label} {allCached ? ( 缓存 ) : ( 新下载 )}
{/* 多 artifact 展示(Gaia 光变 G/BP/RP 三波段) */} {p.artifacts.map((a, ai) => { const previewKey = `${p.source_id}#${ai}`; const previewable = canPreview(product.product, a.file_format); const isOpen = expanded[previewKey] ?? false; const previewState = getPreview(p.source_id, ai); const togglePreview = () => { const next = !isOpen; setExpanded((m) => ({ ...m, [previewKey]: next })); if (next && !previewState.data && !previewState.loading) { fetchPreview( source, product.product, product.subtype, p.source_id, ai ); } }; return (
{a.band && ( {a.band} )} {a.file_format.toUpperCase()} ·{' '} {formatFileSize(a.size_bytes)} {previewable && ( )} {a.file_format.toUpperCase()}
{/* 预览图(按 product 类型分发渲染) */} {previewable && isOpen && (
{previewState.error ? (

{previewState.error}

) : previewState.data ? ( ) : previewState.loading ? (
解析 FITS 中...
) : null}
)}
); })}
); })} {/* 失败列表 */} {failures.map((f, i) => (
{f.source_label}

{f.error}

))} {!hasResult && (

(该区域无{productLabel}数据覆盖)

)}
); }