import React, { useCallback, useEffect, useState } from 'react'; import { deleteRunLog, deleteRunRecord, getRunLog, listEngines, listRuns, previewPyintInputAssets, submitRun } from './api/dinsarProduction'; import { clearTaskLogs, deleteTaskLog, deleteTaskRecord, getActiveTasks, getRecentTasks, 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 RUN_HISTORY_PAGE_SIZE = 200; const TASK_HISTORY_PAGE_SIZE = 500; const TASK_LOG_PAGE_SIZE = 1000; const TERMINAL_STATUS_VALUES = new Set(['COMPLETED', 'FAILED', 'CANCELLED', 'CANCELED', 'success', 'failed', 'cancelled', 'canceled']); 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', pyint: 'PyINT / Gamma', landsar: 'LANDSAR', }; const TASK_TYPE_LABEL = { ISCE2_RUN: 'ISCE2生产', PYINT_RUN: 'PyINT/Gamma生产', IDL_RUN_DINSAR: 'ENVI生产', }; const STATUS_LABEL = { PENDING: '等待中', RUNNING: '运行中', COMPLETED: '已完成', FAILED: '失败', CANCELLED: '已取消', CANCELED: '已取消', success: '成功', failed: '失败', running: '运行中', pending: '等待中', }; const PYINT_DEM_MODE_LABEL = { local_fabdem: '本地 FABDEM', opentopo: 'OpenTopography', prepared_file: '现有 DEM', }; const PYINT_ORBIT_POLICY_LABEL = { validate_only: '仅校验', require_txt: '必须存在', stage_txt: '校验并留痕', }; const PYINT_PRECISE_ORBIT_MODE_LABEL = { replace: '状态向量替换', replace_and_validate: '替换并校验', }; const RERUN_MODE_LABEL = { unfinished_only: '只跑未完成', rerun_all: '全部重跑', }; const RERUN_MODE_OPTIONS = [ { value: 'unfinished_only', label: '只跑未完成', description: '按当前引擎和当前模板检查已有结果,已完成任务会跳过。', }, { value: 'rerun_all', label: '全部重跑', description: '忽略已有结果,对本次选中的任务全部重新执行。', }, ]; 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 taskTypeToEngine(taskType) { if (taskType === 'ISCE2_RUN') return 'isce2'; if (taskType === 'PYINT_RUN') return 'pyint'; if (taskType === 'IDL_RUN_DINSAR') return 'sarscape'; return ''; } function taskStatusToRunStatus(status) { const normalized = String(status || '').toUpperCase(); if (normalized === 'COMPLETED') return 'success'; if (normalized === 'FAILED') return 'failed'; if (normalized === 'CANCELLED' || normalized === 'CANCELED') return 'cancelled'; if (normalized === 'RUNNING') return 'running'; return 'pending'; } function isTerminalRunRow(run) { return TERMINAL_STATUS_VALUES.has(String(run?.raw_status || '').trim()) || TERMINAL_STATUS_VALUES.has(String(run?.status || '').trim()); } function epochSeconds(value) { if (value == null || value === '') return null; if (typeof value === 'number') return value; const parsed = Date.parse(value); return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000); } function taskToRunRow(task) { const taskId = task?.task_id || ''; const message = task?.message || task?.task_name || ''; return { run_id: taskId, record_type: 'task', log_source: 'task', product_family: 'dinsar', engine: taskTypeToEngine(task?.task_type), profile_code: '', status: taskStatusToRunStatus(task?.status), raw_status: task?.status, started_at: epochSeconds(task?.started_at || task?.created_at), ended_at: epochSeconds(task?.ended_at), task_id: taskId, task_type: task?.task_type, workflow_run_id: '', root_dir: '', publish_root_dir: '', message, inferred_paths: inferTaskPaths(task), total_items: null, completed_items: null, failed_items: null, skipped_items: null, }; } function inferTaskPaths(task) { const message = String(task?.message || '').trim(); const workMatch = message.match(/work_dir=([^)\s]+)/); const workDir = workMatch ? workMatch[1] : ''; if (!workDir) return {}; return { work_dir: workDir, ifgrams_dir: workDir ? `${workDir}\\\\ifgrams` : '', }; } function mergeRunRows(productionRuns, recentTasks, limit = null) { const rows = (productionRuns || []).map(run => ({ ...run, record_type: run?.record_type || 'run', log_source: run?.log_source || 'run', })); const representedTaskIds = new Set( rows .map(run => String(run?.task_id || '').trim()) .filter(Boolean), ); (recentTasks || []).forEach(task => { if (!['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'].includes(task?.task_type)) { return; } const taskId = String(task?.task_id || '').trim(); if (!taskId || representedTaskIds.has(taskId)) { return; } rows.push(taskToRunRow(task)); representedTaskIds.add(taskId); }); const sortedRows = rows.sort((a, b) => Number(b?.started_at || 0) - Number(a?.started_at || 0)); return limit == null ? sortedRows : sortedRows.slice(0, limit); } async function fetchAllTaskLogs(taskId) { const allLogs = []; let offset = 0; while (true) { const data = await getTaskLogs(taskId, TASK_LOG_PAGE_SIZE, offset); const pageLogs = data?.logs || []; allLogs.push(...pageLogs); if (pageLogs.length < TASK_LOG_PAGE_SIZE) break; offset += pageLogs.length; } return allLogs; } function formatTaskLogContent(taskId, logs) { if (!Array.isArray(logs) || logs.length === 0) { return '暂无任务日志。'; } const body = logs.map(log => { const timestamp = String(log.timestamp || '').replace('T', ' ').replace('Z', ''); const level = log.level || 'INFO'; return `${timestamp} [${level}] ${log.message || ''}`; }).join('\n'); return `task_id=${taskId}\n${body}`; } function formatPyintDemMode(mode) { return PYINT_DEM_MODE_LABEL[mode] || mode || '-'; } function formatPyintOrbitPolicy(policy) { return PYINT_ORBIT_POLICY_LABEL[policy] || policy || '-'; } function formatPyintPreciseOrbitMode(mode) { return PYINT_PRECISE_ORBIT_MODE_LABEL[mode] || mode || '-'; } function getSchemaOptions(schema) { if (Array.isArray(schema?.enum) && schema.enum.length > 0) return schema.enum; if (Array.isArray(schema?.choices) && schema.choices.length > 0) return schema.choices; return []; } function formatPathValue(value) { const text = String(value || '').trim(); return text || '-'; } function RunPathBlock({ run }) { const items = Array.isArray(run?.items) ? run.items : []; const item = items.find(entry => entry?.status === 'RUNNING') || items[0] || null; const paths = item?.paths || run?.inferred_paths || {}; if (!item && Object.keys(paths).length === 0) return null; const rows = [ ['任务', item?.task_alias || item?.task_name || run?.message || '-'], ['运行目录', paths.run_dir], ['native', paths.native_dir], ['assets', paths.assets_dir], ]; if (run?.engine === 'pyint') { rows.push(['work', paths.work_dir]); rows.push(['project', paths.project_dir]); rows.push(['ifgrams', paths.ifgrams_dir]); rows.push(['重去平日志', paths.reflatten_dir]); } return (
{rows.filter(([, value]) => String(value || '').trim()).map(([label, value]) => (
{label} {formatPathValue(value)}
))}
); } function PreviewIssueList({ title, items, tone = 'warning' }) { if (!Array.isArray(items) || items.length === 0) { return null; } const palette = tone === 'error' ? { background: '#fef2f2', border: '#fecaca', title: '#b91c1c', text: '#7f1d1d', } : { background: '#fff7ed', border: '#fed7aa', title: '#c2410c', text: '#9a3412', }; return (
{title}
{items.slice(0, 6).map((item, index) => (
{item}
))} {items.length > 6 && (
其余 {items.length - 6} 项已折叠。
)}
); } 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 buildParamSections(schema) { const sections = []; const indexByTitle = new Map(); Object.entries(schema || {}).forEach(([name, item]) => { const title = item.section || '处理参数'; if (!indexByTitle.has(title)) { indexByTitle.set(title, sections.length); sections.push({ title, items: [] }); } sections[indexByTitle.get(title)].items.push([name, item]); }); return sections; } 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 ( ); } const options = getSchemaOptions(schema); if (options.length > 0) { return (
{isReadonly && ( 固定值 )}
{description &&
{description}
} {recommendation &&
推荐:{recommendation}
}
); } 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 [submitDialogOpen, setSubmitDialogOpen] = useState(false); const [rerunMode, setRerunMode] = useState('unfinished_only'); const [pyintPreview, setPyintPreview] = useState(null); const [pyintPreviewLoading, setPyintPreviewLoading] = useState(false); const [pyintPreviewFeedback, setPyintPreviewFeedback] = useState({ message: '', error: false }); const [runs, setRuns] = useState([]); const [runsLoading, setRunsLoading] = useState(false); const [logModal, setLogModal] = useState({ open: false, runId: '', taskId: '', source: 'run', content: '', loading: false, }); const [runLogDeletingId, setRunLogDeletingId] = useState(''); const [activeTask, setActiveTask] = useState(null); const [recentTask, setRecentTask] = 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 currentParamSections = buildParamSections(currentParamSchema); const currentDefaultTimeoutSec = Number(currentEngineObj?.default_timeout_seconds || 0) || 0; const currentParamHelpText = selectedEngine === 'pyint' ? 'PyINT/Gamma 会按目标网格尺寸自动换算多视;新增 Gamma 残余重去平在解缠后执行 rascc_mask/quad_fit/quad_sub,再导出 native 和标准 GeoTIFF。' : selectedEngine === 'isce2' ? '这些参数现在按执行、交付、增强分组展示。结果异常时,优先尝试关闭增强项,再回看基础几何和配对质量。' : '这些参数影响当前引擎的生产模板。建议先使用默认值,只有在结果边界、噪声或几何表现异常时再逐项调整。'; const pyintPreviewBlocksSubmit = selectedEngine === 'pyint' && pyintPreview && pyintPreview.allow_submit === false; const latestRunWithTask = runs.find(run => run?.task_id) || null; const monitoredTask = activeTask || recentTask || ( latestRunWithTask ? { task_id: latestRunWithTask.task_id, task_type: latestRunWithTask.engine === 'isce2' ? 'ISCE2_RUN' : latestRunWithTask.engine === 'pyint' ? 'PYINT_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 (options = {}) => { const silent = !!options.silent; if (!silent) setRunsLoading(true); try { const loadProductionRuns = async () => { const allRuns = []; let offset = 0; while (true) { const data = await listRuns(RUN_HISTORY_PAGE_SIZE, offset); const pageRuns = data?.runs || []; allRuns.push(...pageRuns); const total = Number(data?.total || 0); if (pageRuns.length < RUN_HISTORY_PAGE_SIZE || allRuns.length >= total) break; offset += pageRuns.length; } return allRuns; }; const loadRecentTasks = async () => { const allTasks = []; let offset = 0; while (true) { const data = await getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], TASK_HISTORY_PAGE_SIZE, offset); const pageTasks = Array.isArray(data) ? data : (data?.tasks || []); allTasks.push(...pageTasks); if (pageTasks.length < TASK_HISTORY_PAGE_SIZE) break; offset += pageTasks.length; } return allTasks; }; const [productionRuns, recentTasks] = await Promise.all([ loadProductionRuns(), loadRecentTasks(), ]); const nextRuns = mergeRunRows(productionRuns, recentTasks); setRuns(nextRuns); return nextRuns; } catch { setRuns([]); return []; } finally { if (!silent) 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', 'PYINT_RUN', 'IDL_RUN_DINSAR'].includes(task.task_type)) || null; setActiveTask(relevantTask); return relevantTask; } catch { setActiveTask(null); return null; } }, []); const loadRecentTask = useCallback(async () => { try { const data = await getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], 1, 0); const tasks = Array.isArray(data) ? data : (data?.tasks || []); const relevantTask = tasks[0] || null; setRecentTask(relevantTask); return relevantTask; } catch { setRecentTask(null); return null; } }, []); const loadTaskLogs = useCallback(async (taskId, options = {}) => { const silent = !!options.silent; if (!taskId) { setTaskLogs([]); return; } if (!silent) setTaskLogsLoading(true); try { const logs = await fetchAllTaskLogs(taskId); setTaskLogs(logs); } catch { setTaskLogs([]); } finally { if (!silent) 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 (options = {}) => { const silent = !!options.silent; const [nextRuns, nextActiveTask, nextRecentTask] = await Promise.all([ loadRuns({ silent }), loadActiveTask(), loadRecentTask(), ]); const fallbackTaskId = nextActiveTask?.task_id || nextRecentTask?.task_id || nextRuns.find(run => run?.task_id)?.task_id || ''; await loadTaskLogs(fallbackTaskId, { silent }); }, [loadActiveTask, loadRecentTask, loadRuns, loadTaskLogs]); useEffect(() => { loadEngines(); refreshMonitor(); }, [loadEngines, refreshMonitor]); useEffect(() => { const intervalMs = activeTask ? 5000 : 15000; const timer = window.setInterval(() => { refreshMonitor({ silent: true }); }, intervalMs); return () => window.clearInterval(timer); }, [activeTask, refreshMonitor]); useEffect(() => { if (currentProfiles.length > 0) { setSelectedProfile(currentProfiles[0].code); } }, [selectedEngine, currentProfiles]); useEffect(() => { setEngineExtraParams(buildDefaults(currentParamSchema)); }, [selectedEngine, selectedProfile, currentParamSchema]); useEffect(() => { if (currentDefaultTimeoutSec > 0) { setTimeoutSec(String(currentDefaultTimeoutSec)); return; } setTimeoutSec(''); }, [selectedEngine, currentDefaultTimeoutSec]); useEffect(() => { setPyintPreview(null); setPyintPreviewFeedback({ message: '', error: false }); }, [selectedEngine, rootDir, numToProcess]); const handleParamChange = useCallback((name, value) => { setEngineExtraParams(current => ({ ...current, [name]: value, })); }, []); const handlePreviewPyint = useCallback(async () => { if (!rootDir.trim()) { setPyintPreview(null); setPyintPreviewFeedback({ message: '请先输入根目录。', error: true }); return; } setPyintPreviewLoading(true); setPyintPreviewFeedback({ message: '', error: false }); try { const data = await previewPyintInputAssets({ root_dir: rootDir.trim(), num_to_process: Number(numToProcess) || 0, }); setPyintPreview(data); const taskCount = Number(data?.selected_task_count || data?.task_count || 0); const orbitMissingCount = Number(data?.orbits?.missing_task_count || 0); const detail = data?.allow_submit ? `预检完成,可提交 ${taskCount} 个任务。` : `预检完成,存在阻塞项;涉及 ${orbitMissingCount} 个轨道未齐全任务。`; setPyintPreviewFeedback({ message: detail, error: !data?.allow_submit }); } catch (err) { setPyintPreview(null); setPyintPreviewFeedback({ message: `预检失败:${err?.response?.data?.detail || err.message}`, error: true, }); } finally { setPyintPreviewLoading(false); } }, [numToProcess, rootDir]); const handleOpenSubmitDialog = useCallback(() => { if (!rootDir.trim()) { setSubmitError(true); setSubmitMsg('请输入根目录。'); return; } if (pyintPreviewBlocksSubmit) { setSubmitError(true); setSubmitMsg('PyINT 输入资产预检未通过,请先修复阻塞项。'); return; } setSubmitError(false); setSubmitMsg(''); setSubmitDialogOpen(true); }, [pyintPreviewBlocksSubmit, rootDir]); const handleCloseSubmitDialog = useCallback(() => { if (submitting) return; setSubmitDialogOpen(false); }, [submitting]); const handleSubmit = async () => { setSubmitting(true); setSubmitMsg(''); setSubmitError(false); try { const extra = buildExtraPayload(currentParamSchema, engineExtraParams); setSubmitDialogOpen(false); const result = await submitRun({ engine_code: selectedEngine, profile: selectedProfile, root_dir: rootDir.trim(), num_to_process: Number(numToProcess) || 0, rerun_mode: rerunMode, timeout_seconds: timeoutSec ? Number(timeoutSec) : null, extra, }); const taskCount = result?.selected_task_count ? `,选中 ${result.selected_task_count} 个任务` : ''; const skippedCompleted = Number(result?.skipped_completed_count || 0); const skippedText = skippedCompleted > 0 ? `,跳过 ${skippedCompleted} 个已完成任务` : ''; setSubmitError(false); setSubmitMsg(`任务已入队:${result.task_id}${taskCount}${skippedText}`); 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 run => { const runId = typeof run === 'string' ? run : (run?.run_id || run?.task_id || ''); const source = typeof run !== 'string' && run?.log_source === 'task' ? 'task' : 'run'; const taskId = typeof run !== 'string' ? (run?.task_id || '') : ''; setLogModal({ open: true, runId, taskId, source, content: '', loading: true }); try { if (source === 'task' && taskId) { const logs = await fetchAllTaskLogs(taskId); setLogModal({ open: true, runId, taskId, source, content: formatTaskLogContent(taskId, logs), loading: false, }); return; } const data = await getRunLog(runId); setLogModal({ open: true, runId, taskId: '', source, content: data.content || '', loading: false }); } catch { setLogModal({ open: true, runId, taskId, source, content: '日志加载失败,可能日志文件不存在或已被删除。', loading: false }); } }; const handleDeleteRunRowLog = useCallback(async run => { if (readOnly || runLogDeletingId) return; const source = run?.log_source === 'task' ? 'task' : 'run'; const runId = run?.run_id || run?.task_id || ''; const taskId = run?.task_id || ''; const label = source === 'task' ? `任务 ${taskId}` : `运行 ${runId}`; if (!runId) return; if (!window.confirm(`确定要删除${label}的日志吗?运行记录和产物不会删除。`)) return; setRunLogDeletingId(runId); try { if (source === 'task') { await clearTaskLogs(taskId || runId); if (logTaskId === (taskId || runId)) { await loadTaskLogs(taskId || runId); } } else { await deleteRunLog(runId); } if (logModal.open && logModal.runId === runId) { setLogModal(current => ({ ...current, content: '日志已删除。', loading: false, })); } setSubmitError(false); setSubmitMsg('日志已删除。'); await refreshMonitor({ silent: true }); } catch (err) { setSubmitError(true); setSubmitMsg(`删除日志失败:${err?.response?.data?.detail || err.message}`); } finally { setRunLogDeletingId(''); } }, [loadTaskLogs, logModal.open, logModal.runId, logTaskId, readOnly, refreshMonitor, runLogDeletingId]); const handleDeleteOpenLog = useCallback(async () => { if (!logModal.open || readOnly || runLogDeletingId) return; await handleDeleteRunRowLog({ run_id: logModal.runId, task_id: logModal.taskId, log_source: logModal.source, }); }, [handleDeleteRunRowLog, logModal, readOnly, runLogDeletingId]); const handleDeleteRunHistory = useCallback(async run => { if (readOnly || runLogDeletingId) return; const isTaskRecord = run?.log_source === 'task'; const recordId = isTaskRecord ? (run?.task_id || run?.run_id || '') : (run?.run_id || ''); const taskId = run?.task_id || ''; if (!recordId) return; if (!isTerminalRunRow(run)) { setSubmitError(true); setSubmitMsg('运行中记录不能删除。'); return; } if (!window.confirm(`确定要删除运行记录 ${recordId} 吗?只删除记录和日志,不删除产物目录。`)) return; setRunLogDeletingId(recordId); try { if (isTaskRecord) { await deleteTaskRecord(recordId); } else { await deleteRunRecord(recordId); } if (logTaskId === (taskId || recordId)) { setTaskLogs([]); } if (logModal.open && logModal.runId === recordId) { setLogModal({ open: false, runId: '', taskId: '', source: 'run', content: '', loading: false }); } setSubmitError(false); setSubmitMsg('运行记录已删除,产物目录未删除。'); await refreshMonitor({ silent: true }); } catch (err) { setSubmitError(true); setSubmitMsg(`删除运行记录失败:${err?.response?.data?.detail || err.message}`); } finally { setRunLogDeletingId(''); } }, [logModal.open, logModal.runId, logTaskId, readOnly, refreshMonitor, runLogDeletingId]); const isSubmitDisabled = readOnly || submitting || !currentEngineObj?.available || pyintPreviewBlocksSubmit; return (
{logModal.open && (
运行日志 - {logModal.runId}
{!readOnly && ( )}
              {logModal.loading ? '加载中...' : logModal.content || '(日志为空)'}
            
)} {submitDialogOpen && (
提交生产任务
选择本次批处理策略。任务数量限制会在“只跑未完成”过滤之后再生效,按当前引擎和当前模板判断已完成状态。
{RERUN_MODE_OPTIONS.map(option => { const selected = rerunMode === option.value; return ( ); })}
引擎:{formatEngineLabel(selectedEngine, currentEngineObj?.engine_label)}
模板:{currentProfileObj?.label || selectedProfile}
任务数量:{Number(numToProcess) > 0 ? Number(numToProcess) : '全部'}
执行策略:{RERUN_MODE_LABEL[rerunMode] || rerunMode}
)}
引擎状态
{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={currentDefaultTimeoutSec > 0 ? `默认 ${currentDefaultTimeoutSec}` : '默认'} disabled={readOnly} style={{ width: 140, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }} /> {selectedEngine === 'isce2' && currentDefaultTimeoutSec > 0 && (
ISCE2 默认按单对任务使用 {currentDefaultTimeoutSec} 秒;批量目录会串行逐对套用该超时。
)} {selectedEngine === 'pyint' && currentDefaultTimeoutSec > 0 && (
PyINT 默认按单对任务使用 {currentDefaultTimeoutSec} 秒;当前会逐对串行创建工作区并运行外部 PyINT / Gamma 流程,native 会在主流程和重去平后统一写入。
)}
{Object.keys(currentParamSchema).length > 0 && (
处理参数
{currentParamHelpText}
{currentParamSections.map(section => (
{section.title}
{section.items.map(([name, schema]) => ( ))}
))}
)} {selectedEngine === 'pyint' && (
PyINT 输入资产预检
提交前检查 Task_* 结构、DEM 策略和 LT-1 轨道是否齐备。即使不手动预检,后端提交时也会做同样校验。
{pyintPreviewFeedback.message && (
{pyintPreviewFeedback.message}
)} {!pyintPreview && !pyintPreviewLoading && (
尚未执行预检。建议在首次处理新批次前先预检一次。
)} {pyintPreview && ( <>
{[ { label: '任务数', value: pyintPreview.selected_task_count ?? pyintPreview.task_count ?? 0, color: '#0f172a' }, { label: 'DEM 策略', value: formatPyintDemMode(pyintPreview?.dem?.mode), color: '#1d4ed8' }, { label: '轨道策略', value: formatPyintOrbitPolicy(pyintPreview?.orbits?.policy), color: '#7c3aed' }, { label: '精轨桥接', value: pyintPreview?.precise_orbit_bridge?.enabled ? formatPyintPreciseOrbitMode(pyintPreview?.precise_orbit_bridge?.mode) : '关闭', color: pyintPreview?.precise_orbit_bridge?.enabled ? '#0f766e' : '#64748b', }, { label: '可提交', value: pyintPreview.allow_submit ? '是' : '否', color: pyintPreview.allow_submit ? '#15803d' : '#b91c1c' }, { label: '缺轨道任务', value: pyintPreview?.orbits?.missing_task_count ?? 0, color: (pyintPreview?.orbits?.missing_task_count || 0) > 0 ? '#b91c1c' : '#475569' }, { label: '无效目录', value: pyintPreview?.invalid_candidates?.length ?? 0, color: (pyintPreview?.invalid_candidates?.length || 0) > 0 ? '#c2410c' : '#475569' }, ].map(item => (
{item.label}
{item.value}
))}
任务级预检结果
{['任务', '源文件', '轨道解析', '提交'].map(header => (
{header}
))}
{(pyintPreview.tasks || []).map(task => { const masterOrbit = task?.orbit_resolution?.master; const slaveOrbit = task?.orbit_resolution?.slave; return (
{task.task_alias || task.task_name}
{task.master_date || '-'} / {task.slave_date || '-'}
M {task?.archive_counts?.master ?? 0}
S {task?.archive_counts?.slave ?? 0}
M {masterOrbit?.resolved ? `${masterOrbit.satellite}/${masterOrbit.date}` : '缺失'}
S {slaveOrbit?.resolved ? `${slaveOrbit.satellite}/${slaveOrbit.date}` : '缺失'}
{task.allow_submit ? '可提交' : '阻塞'}
); })}
)}
)}
{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 && ( )}
))}
)}
)}
运行记录(已加载 {runs.length} 条)
{runsLoading ? (
加载中...
) : runs.length === 0 ? (
暂无记录。
) : (
{['运行ID', '引擎', '状态', '时间', '路径', '操作'].map(header => ( ))} {runs.map(run => { const recordId = run.log_source === 'task' ? (run.task_id || run.run_id) : run.run_id; const canDeleteRecord = isTerminalRunRow(run); return ( ); })}
{header}
{run.run_id} {formatEngineLabel(run.engine)} {formatStatus(run.status)} {run.started_at ? new Date(run.started_at * 1000).toLocaleString() : '-'} {!readOnly && ( )} {!readOnly && ( )}
)}
); }