Fix D-InSAR runtime logs and UI encoding
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { listEngines, listRuns, submitRun } from './api/dinsarProduction';
|
||||
import { getActiveTasks, getJobLog, getTaskLogs } from './api/idl';
|
||||
import { getJobLog } from './api/idl';
|
||||
import { clearTaskLogs, deleteTaskLog, getActiveTasks, getTaskLogs } from './api/tasks';
|
||||
|
||||
const card = {
|
||||
background: '#fff',
|
||||
@@ -24,10 +25,10 @@ const ENGINE_STATUS_COLOR = {
|
||||
|
||||
const ENGINE_STATUS_LABEL = {
|
||||
ok: '可用',
|
||||
degraded: '部分可用',
|
||||
degraded: '降级',
|
||||
unavailable: '不可用',
|
||||
not_implemented: '预留',
|
||||
error: '错误',
|
||||
error: '异常',
|
||||
};
|
||||
|
||||
const ENGINE_LABEL = {
|
||||
@@ -37,8 +38,8 @@ const ENGINE_LABEL = {
|
||||
};
|
||||
|
||||
const TASK_TYPE_LABEL = {
|
||||
ISCE2_RUN: 'ISCE2生产任务',
|
||||
IDL_RUN_DINSAR: 'ENVI生产任务',
|
||||
ISCE2_RUN: 'ISCE2生产',
|
||||
IDL_RUN_DINSAR: 'ENVI生产',
|
||||
};
|
||||
|
||||
const STATUS_LABEL = {
|
||||
@@ -220,7 +221,7 @@ function ParamField({ name, schema, value, disabled, onChange }) {
|
||||
style={inputStyle}
|
||||
/>
|
||||
{description && <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 4 }}>{description}</div>}
|
||||
{recommendation && <div style={{ fontSize: 11, color: '#2563eb', marginTop: 4 }}>建议:{recommendation}</div>}
|
||||
{recommendation && <div style={{ fontSize: 11, color: '#2563eb', marginTop: 4 }}>推荐:{recommendation}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -236,6 +237,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
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);
|
||||
@@ -244,6 +246,8 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
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;
|
||||
@@ -301,6 +305,42 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
}
|
||||
}, []);
|
||||
|
||||
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();
|
||||
@@ -339,12 +379,14 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!rootDir.trim()) {
|
||||
setSubmitMsg('请填写根目录。');
|
||||
setSubmitError(true);
|
||||
setSubmitMsg('请输入根目录。');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setSubmitMsg('');
|
||||
setSubmitError(false);
|
||||
try {
|
||||
const extra = buildExtraPayload(currentParamSchema, engineExtraParams);
|
||||
const result = await submitRun({
|
||||
@@ -355,12 +397,14 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
timeout_seconds: timeoutSec ? Number(timeoutSec) : null,
|
||||
extra,
|
||||
});
|
||||
const taskCount = result?.selected_task_count ? `,已选 ${result.selected_task_count} 个任务` : '';
|
||||
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);
|
||||
@@ -411,7 +455,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
onClick={() => setLogModal({ open: false, runId: '', content: '', loading: false })}
|
||||
style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', fontSize: 18 }}
|
||||
>
|
||||
×
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<pre
|
||||
@@ -451,7 +495,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
{engines.length === 0 && !enginesLoading && (
|
||||
<span style={{ fontSize: 12, color: '#94a3b8' }}>暂无可用引擎信息。</span>
|
||||
<span style={{ fontSize: 12, color: '#94a3b8' }}>暂无引擎信息。</span>
|
||||
)}
|
||||
{engines.map(engine => (
|
||||
<EngineStatusCard
|
||||
@@ -465,7 +509,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
|
||||
<div style={card}>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>生产提交</strong>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>提交生产任务</strong>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: 1, minWidth: 180 }}>
|
||||
@@ -498,7 +542,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<input
|
||||
value={rootDir}
|
||||
onChange={event => setRootDir(event.target.value)}
|
||||
placeholder="批量根目录或单个任务目录"
|
||||
placeholder="批处理根目录或单个任务目录"
|
||||
disabled={readOnly}
|
||||
style={{
|
||||
width: '100%',
|
||||
@@ -515,7 +559,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ minWidth: 120 }}>
|
||||
<label style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 4 }}>
|
||||
处理任务数(0=全部)
|
||||
任务数量(0 表示全部)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -528,7 +572,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
<div style={{ minWidth: 140 }}>
|
||||
<label style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 4 }}>
|
||||
超时秒数(留空默认)
|
||||
超时时间(秒,可选)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -565,7 +609,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
这些参数主要影响目标网格尺寸、精轨裁剪、地理编码范围和位移结果掩膜。建议先使用默认值,通常优先只调整目标网格尺寸;只有在边缘被裁切、时间窗口异常或噪声较多时,再继续调整其他参数。
|
||||
这些参数主要影响目标网格大小、精裁剪范围、地理编码范围和位移结果掩膜。建议先使用默认值,通常优先只调整目标网格大小;只有在边缘被裁切、时间窗异常或噪声较多时,再继续调整其他参数。
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
{Object.entries(currentParamSchema).map(([name, schema]) => (
|
||||
@@ -600,7 +644,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
{submitting ? '提交中...' : '提交任务'}
|
||||
</button>
|
||||
{submitMsg && (
|
||||
<span style={{ fontSize: 12, color: submitMsg.includes('失败') ? '#ef4444' : '#16a34a' }}>
|
||||
<span style={{ fontSize: 12, color: submitError ? '#ef4444' : '#16a34a' }}>
|
||||
{submitMsg}
|
||||
</span>
|
||||
)}
|
||||
@@ -651,7 +695,26 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 8, background: '#fff', border: '1px solid #fde68a', borderRadius: 6, padding: '8px 10px' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#92400e', marginBottom: 6 }}>任务日志</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#92400e' }}>任务日志</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={handleClearTaskLogs}
|
||||
disabled={taskLogActionLoading || taskLogs.length === 0}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #fcd34d',
|
||||
background: taskLogActionLoading || taskLogs.length === 0 ? '#fef3c7' : '#fff7ed',
|
||||
color: '#9a3412',
|
||||
cursor: taskLogActionLoading || taskLogs.length === 0 ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{taskLogActionLoading && taskLogDeletingId == null ? '清空中...' : '清空日志'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{taskLogsLoading ? (
|
||||
<div style={{ fontSize: 11, color: '#a16207' }}>加载中...</div>
|
||||
) : taskLogs.length === 0 ? (
|
||||
@@ -660,13 +723,38 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<div style={{ maxHeight: 220, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{taskLogs.map((log, index) => (
|
||||
<div
|
||||
key={`${log.timestamp || 'log'}-${index}`}
|
||||
style={{ fontSize: 11, lineHeight: 1.45, color: log.level === 'WARNING' ? '#b45309' : '#334155' }}
|
||||
key={log.id || `${log.timestamp || 'log'}-${index}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#64748b' }}>
|
||||
{(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: 11, lineHeight: 1.45, color: log.level === 'WARNING' ? '#b45309' : '#334155' }}>
|
||||
<div style={{ color: '#64748b' }}>
|
||||
{(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log.message}</div>
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log.message}</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => handleDeleteTaskLog(log.id)}
|
||||
disabled={taskLogActionLoading || !log.id}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
fontSize: 11,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #fecaca',
|
||||
background: '#fef2f2',
|
||||
color: '#b91c1c',
|
||||
cursor: taskLogActionLoading || !log.id ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{taskLogDeletingId === log.id ? '删除中...' : '删除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -675,7 +763,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 6 }}>最近20条运行记录</div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 6 }}>最近 20 条运行记录</div>
|
||||
{runsLoading ? (
|
||||
<div style={{ fontSize: 12, color: '#94a3b8' }}>加载中...</div>
|
||||
) : runs.length === 0 ? (
|
||||
@@ -684,7 +772,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#f8fafc' }}>
|
||||
{['任务编号', '引擎', '状态', '时间', '操作'].map(header => (
|
||||
{['运行ID', '引擎', '状态', '时间', '操作'].map(header => (
|
||||
<th
|
||||
key={header}
|
||||
style={{ padding: '4px 8px', textAlign: 'left', borderBottom: '1px solid #e2e8f0', color: '#64748b' }}
|
||||
@@ -722,7 +810,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
background: '#f8fafc',
|
||||
}}
|
||||
>
|
||||
日志
|
||||
查看日志
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -734,3 +822,4 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { scanDinsarResults } from './api/dinsar';
|
||||
import { extractDispResults, getActiveTasks, getTaskLogs } from './api/idl';
|
||||
import { extractDispResults } from './api/idl';
|
||||
import { clearTaskLogs, deleteTaskLog, getActiveTasks, getTaskLogs } from './api/tasks';
|
||||
import DinsarCatalogPanel from './components/DinsarCatalogPanel';
|
||||
|
||||
const card = {
|
||||
@@ -19,9 +20,9 @@ const PRODUCT_TASK_TYPES = [
|
||||
];
|
||||
|
||||
const TASK_TYPE_LABEL = {
|
||||
SCAN_DINSAR: 'D-InSAR结果扫描任务',
|
||||
PUBLISH_DINSAR_PRODUCTS: '结果包发布任务',
|
||||
REBUILD_DINSAR_CATALOG: '结果目录重建任务',
|
||||
SCAN_DINSAR: 'D-InSAR结果扫描',
|
||||
PUBLISH_DINSAR_PRODUCTS: 'D-InSAR产物发布',
|
||||
REBUILD_DINSAR_CATALOG: 'D-InSAR目录重建',
|
||||
};
|
||||
|
||||
const STATUS_LABEL = {
|
||||
@@ -53,6 +54,8 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
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 loadActiveTask = useCallback(async () => {
|
||||
try {
|
||||
@@ -81,6 +84,42 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 (error) {
|
||||
setActionMessage(`删除日志失败:${error?.response?.data?.detail || error.message}`);
|
||||
setActionError(true);
|
||||
} 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 (error) {
|
||||
setActionMessage(`清空日志失败:${error?.response?.data?.detail || error.message}`);
|
||||
setActionError(true);
|
||||
} finally {
|
||||
setTaskLogActionLoading(false);
|
||||
}
|
||||
}, [activeTask?.task_id, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
|
||||
|
||||
useEffect(() => {
|
||||
loadActiveTask();
|
||||
}, [loadActiveTask]);
|
||||
@@ -137,7 +176,7 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
return (
|
||||
<div style={{ padding: '16px', maxWidth: 960 }}>
|
||||
<div style={card}>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>产物提取与重扫</strong>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>D-InSAR 产物提取与重扫</strong>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@@ -151,20 +190,20 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
这里负责把生产目录中的位移结果提取为标准结果包,并触发结果重扫、发布和编目。生产运行与参数配置已独立放到“D-InSAR生产”选项卡。
|
||||
这里负责把生产目录中的位移结果提取为标准成果包,并触发结果重扫、发布和编目。生产运行与参数配置已独立放到“D-InSAR生产”选项卡。
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||
<input
|
||||
value={extractRootDir}
|
||||
onChange={event => setExtractRootDir(event.target.value)}
|
||||
placeholder="结果根目录(提取位移结果)"
|
||||
placeholder="结果根目录"
|
||||
style={{ flex: 2, minWidth: 220, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }}
|
||||
/>
|
||||
<input
|
||||
value={extractDestDir}
|
||||
onChange={event => setExtractDestDir(event.target.value)}
|
||||
placeholder="目标目录(留空使用默认)"
|
||||
placeholder="目标目录(可选)"
|
||||
style={{ flex: 1, minWidth: 180, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }}
|
||||
/>
|
||||
<button
|
||||
@@ -209,15 +248,17 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
<span style={{ color: '#ef4444' }}>提取失败:{extractResult.error}</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, color: '#16a34a' }}>
|
||||
<span>提取完成:复制 {extractResult.copied || 0} 个文件,覆盖 {extractResult.overwritten || 0} 个文件。</span>
|
||||
<span>
|
||||
提取完成:复制 {extractResult.copied || 0} 个文件,覆盖 {extractResult.overwritten || 0} 个文件。
|
||||
</span>
|
||||
{extractResult.catalog?.attempted && extractResult.catalog?.status === 'ok' && (
|
||||
<span style={{ color: '#166534' }}>
|
||||
已同步标准结果包目录:发布 {extractResult.catalog?.publish?.processed || 0} 项,重建登记 {extractResult.catalog?.rebuild?.registered || 0} 项。
|
||||
成果目录已同步:发布 {extractResult.catalog?.publish?.processed || 0} 项,重建登记 {extractResult.catalog?.rebuild?.registered || 0} 项。
|
||||
</span>
|
||||
)}
|
||||
{extractResult.catalog?.attempted && extractResult.catalog?.status === 'error' && (
|
||||
<span style={{ color: '#b45309' }}>
|
||||
标准结果包目录同步失败:{extractResult.catalog?.message}
|
||||
标准成果包目录同步失败:{extractResult.catalog?.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -245,7 +286,7 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
</div>
|
||||
|
||||
{!activeTask ? (
|
||||
<div style={{ fontSize: 12, color: '#94a3b8' }}>当前没有运行中的产物处理任务。</div>
|
||||
<div style={{ fontSize: 12, color: '#94a3b8' }}>当前没有正在执行的产物处理任务。</div>
|
||||
) : (
|
||||
<div style={{ padding: '8px 10px', background: '#fefce8', borderRadius: 6, border: '1px solid #fde68a' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#92400e', marginBottom: 4 }}>当前任务</div>
|
||||
@@ -269,7 +310,26 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 8, background: '#fff', border: '1px solid #fde68a', borderRadius: 6, padding: '8px 10px' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#92400e', marginBottom: 6 }}>任务日志</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#92400e' }}>任务日志</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={handleClearTaskLogs}
|
||||
disabled={taskLogActionLoading || taskLogs.length === 0}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #fcd34d',
|
||||
background: taskLogActionLoading || taskLogs.length === 0 ? '#fef3c7' : '#fff7ed',
|
||||
color: '#9a3412',
|
||||
cursor: taskLogActionLoading || taskLogs.length === 0 ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{taskLogActionLoading && taskLogDeletingId == null ? '清空中...' : '清空日志'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{taskLogsLoading ? (
|
||||
<div style={{ fontSize: 11, color: '#a16207' }}>加载中...</div>
|
||||
) : taskLogs.length === 0 ? (
|
||||
@@ -278,13 +338,38 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
<div style={{ maxHeight: 220, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{taskLogs.map((log, index) => (
|
||||
<div
|
||||
key={`${log.timestamp || 'log'}-${index}`}
|
||||
style={{ fontSize: 11, lineHeight: 1.45, color: log.level === 'WARNING' ? '#b45309' : '#334155' }}
|
||||
key={log.id || `${log.timestamp || 'log'}-${index}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#64748b' }}>
|
||||
{(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: 11, lineHeight: 1.45, color: log.level === 'WARNING' ? '#b45309' : '#334155' }}>
|
||||
<div style={{ color: '#64748b' }}>
|
||||
{(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log.message}</div>
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log.message}</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => handleDeleteTaskLog(log.id)}
|
||||
disabled={taskLogActionLoading || !log.id}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
fontSize: 11,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #fecaca',
|
||||
background: '#fef2f2',
|
||||
color: '#b91c1c',
|
||||
cursor: taskLogActionLoading || !log.id ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{taskLogDeletingId === log.id ? '删除中...' : '删除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -302,3 +387,4 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cleanupSessions } from './api/auth';
|
||||
import { syncWaterScenesFromDisk } from './api/water';
|
||||
import { listEngines, runWslCheck } from './api/dinsarProduction';
|
||||
import { getOrbitStatus, syncOrbitPools } from './api/orbit';
|
||||
import LogManagementPanel from './LogManagementPanel';
|
||||
import LogManagementPanel from './LogManagementPanel.clean';
|
||||
import DinsarCatalogPanel from './components/DinsarCatalogPanel';
|
||||
|
||||
const toNumber = (value) => {
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { listLogs, getLogContent, deleteLog } from './api/logs';
|
||||
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
const LogManagementPanel = ({ isAdmin }) => {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedLog, setSelectedLog] = useState(null);
|
||||
const [logContent, setLogContent] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [filterType, setFilterType] = useState('');
|
||||
const [totalLines, setTotalLines] = useState(0);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const loadLogs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await listLogs(filterType || null);
|
||||
setLogs(data);
|
||||
} catch (error) {
|
||||
console.error('加载日志列表失败:', error);
|
||||
alert(`加载日志列表失败:${error.response?.data?.detail || error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterType]);
|
||||
|
||||
const loadLogContent = useCallback(async (logPath, offset = 0) => {
|
||||
try {
|
||||
const data = await getLogContent(logPath, offset, PAGE_SIZE);
|
||||
setLogContent(data.content || '');
|
||||
setTotalLines(data.total_lines || 0);
|
||||
setCurrentOffset(offset);
|
||||
} catch (error) {
|
||||
console.error('加载日志内容失败:', error);
|
||||
alert(`加载日志内容失败:${error.response?.data?.detail || error.message}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs();
|
||||
}, [loadLogs]);
|
||||
|
||||
const handleViewLog = async log => {
|
||||
setSelectedLog(log);
|
||||
setShowModal(true);
|
||||
setCurrentOffset(0);
|
||||
setSearchTerm('');
|
||||
await loadLogContent(log.path, 0);
|
||||
};
|
||||
|
||||
const handleDeleteLog = async log => {
|
||||
if (!isAdmin) {
|
||||
alert('只有管理员可以删除日志。');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`确定要删除日志文件“${log.name}”吗?\n\n此操作不可恢复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteLog(log.path);
|
||||
alert('日志文件已删除。');
|
||||
await loadLogs();
|
||||
if (selectedLog && selectedLog.path === log.path) {
|
||||
setShowModal(false);
|
||||
setSelectedLog(null);
|
||||
setLogContent('');
|
||||
setTotalLines(0);
|
||||
setCurrentOffset(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除日志失败:', error);
|
||||
alert(`删除日志失败:${error.response?.data?.detail || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (selectedLog && currentOffset > 0) {
|
||||
const newOffset = Math.max(0, currentOffset - PAGE_SIZE);
|
||||
loadLogContent(selectedLog.path, newOffset);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (selectedLog && currentOffset + PAGE_SIZE < totalLines) {
|
||||
const newOffset = currentOffset + PAGE_SIZE;
|
||||
loadLogContent(selectedLog.path, newOffset);
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = bytes => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
const getTypeLabel = type => {
|
||||
const labels = {
|
||||
app: '应用日志',
|
||||
task: '任务日志',
|
||||
error: '错误日志',
|
||||
other: '其他',
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
const getTypeColor = type => {
|
||||
const colors = {
|
||||
app: '#3b82f6',
|
||||
task: '#10b981',
|
||||
error: '#ef4444',
|
||||
other: '#6b7280',
|
||||
};
|
||||
return colors[type] || '#6b7280';
|
||||
};
|
||||
|
||||
const filteredContent = searchTerm
|
||||
? logContent
|
||||
.split('\n')
|
||||
.filter(line => line.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
.join('\n')
|
||||
: logContent;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 style={{ margin: 0 }}>日志管理</h3>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<label>类型筛选:</label>
|
||||
<select
|
||||
value={filterType}
|
||||
onChange={event => setFilterType(event.target.value)}
|
||||
style={{ padding: '5px 10px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="app">应用日志</option>
|
||||
<option value="task">任务日志</option>
|
||||
<option value="error">错误日志</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={loadLogs}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '5px 15px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{loading ? '加载中...' : '刷新'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logs.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#6b7280' }}>暂无日志文件</div>
|
||||
) : (
|
||||
<table
|
||||
style={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f3f4f6', borderBottom: '2px solid #e5e7eb' }}>
|
||||
<th style={{ padding: '12px', textAlign: 'left' }}>文件名</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left' }}>类型</th>
|
||||
<th style={{ padding: '12px', textAlign: 'right' }}>大小</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left' }}>修改时间</th>
|
||||
<th style={{ padding: '12px', textAlign: 'center' }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((log, index) => (
|
||||
<tr key={index} style={{ borderBottom: '1px solid #e5e7eb' }}>
|
||||
<td style={{ padding: '12px', fontFamily: 'monospace', fontSize: '13px' }}>{log.name}</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: `${getTypeColor(log.type)}20`,
|
||||
color: getTypeColor(log.type),
|
||||
}}
|
||||
>
|
||||
{getTypeLabel(log.type)}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px', textAlign: 'right', fontFamily: 'monospace', fontSize: '13px' }}>
|
||||
{formatSize(log.size)}
|
||||
</td>
|
||||
<td style={{ padding: '12px', fontSize: '13px' }}>{log.modified_at}</td>
|
||||
<td style={{ padding: '12px', textAlign: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<button
|
||||
onClick={() => handleViewLog(log)}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => handleDeleteLog(log)}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
backgroundColor: '#ef4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{showModal && selectedLog && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
width: '90%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '90vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontFamily: 'monospace' }}>{selectedLog.name}</h3>
|
||||
<div style={{ fontSize: '13px', color: '#6b7280' }}>
|
||||
大小:{formatSize(selectedLog.size)} | 修改时间:{selectedLog.modified_at} | 总行数:{totalLines}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#6b7280',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '12px 20px', borderBottom: '1px solid #e5e7eb', display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索日志内容..."
|
||||
value={searchTerm}
|
||||
onChange={event => setSearchTerm(event.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
/>
|
||||
<div style={{ fontSize: '13px', color: '#6b7280' }}>
|
||||
显示行 {totalLines === 0 ? 0 : currentOffset + 1} - {Math.min(currentOffset + PAGE_SIZE, totalLines)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePrevPage}
|
||||
disabled={currentOffset === 0}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: currentOffset === 0 ? '#e5e7eb' : '#3b82f6',
|
||||
color: currentOffset === 0 ? '#9ca3af' : 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: currentOffset === 0 ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={currentOffset + PAGE_SIZE >= totalLines}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: currentOffset + PAGE_SIZE >= totalLines ? '#e5e7eb' : '#3b82f6',
|
||||
color: currentOffset + PAGE_SIZE >= totalLines ? '#9ca3af' : 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: currentOffset + PAGE_SIZE >= totalLines ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: '20px', backgroundColor: '#1e1e1e' }}>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: '#d4d4d4',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{filteredContent || '(空日志)'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogManagementPanel;
|
||||
@@ -2,3 +2,9 @@ import apiClient from './client';
|
||||
|
||||
export const getActiveTasks = () => apiClient.get('/tasks/active').then(r => r.data);
|
||||
export const getTask = (taskId) => apiClient.get(`/tasks/${taskId}`).then(r => r.data);
|
||||
export const getTaskLogs = (taskId, limit = 50, offset = 0) =>
|
||||
apiClient.get(`/tasks/${taskId}/logs?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`).then(r => r.data);
|
||||
export const deleteTaskLog = (taskId, logId) =>
|
||||
apiClient.delete(`/tasks/${taskId}/logs/${encodeURIComponent(logId)}`).then(r => r.data);
|
||||
export const clearTaskLogs = (taskId) =>
|
||||
apiClient.delete(`/tasks/${taskId}/logs`).then(r => r.data);
|
||||
|
||||
Reference in New Issue
Block a user