import React, { useCallback, useEffect, useState } from 'react'; import { listEngines, listRuns, submitRun } from './api/dinsarProduction'; import { getJobLog } from './api/idl'; import { clearTaskLogs, deleteTaskLog, getActiveTasks, getTaskLogs } from './api/tasks'; const card = { background: '#fff', padding: '12px', borderRadius: '8px', border: '1px solid #e2e8f0', marginBottom: '12px', }; const EMPTY_ARRAY = []; const EMPTY_OBJECT = {}; const ENGINE_STATUS_COLOR = { ok: '#22c55e', degraded: '#f59e0b', unavailable: '#ef4444', not_implemented: '#94a3b8', error: '#ef4444', }; const ENGINE_STATUS_LABEL = { ok: '可用', degraded: '降级', unavailable: '不可用', not_implemented: '预留', error: '异常', }; const ENGINE_LABEL = { sarscape: 'SARscape', isce2: 'ISCE2', landsar: 'LANDSAR', }; const TASK_TYPE_LABEL = { ISCE2_RUN: 'ISCE2生产', IDL_RUN_DINSAR: 'ENVI生产', }; const STATUS_LABEL = { PENDING: '等待中', RUNNING: '运行中', COMPLETED: '已完成', FAILED: '失败', CANCELLED: '已取消', CANCELED: '已取消', success: '成功', failed: '失败', running: '运行中', pending: '等待中', }; function formatEngineLabel(engineCode, engineLabel = '') { return engineLabel || ENGINE_LABEL[engineCode] || engineCode || '-'; } function formatTaskType(taskType) { return TASK_TYPE_LABEL[taskType] || taskType || '-'; } function formatStatus(status) { return STATUS_LABEL[status] || status || '-'; } function EngineStatusCard({ engine, onSelect, selected }) { const color = ENGINE_STATUS_COLOR[engine.status] || '#94a3b8'; const label = ENGINE_STATUS_LABEL[engine.status] || engine.status; const clickable = engine.available; return (
clickable && onSelect(engine.engine_code)} style={{ ...card, marginBottom: 0, cursor: clickable ? 'pointer' : 'default', border: selected ? '2px solid #3b82f6' : `1px solid ${clickable ? '#e2e8f0' : '#f1f5f9'}`, background: selected ? '#eff6ff' : clickable ? '#fff' : '#f8fafc', opacity: clickable ? 1 : 0.7, minWidth: 160, flex: 1, }} >
{engine.engine_label}
{label}
{engine.message && (
{engine.message}
)}
); } function buildDefaults(schema) { const defaults = {}; Object.entries(schema || {}).forEach(([key, item]) => { if (Object.prototype.hasOwnProperty.call(item, 'default')) { defaults[key] = item.default; } else if (item.type === 'boolean') { defaults[key] = false; } else { defaults[key] = ''; } }); return defaults; } function buildExtraPayload(schema, values) { const payload = {}; Object.entries(schema || {}).forEach(([key, item]) => { if (item.readonly || item.include_in_payload === false) { return; } const rawValue = values[key]; if (item.type === 'boolean') { payload[key] = !!rawValue; return; } if (rawValue == null || String(rawValue).trim() === '') return; if (item.type === 'number') { const parsed = Number(rawValue); if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { payload[key] = parsed; } return; } payload[key] = String(rawValue).trim(); }); return payload; } function ParamField({ name, schema, value, disabled, onChange }) { const label = schema.label || name; const description = schema.description || ''; const recommendation = schema.recommendation || ''; const isReadonly = !!schema.readonly; const inputStyle = { width: '100%', padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13, boxSizing: 'border-box', background: isReadonly ? '#f8fafc' : '#fff', color: isReadonly ? '#475569' : '#0f172a', }; if (schema.type === 'boolean') { return ( ); } return (
{isReadonly && ( 固定值 )}
onChange(name, event.target.value)} style={inputStyle} /> {description &&
{description}
} {recommendation &&
推荐:{recommendation}
}
); } export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) { const [engines, setEngines] = useState([]); const [enginesLoading, setEnginesLoading] = useState(false); const [selectedEngine, setSelectedEngine] = useState('sarscape'); const [selectedProfile, setSelectedProfile] = useState('custom6'); const [rootDir, setRootDir] = useState(''); const [numToProcess, setNumToProcess] = useState(0); const [timeoutSec, setTimeoutSec] = useState(''); const [engineExtraParams, setEngineExtraParams] = useState({}); const [submitting, setSubmitting] = useState(false); const [submitMsg, setSubmitMsg] = useState(''); const [submitError, setSubmitError] = useState(false); const [runs, setRuns] = useState([]); const [runsLoading, setRunsLoading] = useState(false); const [logModal, setLogModal] = useState({ open: false, runId: '', content: '', loading: false }); const [activeTask, setActiveTask] = useState(null); const [taskLogs, setTaskLogs] = useState([]); const [taskLogsLoading, setTaskLogsLoading] = useState(false); const [taskLogActionLoading, setTaskLogActionLoading] = useState(false); const [taskLogDeletingId, setTaskLogDeletingId] = useState(null); const currentEngineObj = engines.find(engine => engine.engine_code === selectedEngine) || null; const currentProfiles = currentEngineObj?.profiles || EMPTY_ARRAY; const currentProfileObj = currentProfiles.find(profile => profile.code === selectedProfile) || null; const currentParamSchema = currentProfileObj?.params_schema || EMPTY_OBJECT; const latestRunWithTask = runs.find(run => run?.task_id) || null; const monitoredTask = activeTask || ( latestRunWithTask ? { task_id: latestRunWithTask.task_id, task_type: latestRunWithTask.engine === 'isce2' ? 'ISCE2_RUN' : 'IDL_RUN_DINSAR', status: latestRunWithTask.raw_status || latestRunWithTask.status, progress: latestRunWithTask.raw_status === 'COMPLETED' || latestRunWithTask.status === 'success' ? 100 : null, message: latestRunWithTask.message || '最近一次任务', } : null ); const logTaskId = monitoredTask?.task_id || ''; const showingRecentTask = !activeTask && !!monitoredTask; const loadEngines = useCallback(async () => { setEnginesLoading(true); try { const data = await listEngines(); setEngines(data.engines || []); } catch { setEngines([]); } finally { setEnginesLoading(false); } }, []); const loadRuns = useCallback(async () => { setRunsLoading(true); try { const data = await listRuns(20); const nextRuns = data.runs || []; setRuns(nextRuns); return nextRuns; } catch { setRuns([]); return []; } finally { setRunsLoading(false); } }, []); const loadActiveTask = useCallback(async () => { try { const data = await getActiveTasks(); const tasks = Array.isArray(data) ? data : (data?.tasks || []); const relevantTask = tasks.find(task => ['ISCE2_RUN', 'IDL_RUN_DINSAR'].includes(task.task_type)) || null; setActiveTask(relevantTask); return relevantTask; } catch { setActiveTask(null); return null; } }, []); const loadTaskLogs = useCallback(async taskId => { if (!taskId) { setTaskLogs([]); return; } setTaskLogsLoading(true); try { const data = await getTaskLogs(taskId, 50, 0); setTaskLogs(data?.logs || []); } catch { setTaskLogs([]); } finally { setTaskLogsLoading(false); } }, []); const handleDeleteTaskLog = useCallback(async logId => { const taskId = logTaskId; if (!taskId || !logId || taskLogActionLoading) return; if (!window.confirm('确定要删除这条任务日志吗?')) return; setTaskLogDeletingId(logId); setTaskLogActionLoading(true); try { await deleteTaskLog(taskId, logId); await loadTaskLogs(taskId); } catch (err) { setSubmitError(true); setSubmitMsg(`删除日志失败:${err?.response?.data?.detail || err.message}`); } finally { setTaskLogDeletingId(null); setTaskLogActionLoading(false); } }, [logTaskId, loadTaskLogs, taskLogActionLoading]); const handleClearTaskLogs = useCallback(async () => { const taskId = logTaskId; if (!taskId || taskLogActionLoading || taskLogs.length === 0) return; if (!window.confirm(`确定要清空任务 ${taskId} 的全部日志吗?`)) return; setTaskLogActionLoading(true); try { await clearTaskLogs(taskId); await loadTaskLogs(taskId); } catch (err) { setSubmitError(true); setSubmitMsg(`清空日志失败:${err?.response?.data?.detail || err.message}`); } finally { setTaskLogActionLoading(false); } }, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]); const refreshMonitor = useCallback(async () => { const [nextRuns, nextActiveTask] = await Promise.all([ loadRuns(), loadActiveTask(), ]); const fallbackTaskId = nextActiveTask?.task_id || nextRuns.find(run => run?.task_id)?.task_id || ''; await loadTaskLogs(fallbackTaskId); }, [loadActiveTask, loadRuns, loadTaskLogs]); useEffect(() => { loadEngines(); refreshMonitor(); }, [loadEngines, refreshMonitor]); useEffect(() => { if (currentProfiles.length > 0) { setSelectedProfile(currentProfiles[0].code); } }, [selectedEngine, currentProfiles]); useEffect(() => { setEngineExtraParams(buildDefaults(currentParamSchema)); }, [selectedEngine, selectedProfile, currentParamSchema]); const handleParamChange = useCallback((name, value) => { setEngineExtraParams(current => ({ ...current, [name]: value, })); }, []); const handleSubmit = async () => { if (!rootDir.trim()) { setSubmitError(true); setSubmitMsg('请输入根目录。'); return; } setSubmitting(true); setSubmitMsg(''); setSubmitError(false); try { const extra = buildExtraPayload(currentParamSchema, engineExtraParams); const result = await submitRun({ engine_code: selectedEngine, profile: selectedProfile, root_dir: rootDir.trim(), num_to_process: Number(numToProcess) || 0, timeout_seconds: timeoutSec ? Number(timeoutSec) : null, extra, }); const taskCount = result?.selected_task_count ? `,选中 ${result.selected_task_count} 个任务` : ''; setSubmitError(false); setSubmitMsg(`任务已入队:${result.task_id}${taskCount}`); if (onJobQueued) onJobQueued(result.task_id); await refreshMonitor(); } catch (err) { setSubmitError(true); setSubmitMsg(`提交失败:${err?.response?.data?.detail || err.message}`); } finally { setSubmitting(false); } }; const handleViewLog = async runId => { setLogModal({ open: true, runId, content: '', loading: true }); try { const data = await getJobLog(runId); setLogModal({ open: true, runId, content: data.content || '', loading: false }); } catch { setLogModal({ open: true, runId, content: '日志加载失败。', loading: false }); } }; const isSubmitDisabled = readOnly || submitting || !currentEngineObj?.available; return (
{logModal.open && (
运行日志 - {logModal.runId}
              {logModal.loading ? '加载中...' : logModal.content || '(日志为空)'}
            
)}
引擎状态
{engines.length === 0 && !enginesLoading && ( 暂无引擎信息。 )} {engines.map(engine => ( ))}
提交生产任务
{currentProfileObj?.description && (
{currentProfileObj.description}
)}
setRootDir(event.target.value)} placeholder="批处理根目录或单个任务目录" disabled={readOnly} style={{ width: '100%', padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13, boxSizing: 'border-box', }} />
setNumToProcess(event.target.value)} disabled={readOnly} style={{ width: 120, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }} />
setTimeoutSec(event.target.value)} placeholder="默认" disabled={readOnly} style={{ width: 140, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }} />
{Object.keys(currentParamSchema).length > 0 && (
处理参数
这些参数主要影响目标网格大小、精裁剪范围、地理编码范围和位移结果掩膜。建议先使用默认值,通常优先只调整目标网格大小;只有在边缘被裁切、时间窗异常或噪声较多时,再继续调整其他参数。
{Object.entries(currentParamSchema).map(([name, schema]) => ( ))}
)}
{submitMsg && ( {submitMsg} )}
运行监控
监控与日志改为手动刷新,避免界面持续轮询请求。
{monitoredTask && (
{showingRecentTask ? '最近一次任务' : '当前任务'}
{monitoredTask.task_id} - {formatTaskType(monitoredTask.task_type)} - {formatStatus(monitoredTask.status)} - {monitoredTask.message}
{monitoredTask.progress != null && (
{monitoredTask.progress}%
)}
{showingRecentTask ? '最近一次任务日志' : '当前任务日志'}
{!readOnly && ( )}
{taskLogsLoading ? (
加载中...
) : taskLogs.length === 0 ? (
暂无日志。
) : (
{taskLogs.map((log, index) => (
{(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
{log.message}
{!readOnly && ( )}
))}
)}
)}
最近 20 条运行记录
{runsLoading ? (
加载中...
) : runs.length === 0 ? (
暂无记录。
) : ( {['运行ID', '引擎', '状态', '时间', '操作'].map(header => ( ))} {runs.map(run => ( ))}
{header}
{run.run_id} {formatEngineLabel(run.engine)} {formatStatus(run.status)} {run.started_at ? new Date(run.started_at * 1000).toLocaleString() : '-'}
)}
); }