Fix D-InSAR export UI and task folder isolation

This commit is contained in:
2026-04-17 00:18:03 +08:00
parent bc5ed74d4d
commit 6e98bbe8b0
7 changed files with 335 additions and 97 deletions
+24 -5
View File
@@ -3709,6 +3709,12 @@ input[type="checkbox"] {
gap: 6px;
cursor: pointer;
}
.export-select-hint {
margin-bottom: 8px;
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
}
.export-result-list {
list-style: none;
padding: 0;
@@ -3780,15 +3786,28 @@ input[type="checkbox"] {
padding-top: 12px;
border-top: 1px solid var(--color-border);
}
.result-export-modal .modal-footer .btn-primary {
background: var(--color-primary);
color: white;
border: none;
.result-export-modal .modal-footer button {
min-width: 108px;
padding: 6px 16px;
border-radius: 4px;
border: 1px solid var(--color-border);
background: #fff;
color: var(--color-text-primary);
cursor: pointer;
}
.result-export-modal .modal-footer .btn-primary:disabled {
.result-export-modal .modal-footer .btn-secondary:hover:not(:disabled) {
background: var(--color-panel-muted);
}
.result-export-modal .modal-footer .btn-primary {
background: var(--color-accent);
color: #fff;
border: none;
font-weight: 600;
}
.result-export-modal .modal-footer .btn-primary:hover:not(:disabled) {
background: var(--color-accent-strong);
}
.result-export-modal .modal-footer button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
+22 -26
View File
@@ -284,9 +284,12 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
setRunsLoading(true);
try {
const data = await listRuns(20);
setRuns(data.runs || []);
const nextRuns = data.runs || [];
setRuns(nextRuns);
return nextRuns;
} catch {
setRuns([]);
return [];
} finally {
setRunsLoading(false);
}
@@ -298,8 +301,10 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
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;
}
}, []);
@@ -355,27 +360,19 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
}
}, [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();
loadRuns();
loadActiveTask();
}, [loadActiveTask, loadEngines, loadRuns]);
useEffect(() => {
const timer = setInterval(() => {
loadRuns();
loadActiveTask();
}, 5000);
return () => clearInterval(timer);
}, [loadActiveTask, loadRuns]);
useEffect(() => {
const taskId = logTaskId;
loadTaskLogs(taskId);
if (!taskId) return undefined;
const timer = setInterval(() => loadTaskLogs(taskId), 5000);
return () => clearInterval(timer);
}, [logTaskId, loadTaskLogs]);
refreshMonitor();
}, [loadEngines, refreshMonitor]);
useEffect(() => {
if (currentProfiles.length > 0) {
@@ -418,8 +415,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
setSubmitError(false);
setSubmitMsg(`任务已入队:${result.task_id}${taskCount}`);
if (onJobQueued) onJobQueued(result.task_id);
loadRuns();
loadActiveTask();
await refreshMonitor();
} catch (err) {
setSubmitError(true);
setSubmitMsg(`提交失败:${err?.response?.data?.detail || err.message}`);
@@ -672,10 +668,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<strong style={{ fontSize: 14 }}>运行监控</strong>
<button
onClick={() => {
loadRuns();
loadActiveTask();
}}
onClick={refreshMonitor}
style={{
fontSize: 12,
padding: '3px 10px',
@@ -688,6 +681,9 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
刷新
</button>
</div>
<div style={{ fontSize: 11, color: '#94a3b8', marginBottom: 10 }}>
监控与日志改为手动刷新避免界面持续轮询请求
</div>
{monitoredTask && (
<div
+21 -25
View File
@@ -67,17 +67,22 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
const tasks = Array.isArray(data) ? data : (data?.tasks || []);
const relevantTask = tasks.find(task => PRODUCT_TASK_TYPES.includes(task.task_type)) || null;
setActiveTask(relevantTask);
return relevantTask;
} catch {
setActiveTask(null);
return null;
}
}, []);
const loadRecentTask = useCallback(async () => {
try {
const tasks = await getRecentTasks(PRODUCT_TASK_TYPES, [], 1, 0);
setRecentTask(Array.isArray(tasks) ? (tasks[0] || null) : null);
const nextTask = Array.isArray(tasks) ? (tasks[0] || null) : null;
setRecentTask(nextTask);
return nextTask;
} catch {
setRecentTask(null);
return null;
}
}, []);
@@ -133,26 +138,18 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
}
}, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
useEffect(() => {
loadActiveTask();
loadRecentTask();
}, [loadActiveTask, loadRecentTask]);
const refreshMonitor = useCallback(async () => {
const [nextActiveTask, nextRecentTask] = await Promise.all([
loadActiveTask(),
loadRecentTask(),
]);
const nextTaskId = nextActiveTask?.task_id || nextRecentTask?.task_id || '';
await loadTaskLogs(nextTaskId);
}, [loadActiveTask, loadRecentTask, loadTaskLogs]);
useEffect(() => {
const timer = setInterval(() => {
loadActiveTask();
loadRecentTask();
}, 5000);
return () => clearInterval(timer);
}, [loadActiveTask, loadRecentTask]);
useEffect(() => {
const taskId = logTaskId;
loadTaskLogs(taskId);
if (!taskId) return undefined;
const timer = setInterval(() => loadTaskLogs(taskId), 5000);
return () => clearInterval(timer);
}, [logTaskId, loadTaskLogs]);
refreshMonitor();
}, [refreshMonitor]);
const handleExtract = async () => {
if (!extractRootDir.trim()) return;
@@ -181,8 +178,7 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
if (result?.task_id) {
onJobQueued?.(result.task_id);
}
loadActiveTask();
loadRecentTask();
await refreshMonitor();
} catch (err) {
setActionError(true);
setActionMessage(err?.response?.data?.detail || err.message || 'D-InSAR结果扫描失败');
@@ -289,10 +285,7 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<strong style={{ fontSize: 14 }}>产物任务监控</strong>
<button
onClick={() => {
loadActiveTask();
loadRecentTask();
}}
onClick={refreshMonitor}
style={{
fontSize: 12,
padding: '3px 10px',
@@ -305,6 +298,9 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
刷新
</button>
</div>
<div style={{ fontSize: 11, color: '#94a3b8', marginBottom: 10 }}>
任务状态与日志改为手动刷新避免页面持续轮询
</div>
{!monitoredTask ? (
<div style={{ fontSize: 12, color: '#94a3b8' }}>当前没有正在执行的产物处理任务</div>
@@ -133,6 +133,7 @@ export default function DinsarCatalogPanel({
setSelectedProduct(null);
return;
}
setSelectedProduct(null);
setDetailLoading(true);
try {
const detail = await getDinsarProductDetail(productId);
@@ -148,13 +149,7 @@ export default function DinsarCatalogPanel({
useEffect(() => {
loadCatalog();
const timer = setInterval(() => {
if (!actionLoading) {
loadCatalog();
}
}, 10000);
return () => clearInterval(timer);
}, [actionLoading, loadCatalog]);
}, [loadCatalog]);
useEffect(() => {
loadProductDetail(selectedProductId);
@@ -408,7 +403,7 @@ export default function DinsarCatalogPanel({
</div>
{!selectedProductId ? (
<div style={{ padding: '12px', fontSize: 12, color: '#94a3b8' }}>请选择一个结果包查看详情</div>
) : detailLoading ? (
) : detailLoading || !selectedProduct ? (
<div style={{ padding: '12px', fontSize: 12, color: '#94a3b8' }}>正在加载详情...</div>
) : selectedProduct?.error ? (
<div style={{ padding: '12px', fontSize: 12, color: '#dc2626' }}>{selectedProduct.error}</div>
+44 -28
View File
@@ -1,18 +1,23 @@
import { useState } from 'react';
import { exportDinsarResults } from '../api/dinsar';
export default function ResultExportModal({ results, onClose }) {
const EXAMPLE_TARGET_DIR = String.raw`例如: D:\Export\Results 或 \\server\share\results`;
export default function ResultExportModal({ results = [], onClose }) {
const [targetDir, setTargetDir] = useState('');
const [selectedIds, setSelectedIds] = useState(() => new Set(results.map(r => r.id)));
const [selectedIds, setSelectedIds] = useState(() => new Set(results.map((result) => result.id)));
const [exporting, setExporting] = useState(false);
const [exportResult, setExportResult] = useState(null);
const [error, setError] = useState('');
const toggleSelect = (id) => {
setSelectedIds(prev => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
@@ -20,29 +25,30 @@ export default function ResultExportModal({ results, onClose }) {
const toggleAll = () => {
if (selectedIds.size === results.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(results.map(r => r.id)));
return;
}
setSelectedIds(new Set(results.map((result) => result.id)));
};
const handleExport = async () => {
const dir = targetDir.trim();
if (!dir) {
setError('请输入目标路径');
setError('请输入目标路径');
return;
}
if (selectedIds.size === 0) {
setError('请至少选择一个结果');
setError('请至少选择一个结果');
return;
}
setError('');
setExporting(true);
setExportResult(null);
try {
const res = await exportDinsarResults([...selectedIds], dir);
setExportResult(res);
} catch (e) {
setError(e.response?.data?.detail || e.message || '导出失败');
const response = await exportDinsarResults([...selectedIds], dir);
setExportResult(response);
} catch (eventualError) {
setError(eventualError.response?.data?.detail || eventualError.message || '提取失败');
} finally {
setExporting(false);
}
@@ -50,20 +56,22 @@ export default function ResultExportModal({ results, onClose }) {
return (
<div className="modal-overlay visible" onClick={onClose}>
<div className="modal-content result-export-modal" onClick={e => e.stopPropagation()}>
<div className="modal-content result-export-modal" onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h3>提取 D-InSAR 结果</h3>
<button className="modal-close-btn" onClick={onClose}>&times;</button>
<button type="button" className="modal-close-btn" onClick={onClose} aria-label="关闭">
&times;
</button>
</div>
<div className="modal-body">
<div className="export-path-section">
<label>目标路径支持 UNC 路径 \\\\server\\share\\path</label>
<label>目标路径支持本地盘符或 UNC 路径 `D:\Export\Results`</label>
<input
type="text"
value={targetDir}
onChange={e => setTargetDir(e.target.value)}
placeholder="例如: D:\Export\Results 或 \\server\share\results"
onChange={(event) => setTargetDir(event.target.value)}
placeholder={EXAMPLE_TARGET_DIR}
disabled={exporting}
className="export-path-input"
/>
@@ -76,23 +84,28 @@ export default function ResultExportModal({ results, onClose }) {
type="checkbox"
checked={selectedIds.size === results.length && results.length > 0}
onChange={toggleAll}
disabled={exporting}
disabled={exporting || results.length === 0}
/>
全选 ({selectedIds.size}/{results.length})
</label>
</div>
<div className="export-select-hint">
导出时会优先按任务名创建子目录如果同名结果已存在且内容不同会自动追加后缀避免覆盖
</div>
<ul className="export-result-list">
{results.map(r => (
<li key={r.id} className="export-result-item">
{results.map((result) => (
<li key={result.id} className="export-result-item">
<label>
<input
type="checkbox"
checked={selectedIds.has(r.id)}
onChange={() => toggleSelect(r.id)}
checked={selectedIds.has(result.id)}
onChange={() => toggleSelect(result.id)}
disabled={exporting}
/>
<span className="export-result-name" title={r.file_path || r.name}>
{r.name}
<span className="export-result-name" title={result.file_path || result.name}>
{result.name}
</span>
</label>
</li>
@@ -104,7 +117,7 @@ export default function ResultExportModal({ results, onClose }) {
{exportResult && (
<div className="export-summary">
<div className="export-summary-title">导出完成</div>
<div className="export-summary-title">提取完成</div>
<div className="export-summary-stats">
<span className="stat-ok">复制: {exportResult.copied}</span>
<span className="stat-skip">跳过: {exportResult.skipped}</span>
@@ -120,13 +133,16 @@ export default function ResultExportModal({ results, onClose }) {
</div>
<div className="modal-footer">
<button onClick={onClose} disabled={exporting}>关闭</button>
<button type="button" className="btn-secondary" onClick={onClose} disabled={exporting}>
关闭
</button>
<button
type="button"
onClick={handleExport}
disabled={exporting || selectedIds.size === 0 || !targetDir.trim()}
className="btn-primary"
>
{exporting ? '导出中...' : `提取 ${selectedIds.size} 个结果`}
{exporting ? '提取中...' : `确定提取 ${selectedIds.size} 个结果`}
</button>
</div>
</div>