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 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);
setRuns(data.runs || []);
} catch {
setRuns([]);
} 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);
} catch {
setActiveTask(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 = activeTask?.task_id;
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);
}
}, [activeTask?.task_id, loadTaskLogs, taskLogActionLoading]);
const handleClearTaskLogs = useCallback(async () => {
const taskId = activeTask?.task_id;
if (!taskId || taskLogActionLoading || taskLogs.length === 0) return;
if (!window.confirm('确定要清空当前任务的全部日志吗?')) return;
setTaskLogActionLoading(true);
try {
await clearTaskLogs(taskId);
await loadTaskLogs(taskId);
} catch (err) {
setSubmitError(true);
setSubmitMsg(`清空日志失败:${err?.response?.data?.detail || err.message}`);
} finally {
setTaskLogActionLoading(false);
}
}, [activeTask?.task_id, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
useEffect(() => {
loadEngines();
loadRuns();
loadActiveTask();
}, [loadActiveTask, loadEngines, loadRuns]);
useEffect(() => {
const timer = setInterval(loadActiveTask, 5000);
return () => clearInterval(timer);
}, [loadActiveTask]);
useEffect(() => {
const taskId = activeTask?.task_id || '';
loadTaskLogs(taskId);
if (!taskId) return undefined;
const timer = setInterval(() => loadTaskLogs(taskId), 5000);
return () => clearInterval(timer);
}, [activeTask?.task_id, loadTaskLogs]);
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);
loadRuns();
loadActiveTask();
} 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',
}}
/>
{Object.keys(currentParamSchema).length > 0 && (
处理参数
这些参数主要影响目标网格大小、精裁剪范围、地理编码范围和位移结果掩膜。建议先使用默认值,通常优先只调整目标网格大小;只有在边缘被裁切、时间窗异常或噪声较多时,再继续调整其他参数。
{Object.entries(currentParamSchema).map(([name, schema]) => (
))}
)}
{submitMsg && (
{submitMsg}
)}
运行监控
{activeTask && (
当前任务
{activeTask.task_id} - {formatTaskType(activeTask.task_type)} - {formatStatus(activeTask.status)} - {activeTask.message}
{activeTask.progress != null && (
)}
任务日志
{!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 => (
|
{header}
|
))}
{runs.map(run => (
| {run.run_id} |
{formatEngineLabel(run.engine)} |
{formatStatus(run.status)}
|
{run.started_at ? new Date(run.started_at * 1000).toLocaleString() : '-'}
|
|
))}
)}
);
}