import React, { useCallback, useEffect, useState } from 'react'; import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title } from 'chart.js'; import { Doughnut, Bar } from 'react-chartjs-2'; import { getEnviStatus, inspectImport, inspectDinsar, queueImportJob, queueDinsarJob, getRecentRuns, forceCancelTask, extractDispResults, getTaskOverview, getJobLog, deleteRun, } from './api/idl'; import TaskStatusPanel from './components/tasks/TaskStatusPanel'; import useTaskMonitor from './hooks/useTaskMonitor'; import { getStatistics } from './api/stats'; ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title); const cardStyle = { background: '#fff', padding: '12px', borderRadius: '8px', border: '1px solid #e2e8f0', marginBottom: '12px', }; function IDLAutomationPanel({ readOnly = false, onJobQueued }) { const [status, setStatus] = useState(null); const [recentRuns, setRecentRuns] = useState([]); const [isBusy, setIsBusy] = useState(false); const [message, setMessage] = useState(''); const [showCancelInput, setShowCancelInput] = useState(false); const [cancelPassword, setCancelPassword] = useState(''); const idlTaskMonitor = useTaskMonitor({ taskTypes: ['IDL_RUN_IMPORT', 'IDL_RUN_DINSAR', 'EXTRACT_DINSAR_PRODUCTS'], showRecent: true, recentLimit: 1, }); const runningTask = idlTaskMonitor.activeTasks[0] || null; const [importRootDir, setImportRootDir] = useState(''); const [importNumToProcess, setImportNumToProcess] = useState(0); const [dinsarRootDir, setDinsarRootDir] = useState(''); const [dinsarNumToProcess, setDinsarNumToProcess] = useState(0); const [extractRootDir, setExtractRootDir] = useState(''); const [extractDestDir, setExtractDestDir] = useState(''); const [extractResult, setExtractResult] = useState(null); const [overview, setOverview] = useState(null); const [overviewLoading, setOverviewLoading] = useState(false); const [logModal, setLogModal] = useState({ open: false, runId: '', content: '', loading: false, truncated: false, sizeBytes: 0 }); const [chartStats, setChartStats] = useState(null); useEffect(() => { getStatistics().then(setChartStats).catch(() => {}); }, []); const [importInspect, setImportInspect] = useState(null); const [dinsarInspect, setDinsarInspect] = useState(null); const refreshData = useCallback(async () => { const [s, runs] = await Promise.all([ getEnviStatus(), getRecentRuns(20), ]); setStatus(s); setRecentRuns(Array.isArray(runs?.runs) ? runs.runs : []); }, []); useEffect(() => { refreshData().catch(() => {}); if (!runningTask) { return undefined; } const timer = setInterval(() => refreshData().catch(() => {}), 15000); return () => clearInterval(timer); }, [refreshData, runningTask]); const runAction = async (action) => { setIsBusy(true); setMessage(''); try { await action(); await refreshData(); } catch (error) { const detail = error?.response?.data?.detail || error?.message || '未知错误'; setMessage(`执行失败: ${detail}`); } finally { setIsBusy(false); } }; const handleInspectImport = () => runAction(async () => { const r = await inspectImport(importRootDir.trim()); setImportInspect(r); setMessage(`Import 预检查: ${r?.ready ? '就绪' : '未就绪'}`); }); const handleInspectDinsar = () => runAction(async () => { const r = await inspectDinsar(dinsarRootDir.trim()); setDinsarInspect(r); setMessage(`D-InSAR 预检查: ${r?.ready ? '就绪' : '未就绪'}`); }); const handleQueueImport = () => { if (readOnly) return setMessage('只读账号无法提交任务。'); const root = importRootDir.trim(); if (!root) return setMessage('请填写数据目录。'); runAction(async () => { const r = await queueImportJob({ root_dir: root, num_to_process: Number(importNumToProcess) || 0, }); setMessage(`Import 任务已入队。task_id=${r?.task_id || '-'}`); if (r?.task_id) onJobQueued?.(r.task_id); }); }; const handleQueueDinsar = (mode) => { if (readOnly) return setMessage('只读账号无法提交任务。'); const root = dinsarRootDir.trim(); if (!root) return setMessage('请填写数据目录。'); const modeLabel = mode === 'custom' ? '自定义' : '默认'; runAction(async () => { const r = await queueDinsarJob({ root_dir: root, num_to_process: Number(dinsarNumToProcess) || 0, mode, }); setMessage(`D-InSAR (${modeLabel}) 任务已入队。task_id=${r?.task_id || '-'}`); if (r?.task_id) onJobQueued?.(r.task_id); }); }; const handleExtractDisp = () => { if (readOnly) return setMessage('只读账号无法执行提取。'); const root = extractRootDir.trim(); if (!root) return setMessage('请填写数据目录。'); const dest = extractDestDir.trim() || null; runAction(async () => { const r = await extractDispResults(root, dest); setExtractResult(r); setMessage(`D-InSAR 结果提取与登记任务已入队。task_id=${r?.task_id || '-'}`); if (r?.task_id) onJobQueued?.(r.task_id); }); }; const handleLoadOverview = async () => { const root = dinsarRootDir.trim(); if (!root) return; setOverviewLoading(true); try { const r = await getTaskOverview(root); setOverview(r); } catch (e) { setOverview(null); setMessage(`总览加载失败: ${e?.response?.data?.detail || e?.message}`); } finally { setOverviewLoading(false); } }; const handleOpenLog = async (runId) => { setLogModal({ open: true, runId, content: '', loading: true, truncated: false, sizeBytes: 0 }); try { const r = await getJobLog(runId); setLogModal({ open: true, runId, content: r.content, loading: false, truncated: r.truncated, sizeBytes: r.size_bytes }); } catch (e) { const msg = e?.response?.data?.detail || e?.message || '加载失败'; setLogModal({ open: true, runId, content: `错误: ${msg}`, loading: false, truncated: false, sizeBytes: 0 }); } }; const handleCancelRunningTask = async () => { if (!runningTask || !cancelPassword) return; try { await forceCancelTask(runningTask.task_id, cancelPassword); setMessage('任务取消请求已提交。'); setShowCancelInput(false); setCancelPassword(''); await idlTaskMonitor.refreshRecentTasks(); await refreshData(); } catch (error) { const detail = error?.response?.data?.detail || error?.message || '取消失败'; setMessage(`取消任务失败: ${detail}`); } }; const renderInspect = (title, result) => { if (!result) return null; return (
{title}
状态: {result?.ready ? '就绪' : '未就绪'}
{result?.warnings?.length > 0 && (
{result.warnings.map((w, i) =>
⚠ {w}
)}
)}
          {JSON.stringify(result?.summary || {}, null, 2)}
        
); }; // Buttons are locally disabled when submitting API calls or an IDL task is already active. const isLocked = isBusy || !!runningTask; const demDisplay = status?.dem_base_file || '-'; const demOk = status?.dem_exists; return (

SARscape 处理中心

{/* Status bar */}
ENVI: {status?.idl_installed ? '已安装' : '未检测到'} {status?.idl_running && ' (运行中)'} 引擎: {status?.engine || '-'} DEM: {demDisplay.length > 40 ? '...' + demDisplay.slice(-37) : demDisplay} {demOk ? ' ✓' : ' ✗'}
setShowCancelInput(true)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid #dc2626', background: '#fef2f2', color: '#dc2626', fontSize: '12px', cursor: 'pointer', }} > 取消任务 ) : null} footer={runningTask ? ( <> {showCancelInput && (
setCancelPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleCancelRunningTask()} style={{ padding: '4px 8px', fontSize: '12px', borderRadius: '4px', border: '1px solid #d1d5db', width: '160px' }} />
)} {!showCancelInput && (
同类 ENVI/SARscape 任务运行中,当前提交按钮暂不可用。
)} ) : null} /> {/* Task 状态总览 */}

Task 状态总览

{!overview && !overviewLoading && (
填写下方数据目录后点击刷新
)} {overview && ( <>
共 {overview.summary.total} 个 Task  ·  已导入 {overview.summary.imported}  ·  已处理 {overview.summary.dinsar_done}  ·  已提取 {overview.summary.extracted}
{overview.tasks.map((t) => ( {[t.has_structure, t.imported, t.dinsar_done, t.extracted].map((v, i) => ( ))} ))}
Task 结构 已导入 已处理 已提取 修改时间
{t.task_name} {v ? '✓' : '✗'} {t.last_modified}
)}
{/* Step 1: Import */}

Step 1: 数据导入 (Import)

扫描目录下的 LuTan-1 原始数据 (*.meta.xml),转换为 ENVI SARscape 格式 (*.sml)。 支持 Task_*/master|slave 结构和平铺目录。

{renderInspect('Import 检查结果', importInspect)}
{/* Step 2: D-InSAR */}

Step 2: D-InSAR 生产

对 Task_* 目录执行 D-InSAR 位移生成。 未导入的数据将自动先执行 Import,再进行 D-InSAR 处理。 DEM 路径由系统配置管理,无需手动指定。

{/* 预检查 */}
扫描目录,统计待处理对数,检查 DEM 和 SML 文件是否就绪,不执行任何处理。
{/* 默认处理 */}
调用 ENVI SARscape metatask 一键执行全流程。参数:3×3 多视、默认自适应滤波、自动 GCP 精配准。
{/* 自定义处理 */}
逐步调用各处理模块,参数可控。多视:动态计算、GOLDSTEIN 滤波、自动 GCP、相干阈值 coh=0.05。
{renderInspect('D-InSAR 检查结果', dinsarInspect)}
{/* Step 3: Extract Disp */}

Step 3: 结果提取

将 D-InSAR 生产结果中的 disp 文件提取到目标目录,以便系统识别。

{extractResult && (
{extractResult.queued ? (
D-InSAR 结果提取与登记任务已入队。task_id={extractResult.task_id || '-'}
) : ( <>
目标目录: {extractResult.target_dir}
处理 {extractResult.processed} 个 Task  ·  新增 {extractResult.copied}  ·  更新 {extractResult.overwritten}  ·  跳过 {extractResult.skipped} {extractResult.failed > 0 && (  ·  失败 {extractResult.failed} )}
)}
)}
{/* Run history */}

任务历史

{recentRuns.length === 0 ? (
暂无运行记录
) : (
{recentRuns.map((run) => { const s = run.summary || {}; const counts = s.processed != null ? `成功 ${s.processed} / 失败 ${s.failed || 0} / 跳过 ${s.skipped || 0}` : ''; const autoImported = s.auto_imported > 0 ? ` (自动导入 ${s.auto_imported})` : ''; const statusLabel = run.status === 'success' ? '成功' : run.status === 'failed' ? '失败' : run.status; const statusColor = run.status === 'success' ? '#16a34a' : run.status === 'failed' ? '#dc2626' : '#d97706'; return (
{run.workflow === 'dinsar' ? 'D-InSAR' : run.workflow === 'dinsar_custom' ? 'D-InSAR (自定义)' : 'Import'} {statusLabel} {run.duration_seconds}s {!readOnly && ( )}
{counts && (
{counts}{autoImported}
)}
{run.finished_at || run.started_at || '-'}
); })}
)}
{message && (
消息: {message}
)} {/* 统计图表 */} {(recentRuns.length > 0 || chartStats) && (() => { // 图表1:近期运行成功/失败(来自 recentRuns) const successCount = recentRuns.filter(r => r.status === 'success').length; const failedCount = recentRuns.filter(r => r.status === 'failed').length; const otherCount = recentRuns.length - successCount - failedCount; const doughnutData = { labels: ['成功', '失败', '其他'], datasets: [{ data: [successCount, failedCount, otherCount], backgroundColor: ['#16a34a', '#dc2626', '#94a3b8'] }], }; // 图表2:各工作流平均耗时 const avgDur = chartStats?.idl_processing_stats?.avg_duration_by_workflow || {}; const wfLabels = { import: 'Import', dinsar: 'D-InSAR', dinsar_custom: 'D-InSAR 自定义' }; const durKeys = Object.keys(avgDur); const barDurData = { labels: durKeys.map(k => wfLabels[k] || k), datasets: [{ label: '平均耗时 (s)', data: durKeys.map(k => avgDur[k]), backgroundColor: '#818cf8' }], }; // 图表3:D-InSAR 结果按月入库 const byMonth = chartStats?.dinsar_by_month || []; const barMonthData = { labels: byMonth.map(d => d.month), datasets: [{ label: '入库数量', data: byMonth.map(d => d.count), backgroundColor: '#34d399' }], }; const miniOpts = { responsive: true, plugins: { legend: { display: false }, title: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } }; const doughnutOpts = { responsive: true, plugins: { legend: { position: 'bottom', labels: { font: { size: 11 } } } } }; return (

处理统计

近期运行结果
{durKeys.length > 0 && (
平均耗时 (秒)
)} {byMonth.length > 0 && (
结果入库趋势
)}
); })()} {/* 日志 Modal */} {logModal.open && (
setLogModal((s) => ({ ...s, open: false }))} >
e.stopPropagation()} >
{logModal.runId}.log
{logModal.loading ? (
加载中...
) : (
                  {logModal.content}
                
)}
{logModal.sizeBytes > 0 && `文件大小: ${(logModal.sizeBytes / 1024).toFixed(1)} KB`} {logModal.truncated && ' · 已截断,仅显示末尾 200KB'}
)}
); } export default IDLAutomationPanel;