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 (
{JSON.stringify(result?.summary || {}, null, 2)}
{demDisplay.length > 40 ? '...' + demDisplay.slice(-37) : demDisplay}
{demOk ? ' ✓' : ' ✗'}
| Task | 结构 | 已导入 | 已处理 | 已提取 | 修改时间 |
|---|---|---|---|---|---|
| {t.task_name} | {[t.has_structure, t.imported, t.dinsar_done, t.extracted].map((v, i) => ({v ? '✓' : '✗'} | ))}{t.last_modified} |
扫描目录下的 LuTan-1 原始数据 (*.meta.xml),转换为 ENVI SARscape 格式 (*.sml)。 支持 Task_*/master|slave 结构和平铺目录。
对 Task_* 目录执行 D-InSAR 位移生成。 未导入的数据将自动先执行 Import,再进行 D-InSAR 处理。 DEM 路径由系统配置管理,无需手动指定。
将 D-InSAR 生产结果中的 disp 文件提取到目标目录,以便系统识别。
{extractResult.target_dir}
{logModal.content}
)}