Add Sentinel-1 asset management and PyINT pipeline support
This commit is contained in:
@@ -2400,6 +2400,237 @@ input[type="checkbox"] {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.asset-inventory-panel {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.asset-toolbar h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.asset-toolbar p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.asset-actions select,
|
||||
.asset-actions button {
|
||||
height: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-message {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent-strong);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.asset-message--error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.asset-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-metric {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.asset-metric span,
|
||||
.asset-metric small {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-metric strong {
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
font-size: 20px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.asset-root-strip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.asset-root-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 7px 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-root-item strong,
|
||||
.asset-root-item span {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-root-item span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-tabbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.asset-tabbar button {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
padding: 7px 6px;
|
||||
}
|
||||
|
||||
.asset-tabbar button.active-tab {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
border-color: var(--color-accent-strong);
|
||||
}
|
||||
|
||||
.asset-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-table {
|
||||
width: 100%;
|
||||
min-width: 760px;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-table th,
|
||||
.asset-table td {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 7px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.asset-table th {
|
||||
background: #f8fafc;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.asset-table td small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-inline-action {
|
||||
min-width: 46px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.asset-action-placeholder {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
border-radius: 999px;
|
||||
padding: 2px 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-badge--ok {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.asset-badge--warn {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.asset-badge--bad {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.asset-badge--neutral {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.asset-pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-pager button {
|
||||
padding: 5px 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.top-status-bar {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
getAssetInventoryStatus,
|
||||
listAssetIssues,
|
||||
listOrbitAssets,
|
||||
listSourceAssets,
|
||||
scanAssetInventory,
|
||||
} from './api/assets';
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
const fmtDateTime = (value) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const fmtBytes = (value) => {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) return '-';
|
||||
if (n >= 1024 ** 3) return `${(n / (1024 ** 3)).toFixed(2)} GB`;
|
||||
if (n >= 1024 ** 2) return `${(n / (1024 ** 2)).toFixed(1)} MB`;
|
||||
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
return `${n} B`;
|
||||
};
|
||||
|
||||
const StatusBadge = ({ value }) => {
|
||||
const text = String(value || '-');
|
||||
const status = text.toUpperCase();
|
||||
const tone = status === 'OK' || status === 'MATCHED' || status === 'SELECTED'
|
||||
? 'ok'
|
||||
: status === 'WARNING' || status === 'OPEN' || status === 'MISSING'
|
||||
? 'warn'
|
||||
: status === 'FAILED' || status === 'INACCESSIBLE' || status === 'ERROR'
|
||||
? 'bad'
|
||||
: 'neutral';
|
||||
return <span className={`asset-badge asset-badge--${tone}`}>{text}</span>;
|
||||
};
|
||||
|
||||
const Metric = ({ label, value, hint }) => (
|
||||
<div className="asset-metric">
|
||||
<span>{label}</span>
|
||||
<strong>{value ?? 0}</strong>
|
||||
{hint ? <small>{hint}</small> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function AssetInventoryPanel({ readOnly = false, onTaskStart }) {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [sources, setSources] = useState({ items: [], total: 0, offset: 0, has_more: false });
|
||||
const [orbits, setOrbits] = useState({ items: [], total: 0, offset: 0, has_more: false });
|
||||
const [issues, setIssues] = useState({ items: [], total: 0, offset: 0, has_more: false });
|
||||
const [activeTab, setActiveTab] = useState('sources');
|
||||
const [family, setFamily] = useState('all');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scanLoading, setScanLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const familyParam = useMemo(() => (family === 'all' ? undefined : family), [family]);
|
||||
|
||||
const refresh = useCallback(async ({ sourceOffset = 0, orbitOffset = 0, issueOffset = 0 } = {}) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [nextStatus, nextSources, nextOrbits, nextIssues] = await Promise.all([
|
||||
getAssetInventoryStatus(),
|
||||
listSourceAssets({ satellite_family: familyParam, limit: PAGE_SIZE, offset: sourceOffset }),
|
||||
listOrbitAssets({ satellite_family: familyParam, limit: PAGE_SIZE, offset: orbitOffset }),
|
||||
listAssetIssues({ status: 'OPEN', limit: PAGE_SIZE, offset: issueOffset }),
|
||||
]);
|
||||
setStatus(nextStatus);
|
||||
setSources(nextSources);
|
||||
setOrbits(nextOrbits);
|
||||
setIssues(nextIssues);
|
||||
} catch (err) {
|
||||
setError(err?.response?.data?.detail || err.message || '加载资产库存失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [familyParam]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleScan = async () => {
|
||||
if (readOnly || scanLoading) return;
|
||||
setScanLoading(true);
|
||||
setMessage('');
|
||||
setError('');
|
||||
try {
|
||||
const result = await scanAssetInventory({ inventory_types: [], root_ids: [], bind_orbits: true });
|
||||
setMessage(`资产扫描任务已入队: ${result.task_id}`);
|
||||
onTaskStart?.(result.task_id, '源数据/精轨资产扫描已入队', {
|
||||
taskType: 'SCAN_ASSET_INVENTORY',
|
||||
nonBlocking: true,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err?.response?.data?.detail || err.message || '启动资产扫描失败');
|
||||
} finally {
|
||||
setScanLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const states = status?.states || [];
|
||||
const sourceRoots = states.filter(item => item.inventory_type === 'source_product');
|
||||
const orbitRoots = states.filter(item => item.inventory_type === 'orbit_asset');
|
||||
|
||||
const renderPager = (data, onPage) => (
|
||||
<div className="asset-pager">
|
||||
<button type="button" disabled={data.offset <= 0 || loading} onClick={() => onPage(Math.max(0, data.offset - PAGE_SIZE))}>
|
||||
上一页
|
||||
</button>
|
||||
<span>{data.offset + 1}-{data.offset + data.items.length} / {data.total}</span>
|
||||
<button type="button" disabled={!data.has_more || loading} onClick={() => onPage(data.offset + PAGE_SIZE)}>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="asset-inventory-panel">
|
||||
<div className="asset-toolbar">
|
||||
<div>
|
||||
<h3>源数据与精轨资产</h3>
|
||||
<p>Sentinel-1 与 LT-1 的源产品、精密轨道和绑定状态</p>
|
||||
</div>
|
||||
<div className="asset-actions">
|
||||
<select value={family} onChange={(e) => setFamily(e.target.value)} disabled={loading}>
|
||||
<option value="all">全部卫星族</option>
|
||||
<option value="S1">Sentinel-1</option>
|
||||
<option value="LT1">LT-1</option>
|
||||
</select>
|
||||
<button type="button" onClick={() => refresh()} disabled={loading}>刷新</button>
|
||||
<button type="button" onClick={handleScan} disabled={readOnly || scanLoading}>扫描资产</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className="asset-message asset-message--error">{error}</div> : null}
|
||||
{message ? <div className="asset-message">{message}</div> : null}
|
||||
|
||||
<div className="asset-metrics">
|
||||
<Metric label="源产品" value={status?.source_asset_count} hint={`${sourceRoots.length} 个源数据根`} />
|
||||
<Metric label="精轨资产" value={status?.orbit_asset_count} hint={`${orbitRoots.length} 个精轨根`} />
|
||||
<Metric label="已绑定场景" value={status?.selected_binding_count} />
|
||||
<Metric label="开放问题" value={status?.open_issue_count} />
|
||||
</div>
|
||||
|
||||
<div className="asset-root-strip">
|
||||
{states.map((item) => (
|
||||
<div className="asset-root-item" key={`${item.inventory_type}-${item.root_ref_id}`}>
|
||||
<div>
|
||||
<strong>{item.inventory_type === 'source_product' ? '源数据池' : '精轨池'}</strong>
|
||||
<span title={item.root_path}>{item.root_path}</span>
|
||||
</div>
|
||||
<StatusBadge value={item.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="asset-tabbar">
|
||||
<button type="button" className={activeTab === 'sources' ? 'active-tab' : ''} onClick={() => setActiveTab('sources')}>
|
||||
源产品 ({sources.total})
|
||||
</button>
|
||||
<button type="button" className={activeTab === 'orbits' ? 'active-tab' : ''} onClick={() => setActiveTab('orbits')}>
|
||||
精轨 ({orbits.total})
|
||||
</button>
|
||||
<button type="button" className={activeTab === 'issues' ? 'active-tab' : ''} onClick={() => setActiveTab('issues')}>
|
||||
问题 ({issues.total})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'sources' && (
|
||||
<div className="asset-table-wrap">
|
||||
<table className="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>卫星</th>
|
||||
<th>日期/时间</th>
|
||||
<th>产品</th>
|
||||
<th>轨道</th>
|
||||
<th>状态</th>
|
||||
<th>动作</th>
|
||||
<th>文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.items.map(item => {
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td><strong>{item.satellite}</strong><small>{item.satellite_family}</small></td>
|
||||
<td>{item.imaging_date}<small>{fmtDateTime(item.acquisition_start_time_utc)}</small></td>
|
||||
<td>{item.source_format}<small>{item.imaging_mode} / {item.polarization}</small></td>
|
||||
<td>{item.relative_orbit || '-'}<small>abs {item.absolute_orbit || '-'}</small></td>
|
||||
<td><StatusBadge value={item.parse_status} /></td>
|
||||
<td>
|
||||
<span className="asset-action-placeholder">-</span>
|
||||
</td>
|
||||
<td title={item.file_path}>{item.file_name || item.logical_product_uid}<small>{fmtBytes(item.size_bytes)}</small></td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{renderPager(sources, (offset) => refresh({ sourceOffset: offset, orbitOffset: orbits.offset, issueOffset: issues.offset }))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'orbits' && (
|
||||
<div className="asset-table-wrap">
|
||||
<table className="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>卫星</th>
|
||||
<th>类型</th>
|
||||
<th>有效期</th>
|
||||
<th>质量</th>
|
||||
<th>状态</th>
|
||||
<th>文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orbits.items.map(item => (
|
||||
<tr key={item.id}>
|
||||
<td><strong>{item.satellite}</strong><small>{item.satellite_family}</small></td>
|
||||
<td>{item.orbit_type}<small>{item.native_format}</small></td>
|
||||
<td>{fmtDateTime(item.validity_start_time_utc)}<small>{fmtDateTime(item.validity_stop_time_utc)}</small></td>
|
||||
<td>{item.quality_class}</td>
|
||||
<td><StatusBadge value={item.parse_status} /></td>
|
||||
<td title={item.file_path}>{item.file_name}<small>{fmtBytes(item.size_bytes)}</small></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{renderPager(orbits, (offset) => refresh({ sourceOffset: sources.offset, orbitOffset: offset, issueOffset: issues.offset }))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'issues' && (
|
||||
<div className="asset-table-wrap">
|
||||
<table className="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>级别</th>
|
||||
<th>代码</th>
|
||||
<th>对象</th>
|
||||
<th>说明</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issues.items.map(item => (
|
||||
<tr key={item.id}>
|
||||
<td><StatusBadge value={item.severity} /></td>
|
||||
<td>{item.issue_code}</td>
|
||||
<td>{item.inventory_type}<small>{item.source_path || `radar ${item.radar_data_id || '-'}`}</small></td>
|
||||
<td>{item.issue_message || '-'}</td>
|
||||
<td>{fmtDateTime(item.last_seen_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{renderPager(issues, (offset) => refresh({ sourceOffset: sources.offset, orbitOffset: orbits.offset, issueOffset: offset }))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+267
-101
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import './App.css';
|
||||
import { useI18n } from './i18n/I18nContext';
|
||||
import { getAssetInventoryStatus, scanAssetInventory, unpackSentinel1Batch } from './api/assets';
|
||||
|
||||
const DEFAULT_MONITOR_CONFIG = {
|
||||
radar_dirs: [],
|
||||
@@ -8,6 +9,9 @@ const DEFAULT_MONITOR_CONFIG = {
|
||||
dinsar_dirs: [],
|
||||
gf3_source_dirs: [],
|
||||
gf3_storage_dirs: [],
|
||||
s1_source_dirs: [],
|
||||
s1_storage_dirs: [],
|
||||
s1_orbit_dirs: [],
|
||||
};
|
||||
|
||||
const DEFAULT_UNPACK_CONFIG = {
|
||||
@@ -42,6 +46,7 @@ const parseUnpackRunValue = (rawValue, label) => {
|
||||
};
|
||||
|
||||
const formatList = (list) => (Array.isArray(list) && list.length ? list.join('; ') : '未配置');
|
||||
const normalizeComparePath = (value) => String(value || '').trim().replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
||||
|
||||
const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled = true }) => {
|
||||
const { t } = useI18n();
|
||||
@@ -50,8 +55,6 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [activeTasks, setActiveTasks] = useState([]);
|
||||
const [unpackConfig, setUnpackConfig] = useState(DEFAULT_UNPACK_CONFIG);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [unpackLoading, setUnpackLoading] = useState(false);
|
||||
const [unpackMessage, setUnpackMessage] = useState('');
|
||||
const [showUnpackDialog, setShowUnpackDialog] = useState(false);
|
||||
@@ -59,7 +62,11 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const [unpackDialogError, setUnpackDialogError] = useState('');
|
||||
const [unpackTaskId, setUnpackTaskId] = useState('');
|
||||
const [unpackTaskTerminal, setUnpackTaskTerminal] = useState(false);
|
||||
const [s1Loading, setS1Loading] = useState(false);
|
||||
const [s1ScanLoading, setS1ScanLoading] = useState(false);
|
||||
const [s1Message, setS1Message] = useState('');
|
||||
const [gf3Loading, setGf3Loading] = useState(false);
|
||||
const [gf3ProcessLoading, setGf3ProcessLoading] = useState(false);
|
||||
const [gf3Message, setGf3Message] = useState('');
|
||||
const logEndRef = useRef(null);
|
||||
|
||||
@@ -76,6 +83,7 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const unpackActiveTask = displayActiveTasks.find((task) =>
|
||||
task.task_id === unpackTaskId || task.task_type === 'UNPACK_ARCHIVES'
|
||||
);
|
||||
const s1ActiveTask = displayActiveTasks.find((task) => task.task_type === 'UNPACK_SENTINEL1');
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
@@ -102,6 +110,9 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
...data,
|
||||
radar_dirs: toArray(data?.radar_dirs),
|
||||
dinsar_dirs: toArray(data?.dinsar_dirs),
|
||||
s1_source_dirs: toArray(data?.s1_source_dirs),
|
||||
s1_storage_dirs: toArray(data?.s1_storage_dirs),
|
||||
s1_orbit_dirs: toArray(data?.s1_orbit_dirs),
|
||||
gf3_source_dirs: toArray(data?.gf3_source_dirs),
|
||||
gf3_storage_dirs: toArray(data?.gf3_storage_dirs),
|
||||
});
|
||||
@@ -267,51 +278,134 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
|
||||
const hasRadarDirs = config.radar_dirs.length > 0;
|
||||
const hasOrbitDir = typeof config.orbit_dir === 'string' && config.orbit_dir.trim() !== '';
|
||||
const hasDinsarDirs = config.dinsar_dirs.length > 0;
|
||||
const hasS1SourceDirs = config.s1_source_dirs.length > 0;
|
||||
const hasS1StorageDirs = config.s1_storage_dirs.length > 0;
|
||||
const hasS1OrbitDirs = config.s1_orbit_dirs.length > 0;
|
||||
const hasGf3SourceDirs = config.gf3_source_dirs.length > 0;
|
||||
const hasGf3StorageDirs = config.gf3_storage_dirs.length > 0;
|
||||
|
||||
const canRunRadar = !readOnly && configLoaded && hasRadarDirs;
|
||||
const canRunOrbit = !readOnly && configLoaded && hasOrbitDir;
|
||||
const canRunDinsar = !readOnly && configLoaded && hasDinsarDirs;
|
||||
const canRunS1 = !readOnly && configLoaded && (hasS1SourceDirs || hasS1StorageDirs || hasS1OrbitDirs);
|
||||
const canRunS1Scan = !readOnly && configLoaded && hasS1SourceDirs;
|
||||
const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs;
|
||||
const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs;
|
||||
const canRunGf3Process = !readOnly && configLoaded && hasGf3SourceDirs;
|
||||
const canOpenUnpackDialog = !readOnly && unpackConfig.source_dirs.length > 0;
|
||||
|
||||
const handleRunNow = async (target) => {
|
||||
const handleS1Run = async () => {
|
||||
if (readOnly) {
|
||||
setMessage('当前账户为只读模式,无法触发扫描。');
|
||||
setS1Message('当前账户为只读模式,无法触发 Sentinel-1 任务。');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const targetMap = {
|
||||
radar: 'LT-1 数据',
|
||||
orbit: '精轨数据',
|
||||
dinsar: 'D-InSAR 结果',
|
||||
gf3: 'GF3 数据',
|
||||
};
|
||||
setMessage(`正在触发${targetMap[target] || '全部'}手动扫描...`);
|
||||
|
||||
setS1Loading(true);
|
||||
setS1Message('Sentinel-1 任务启动中...');
|
||||
try {
|
||||
const url = target ? `${apiEndpoint}/monitor/run-now?target=${target}` : `${apiEndpoint}/monitor/run-now`;
|
||||
const res = await fetch(url, {
|
||||
const res = await unpackSentinel1Batch({
|
||||
scan_before_unpack: true,
|
||||
overwrite: false,
|
||||
});
|
||||
setS1Message(res.message || 'Sentinel-1 任务已启动');
|
||||
if (onTaskStart) {
|
||||
onTaskStart(res.task_id, 'Sentinel-1 任务已启动。', {
|
||||
nonBlocking: true,
|
||||
taskType: 'UNPACK_SENTINEL1',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setS1Message(`失败:${err?.response?.data?.detail || err.message || '未知错误'}`);
|
||||
} finally {
|
||||
setS1Loading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleS1Scan = async () => {
|
||||
if (readOnly) {
|
||||
setS1Message('当前账户为只读模式,无法触发 Sentinel-1 扫描。');
|
||||
return;
|
||||
}
|
||||
setS1ScanLoading(true);
|
||||
setS1Message('Sentinel-1 源数据扫描启动中...');
|
||||
try {
|
||||
const inventoryStatus = await getAssetInventoryStatus();
|
||||
const sourcePathSet = new Set(config.s1_source_dirs.map(normalizeComparePath));
|
||||
const rootIds = (inventoryStatus?.states || [])
|
||||
.filter((item) => item?.inventory_type === 'source_product' && sourcePathSet.has(normalizeComparePath(item?.root_path)))
|
||||
.map((item) => item.root_ref_id)
|
||||
.filter((value, index, array) => value && array.indexOf(value) === index);
|
||||
const res = await scanAssetInventory({
|
||||
inventory_types: ['source_product'],
|
||||
root_ids: rootIds,
|
||||
bind_orbits: true,
|
||||
});
|
||||
setS1Message(res.message || 'Sentinel-1 源数据扫描任务已启动');
|
||||
onTaskStart?.(res.task_id, 'Sentinel-1 源数据扫描任务已启动。', {
|
||||
nonBlocking: true,
|
||||
taskType: 'SCAN_ASSET_INVENTORY',
|
||||
});
|
||||
} catch (err) {
|
||||
setS1Message(`失败:${err?.response?.data?.detail || err.message || '未知错误'}`);
|
||||
} finally {
|
||||
setS1ScanLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleS1OrbitScan = async () => {
|
||||
if (readOnly) {
|
||||
setS1Message('当前账户为只读模式,无法触发 Sentinel-1 精轨扫描。');
|
||||
return;
|
||||
}
|
||||
setS1ScanLoading(true);
|
||||
setS1Message('Sentinel-1 精轨扫描启动中...');
|
||||
try {
|
||||
const inventoryStatus = await getAssetInventoryStatus();
|
||||
const orbitPathSet = new Set(config.s1_orbit_dirs.map(normalizeComparePath));
|
||||
const rootIds = (inventoryStatus?.states || [])
|
||||
.filter((item) => item?.inventory_type === 'orbit_asset' && orbitPathSet.has(normalizeComparePath(item?.root_path)))
|
||||
.map((item) => item.root_ref_id)
|
||||
.filter((value, index, array) => value && array.indexOf(value) === index);
|
||||
const res = await scanAssetInventory({
|
||||
inventory_types: ['orbit_asset'],
|
||||
root_ids: rootIds,
|
||||
bind_orbits: true,
|
||||
});
|
||||
setS1Message(res.message || 'Sentinel-1 精轨扫描任务已启动');
|
||||
onTaskStart?.(res.task_id, 'Sentinel-1 精轨扫描任务已启动。', {
|
||||
nonBlocking: true,
|
||||
taskType: 'SCAN_ASSET_INVENTORY',
|
||||
});
|
||||
} catch (err) {
|
||||
setS1Message(`失败:${err?.response?.data?.detail || err.message || '未知错误'}`);
|
||||
} finally {
|
||||
setS1ScanLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGf3BatchProcess = async () => {
|
||||
if (readOnly) {
|
||||
setGf3Message('当前账户为只读模式,无法触发 GF3 预处理。');
|
||||
return;
|
||||
}
|
||||
setGf3ProcessLoading(true);
|
||||
setGf3Message('GF3 预处理启动中...');
|
||||
try {
|
||||
const res = await fetch(`${apiEndpoint}/monitor/gf3-process`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await parseJsonSafe(res, {});
|
||||
if (res.ok) {
|
||||
setMessage(data.message || '扫描任务已启动。');
|
||||
setGf3Message(data.message || 'GF3 批量处理任务已启动');
|
||||
if (onTaskStart) {
|
||||
onTaskStart(data.task_id, `已触发${targetMap[target] || '全部'}手动扫描...`);
|
||||
onTaskStart(data.task_id, 'GF3 L1A→L2 批量处理已启动。');
|
||||
}
|
||||
} else {
|
||||
setMessage(`触发失败: ${data.detail || '未知错误'}`);
|
||||
setGf3Message(`失败:${data.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setMessage(`触发失败: ${err.message}`);
|
||||
setGf3Message(`失败:${err.message || '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setGf3ProcessLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -397,29 +491,79 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
}
|
||||
};
|
||||
|
||||
const handleGf3BatchProcess = async () => {
|
||||
const handleRadarScan = async () => {
|
||||
if (readOnly) {
|
||||
setGf3Message('当前账户为只读模式,无法触发 GF3 处理。');
|
||||
setUnpackMessage('当前账户为只读模式,无法触发扫描。');
|
||||
return;
|
||||
}
|
||||
setGf3Loading(true);
|
||||
setGf3Message('GF3 批量处理启动中...');
|
||||
setUnpackMessage('LT-1 扫描启动中...');
|
||||
try {
|
||||
const res = await fetch(`${apiEndpoint}/monitor/gf3-process`, {
|
||||
const res = await fetch(`${apiEndpoint}/monitor/run-now?target=radar`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await parseJsonSafe(res, {});
|
||||
if (res.ok) {
|
||||
setGf3Message(data.message || 'GF3 批量处理任务已启动');
|
||||
setUnpackMessage(data.message || 'LT-1 扫描任务已启动');
|
||||
if (onTaskStart) {
|
||||
onTaskStart(data.task_id, 'GF3 L1A→L2 批量处理已启动。');
|
||||
onTaskStart(data.task_id, '已触发 LT-1 手动扫描...');
|
||||
}
|
||||
} else {
|
||||
setUnpackMessage(`失败:${data.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setUnpackMessage(`失败:${err.message || '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrbitScan = async () => {
|
||||
if (readOnly) {
|
||||
setUnpackMessage('当前账户为只读模式,无法触发扫描。');
|
||||
return;
|
||||
}
|
||||
setUnpackMessage('精轨扫描启动中...');
|
||||
try {
|
||||
const res = await fetch(`${apiEndpoint}/monitor/run-now?target=orbit`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await parseJsonSafe(res, {});
|
||||
if (res.ok) {
|
||||
setUnpackMessage(data.message || '精轨扫描任务已启动');
|
||||
if (onTaskStart) {
|
||||
onTaskStart(data.task_id, '已触发精轨手动扫描...');
|
||||
}
|
||||
} else {
|
||||
setUnpackMessage(`失败:${data.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setUnpackMessage(`失败:${err.message || '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGf3Scan = async () => {
|
||||
if (readOnly) {
|
||||
setGf3Message('当前账户为只读模式,无法触发扫描。');
|
||||
return;
|
||||
}
|
||||
setGf3Loading(true);
|
||||
setGf3Message('GF3 扫描启动中...');
|
||||
try {
|
||||
const res = await fetch(`${apiEndpoint}/monitor/run-now?target=gf3`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await parseJsonSafe(res, {});
|
||||
if (res.ok) {
|
||||
setGf3Message(data.message || 'GF3 扫描任务已启动');
|
||||
if (onTaskStart) {
|
||||
onTaskStart(data.task_id, '已触发 GF3 手动扫描...');
|
||||
}
|
||||
} else {
|
||||
setGf3Message(`失败:${data.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setGf3Message(`失败:${err.message}`);
|
||||
setGf3Message(`失败:${err.message || '未知错误'}`);
|
||||
} finally {
|
||||
setGf3Loading(false);
|
||||
}
|
||||
@@ -437,17 +581,6 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const rowStyle = { display: 'flex', gap: '8px' };
|
||||
const gridStyle = { display: 'grid', rowGap: '6px', fontSize: '0.9em', color: 'var(--color-text-secondary)' };
|
||||
|
||||
const scanBtnStyle = (canRun) => ({
|
||||
flex: 1,
|
||||
padding: '8px 5px',
|
||||
backgroundColor: 'var(--color-info)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading || !canRun ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.85em',
|
||||
});
|
||||
|
||||
const actionBtnStyle = (isLoading, isDisabled) => ({
|
||||
padding: '6px 10px',
|
||||
backgroundColor: 'var(--color-accent)',
|
||||
@@ -495,6 +628,9 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
<div style={gridStyle}>
|
||||
<div style={rowStyle}><span style={labelStyle}>LT-1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.radar_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>LT-1 精轨</span><span style={{ wordBreak: 'break-all' }}>{config.orbit_dir || '未配置'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 源数据</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 精轨</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_orbit_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>D-InSAR 结果</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.dinsar_dirs)}</span></div>
|
||||
@@ -502,14 +638,14 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
</div>
|
||||
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>LT-1 归档解包</div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>LT-1 归档解包 / 扫描</div>
|
||||
<div style={{ ...gridStyle, marginBottom: '8px' }}>
|
||||
<div style={rowStyle}><span style={labelStyle}>来源目录</span><span style={{ wordBreak: 'break-all' }}>{formatList(unpackConfig.source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>LT-1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(unpackConfig.insar_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>单次上限</span><span>{unpackConfig.max_files_per_run > 0 ? `${unpackConfig.max_files_per_run} 个压缩包` : '不限'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>最长运行</span><span>{unpackConfig.max_runtime_minutes > 0 ? `${unpackConfig.max_runtime_minutes} 分钟` : '不限'}</span></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={handleOpenUnpackDialog}
|
||||
disabled={unpackLoading || !canOpenUnpackDialog}
|
||||
@@ -517,6 +653,20 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
>
|
||||
{unpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'LT-1 解包')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRadarScan}
|
||||
disabled={readOnly || !canRunRadar}
|
||||
style={actionBtnStyle(false, !canRunRadar)}
|
||||
>
|
||||
{readOnly ? '只读模式' : '扫描 LT-1'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOrbitScan}
|
||||
disabled={readOnly || !canRunOrbit}
|
||||
style={actionBtnStyle(false, !canRunOrbit)}
|
||||
>
|
||||
{readOnly ? '只读模式' : '扫描精轨'}
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.85em',
|
||||
@@ -530,7 +680,48 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
</div>
|
||||
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 L1A → L2 处理</div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>Sentinel-1 解包 / 扫描</div>
|
||||
<div style={{ ...gridStyle, marginBottom: '8px' }}>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 源数据</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 精轨</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_orbit_dirs)}</span></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={handleS1Run}
|
||||
disabled={s1Loading || s1ScanLoading || !canRunS1}
|
||||
style={actionBtnStyle(s1Loading, !canRunS1)}
|
||||
>
|
||||
{s1Loading ? '运行中...' : (readOnly ? '只读模式' : 'Sentinel-1 解包')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleS1Scan}
|
||||
disabled={s1ScanLoading || s1Loading || !canRunS1Scan}
|
||||
style={actionBtnStyle(s1ScanLoading, !canRunS1Scan)}
|
||||
>
|
||||
{s1ScanLoading ? '运行中...' : (readOnly ? '只读模式' : '扫描 S1 源数据')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleS1OrbitScan}
|
||||
disabled={s1ScanLoading || s1Loading || !canRunS1OrbitScan}
|
||||
style={actionBtnStyle(s1ScanLoading, !canRunS1OrbitScan)}
|
||||
>
|
||||
{s1ScanLoading ? '运行中...' : (readOnly ? '只读模式' : '扫描 S1 精轨')}
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.85em',
|
||||
color: s1Message.includes('失败') ? 'var(--color-danger)' : 'var(--color-text-muted)',
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
>
|
||||
{s1ActiveTask ? (s1ActiveTask.message || 'Sentinel-1 任务运行中...') : s1Message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 归档预处理</div>
|
||||
<div style={{ ...gridStyle, marginBottom: '8px' }}>
|
||||
<div style={rowStyle}><span style={labelStyle}>L1A 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>L2 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
|
||||
@@ -538,10 +729,17 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={handleGf3BatchProcess}
|
||||
disabled={gf3Loading || readOnly || !canRunGf3Process}
|
||||
style={actionBtnStyle(gf3Loading, readOnly || !canRunGf3Process)}
|
||||
disabled={gf3ProcessLoading || readOnly || !canRunGf3Process}
|
||||
style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)}
|
||||
>
|
||||
{gf3Loading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 L1A→L2')}
|
||||
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 预处理')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGf3Scan}
|
||||
disabled={gf3Loading || readOnly || !canRunGf3Scan}
|
||||
style={actionBtnStyle(gf3Loading, readOnly || !canRunGf3Scan)}
|
||||
>
|
||||
{gf3Loading ? '运行中...' : (readOnly ? '只读模式' : '扫描 GF3')}
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
@@ -554,63 +752,31 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>活动任务</div>
|
||||
{displayActiveTasks.length === 0 ? (
|
||||
<div style={{ fontSize: '0.85em', color: 'var(--color-text-muted)' }}>当前无活动任务。</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', rowGap: '8px' }}>
|
||||
{displayActiveTasks.slice(0, 4).map((task) => (
|
||||
<div key={task.task_id} style={{ fontSize: '0.85em', color: 'var(--color-text-secondary)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
|
||||
<span>{task.task_type}</span>
|
||||
<span>{task.progress}%</span>
|
||||
</div>
|
||||
<div style={{ height: '6px', background: 'var(--color-panel-muted)', borderRadius: '3px', overflow: 'hidden' }}>
|
||||
<div style={{ width: `${task.progress}%`, height: '100%', background: 'var(--color-info)' }} />
|
||||
</div>
|
||||
<div style={{ color: 'var(--color-text-muted)', marginTop: '4px', wordBreak: 'break-all' }}>{t(task.message || '')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<h4 style={{ margin: '0 0 5px 0', fontSize: '1em' }}>实时日志</h4>
|
||||
<div
|
||||
style={{
|
||||
height: '160px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#22c55e',
|
||||
padding: '10px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85em',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{displayLogs.length === 0 ? (
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>暂无日志...</div>
|
||||
) : (
|
||||
displayLogs.map((log, index) => (
|
||||
<div key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{t(log)}</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flexShrink: 0, borderTop: '1px solid var(--color-border)', paddingTop: '10px', marginTop: '6px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '6px' }}>
|
||||
<button onClick={() => handleRunNow('radar')} disabled={loading || !canRunRadar} style={scanBtnStyle(canRunRadar)}>扫描 LT-1</button>
|
||||
<button onClick={() => handleRunNow('gf3')} disabled={loading || !canRunGf3Scan} style={scanBtnStyle(canRunGf3Scan)}>扫描 GF3</button>
|
||||
<button onClick={() => handleRunNow('orbit')} disabled={loading || !canRunOrbit} style={scanBtnStyle(canRunOrbit)}>扫描精轨</button>
|
||||
<button onClick={() => handleRunNow('dinsar')} disabled={loading || !canRunDinsar} style={scanBtnStyle(canRunDinsar)}>扫描 D-InSAR</button>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '6px', color: 'var(--color-text-primary)' }}>实时日志</div>
|
||||
<div
|
||||
style={{
|
||||
height: '160px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#22c55e',
|
||||
padding: '10px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85em',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{displayLogs.length === 0 ? (
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>暂无日志...</div>
|
||||
) : (
|
||||
displayLogs.map((log, index) => (
|
||||
<div key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{t(log)}</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
{message && <div style={{ color: message.includes('失败') ? 'red' : 'green', fontSize: '0.9em' }}>{message}</div>}
|
||||
</div>
|
||||
|
||||
{showUnpackDialog && (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { deleteRunLog, deleteRunRecord, getRunLog, listEngines, listRuns, previewPyintInputAssets, submitRun } from './api/dinsarProduction';
|
||||
import { clearTaskLogs, deleteTaskLog, deleteTaskRecord, getActiveTasks, getRecentTasks, getTaskLogs } from './api/tasks';
|
||||
import { formatSatelliteFamilyLabel, inferSatelliteFamilyFromResultLike } from './utils/satelliteFamily';
|
||||
|
||||
const card = {
|
||||
background: '#fff',
|
||||
@@ -160,6 +161,9 @@ function taskToRunRow(task) {
|
||||
completed_items: null,
|
||||
failed_items: null,
|
||||
skipped_items: null,
|
||||
master_satellite: task?.master_satellite || '',
|
||||
slave_satellite: task?.slave_satellite || '',
|
||||
pair_key: task?.pair_key || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1375,12 +1379,12 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#0f172a', fontWeight: 600 }}>PyINT 输入资产预检</div>
|
||||
<div style={{ fontSize: 11, color: '#64748b', marginTop: 4 }}>
|
||||
提交前检查 Task_* 结构、DEM 策略和 LT-1 轨道是否齐备。即使不手动预检,后端提交时也会做同样校验。
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#0f172a', fontWeight: 600 }}>PyINT 输入资产预检</div>
|
||||
<div style={{ fontSize: 11, color: '#64748b', marginTop: 4 }}>
|
||||
提交前检查 Task_* 结构、DEM 策略以及生产所需源数据和轨道文件是否齐备。即使不手动预检,后端提交时也会做同样校验。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePreviewPyint}
|
||||
disabled={readOnly || pyintPreviewLoading || !rootDir.trim()}
|
||||
@@ -1436,7 +1440,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
{ label: 'DEM 策略', value: formatPyintDemMode(pyintPreview?.dem?.mode), color: '#1d4ed8' },
|
||||
{ label: '轨道策略', value: formatPyintOrbitPolicy(pyintPreview?.orbits?.policy), color: '#7c3aed' },
|
||||
{
|
||||
label: '精轨桥接',
|
||||
label: '轨道处理',
|
||||
value: pyintPreview?.precise_orbit_bridge?.enabled
|
||||
? formatPyintPreciseOrbitMode(pyintPreview?.precise_orbit_bridge?.mode)
|
||||
: '关闭',
|
||||
@@ -1732,7 +1736,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{['运行ID', '引擎', '状态', '时间', '路径', '操作'].map(header => (
|
||||
{['运行ID', '引擎', '数据', '状态', '时间', '路径', '操作'].map(header => (
|
||||
<th
|
||||
key={header}
|
||||
style={{
|
||||
@@ -1759,6 +1763,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
<tr key={`${run.record_type || 'run'}-${run.run_id}`} style={{ borderBottom: '1px solid #f1f5f9' }}>
|
||||
<td style={{ padding: '6px 8px', fontFamily: 'monospace', fontSize: 11 }}>{run.run_id}</td>
|
||||
<td style={{ padding: '6px 8px' }}>{formatEngineLabel(run.engine)}</td>
|
||||
<td style={{ padding: '6px 8px' }}>{formatSatelliteFamilyLabel(inferSatelliteFamilyFromResultLike(run))}</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export const getAssetInventoryStatus = () =>
|
||||
apiClient.get('/assets/inventory/status').then(r => r.data);
|
||||
|
||||
export const scanAssetInventory = (payload = {}) =>
|
||||
apiClient.post('/assets/inventory/scan', payload).then(r => r.data);
|
||||
|
||||
export const listSourceAssets = (params = {}) =>
|
||||
apiClient.get('/assets/sources', { params }).then(r => r.data);
|
||||
|
||||
export const listOrbitAssets = (params = {}) =>
|
||||
apiClient.get('/assets/orbits', { params }).then(r => r.data);
|
||||
|
||||
export const listAssetIssues = (params = {}) =>
|
||||
apiClient.get('/assets/issues', { params }).then(r => r.data);
|
||||
|
||||
export const unpackSentinel1Source = (assetId, payload = {}) =>
|
||||
apiClient.post(`/assets/sources/${assetId}/unpack-sentinel1`, payload).then(r => r.data);
|
||||
|
||||
export const unpackSentinel1Batch = (payload = {}) =>
|
||||
apiClient.post('/assets/inventory/unpack-sentinel1', payload).then(r => r.data);
|
||||
@@ -3,6 +3,9 @@ import axios from 'axios';
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
withCredentials: true,
|
||||
paramsSerializer: {
|
||||
indexes: null,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
|
||||
@@ -20,6 +20,10 @@ const getTaskTypeLabel = (taskType) => {
|
||||
return '灾害点同步';
|
||||
case 'UNPACK_ARCHIVES':
|
||||
return 'LT-1 解包';
|
||||
case 'UNPACK_SENTINEL1':
|
||||
return 'Sentinel-1 解包';
|
||||
case 'SCAN_ASSET_INVENTORY':
|
||||
return '资产库存扫描';
|
||||
case 'IDL_IMPORT':
|
||||
return 'ENVI 数据导入';
|
||||
case 'IDL_DINSAR':
|
||||
|
||||
@@ -1,33 +1,92 @@
|
||||
const createRows = (dataInfo, language, formatYmd) => [
|
||||
{ label: language === 'en' ? 'Satellite:' : '卫星:', value: dataInfo.satellite || '-' },
|
||||
{ label: language === 'en' ? 'Satellite Mode:' : '卫星模式:', value: dataInfo.satellite_mode || '-' },
|
||||
{ label: language === 'en' ? 'Receiving Station:' : '接收站:', value: dataInfo.receiving_station || '-' },
|
||||
{ label: language === 'en' ? 'Imaging Date:' : '成像日期:', value: formatYmd(dataInfo.imaging_date) },
|
||||
{ label: language === 'en' ? 'Imaging Mode:' : '成像模式:', value: dataInfo.imaging_mode || '-' },
|
||||
{ label: language === 'en' ? 'Orbit Circle:' : '轨道圈号:', value: dataInfo.orbit_circle || '-' },
|
||||
{ label: language === 'en' ? 'Scene Center Lon:' : '场景中心经度:', value: dataInfo.scene_center_lon ?? '-' },
|
||||
{ label: language === 'en' ? 'Scene Center Lat:' : '场景中心纬度:', value: dataInfo.scene_center_lat ?? '-' },
|
||||
{ label: language === 'en' ? 'Acquisition Time:' : '采集时间:', value: dataInfo.acquisition_time_utc || '-' },
|
||||
{ label: language === 'en' ? 'Product Type:' : '产品类型:', value: dataInfo.product_type || '-' },
|
||||
{ label: language === 'en' ? 'Polarization:' : '极化方式:', value: dataInfo.polarization || '-' },
|
||||
{ label: language === 'en' ? 'Product Level:' : '产品级别:', value: dataInfo.product_level || '-' },
|
||||
{ label: language === 'en' ? 'Product Unique ID:' : '产品唯一ID:', value: dataInfo.product_unique_id || '-' },
|
||||
{ label: language === 'en' ? 'Orbit Direction:' : '轨道方向:', value: dataInfo.orbit_direction || '-' },
|
||||
{
|
||||
label: language === 'en' ? 'Has Orbit:' : '有精轨:',
|
||||
value: dataInfo.has_orbit_data ? (language === 'en' ? 'Yes' : '是') : (language === 'en' ? 'No' : '否'),
|
||||
},
|
||||
{
|
||||
label: language === 'en' ? 'Orbit File:' : '轨道文件:',
|
||||
value: dataInfo.orbit_file_path || '-',
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const joinChannels = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||
return items.length ? items.join(' / ') : '-';
|
||||
}
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const yesNo = (flag, language) => {
|
||||
if (flag) return language === 'en' ? 'Yes' : '是';
|
||||
return language === 'en' ? 'No' : '否';
|
||||
};
|
||||
|
||||
const field = (label, value, extra = {}) => ({
|
||||
label,
|
||||
value: value === null || value === undefined || value === '' ? '-' : value,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const createSentinelRows = (dataInfo, language, formatYmd) => {
|
||||
const metadata = dataInfo.metadata_json || {};
|
||||
const polarizationChannels = metadata.polarization_channels || metadata.manifest_polarizations;
|
||||
|
||||
return [
|
||||
field(language === 'en' ? 'Satellite:' : '卫星:', dataInfo.satellite),
|
||||
field(language === 'en' ? 'Satellite Family:' : '卫星系列:', dataInfo.satellite_family),
|
||||
field(language === 'en' ? 'Source Format:' : '源格式:', dataInfo.source_format),
|
||||
field(language === 'en' ? 'Imaging Date:' : '成像日期:', formatYmd(dataInfo.imaging_date)),
|
||||
field(language === 'en' ? 'Acquisition Start:' : '采集开始:', formatDateTime(dataInfo.acquisition_start_time_utc)),
|
||||
field(language === 'en' ? 'Acquisition Stop:' : '采集结束:', formatDateTime(dataInfo.acquisition_stop_time_utc)),
|
||||
field(language === 'en' ? 'Imaging Mode:' : '成像模式:', dataInfo.imaging_mode),
|
||||
field(language === 'en' ? 'Product Type:' : '产品类型:', dataInfo.product_type),
|
||||
field(language === 'en' ? 'Product Level:' : '产品级别:', dataInfo.product_level),
|
||||
field(language === 'en' ? 'Orbit Direction:' : '轨道方向:', dataInfo.orbit_direction),
|
||||
field(language === 'en' ? 'Relative Orbit:' : '相对轨道:', dataInfo.relative_orbit),
|
||||
field(language === 'en' ? 'Absolute Orbit:' : '绝对轨道:', dataInfo.absolute_orbit),
|
||||
field(language === 'en' ? 'Polarization:' : '极化方式:', dataInfo.polarization),
|
||||
field(language === 'en' ? 'Polarization Channels:' : '极化通道:', joinChannels(polarizationChannels)),
|
||||
field(language === 'en' ? 'Datatake:' : '数据采集号:', metadata.filename_datatake),
|
||||
field(language === 'en' ? 'Scene Center Lon:' : '场景中心经度:', dataInfo.scene_center_lon),
|
||||
field(language === 'en' ? 'Scene Center Lat:' : '场景中心纬度:', dataInfo.scene_center_lat),
|
||||
field(language === 'en' ? 'Product Unique ID:' : '产品唯一ID:', dataInfo.product_unique_id, {
|
||||
valueStyle: { wordBreak: 'break-all' },
|
||||
}),
|
||||
field(language === 'en' ? 'Has Orbit:' : '有精轨:', yesNo(dataInfo.has_orbit_data, language)),
|
||||
field(language === 'en' ? 'Orbit File:' : '轨道文件:', dataInfo.orbit_file_path, {
|
||||
valueStyle: { wordBreak: 'break-all' },
|
||||
}),
|
||||
field(language === 'en' ? 'ENVI Processed:' : 'ENVI已处理:', yesNo(dataInfo.is_envi_processed, language)),
|
||||
];
|
||||
};
|
||||
|
||||
const createDefaultRows = (dataInfo, language, formatYmd) => [
|
||||
field(language === 'en' ? 'Satellite:' : '卫星:', dataInfo.satellite),
|
||||
field(language === 'en' ? 'Satellite Mode:' : '卫星模式:', dataInfo.satellite_mode),
|
||||
field(language === 'en' ? 'Receiving Station:' : '接收站:', dataInfo.receiving_station),
|
||||
field(language === 'en' ? 'Imaging Date:' : '成像日期:', formatYmd(dataInfo.imaging_date)),
|
||||
field(language === 'en' ? 'Imaging Mode:' : '成像模式:', dataInfo.imaging_mode),
|
||||
field(language === 'en' ? 'Orbit Circle:' : '轨道圈号:', dataInfo.orbit_circle),
|
||||
field(language === 'en' ? 'Scene Center Lon:' : '场景中心经度:', dataInfo.scene_center_lon),
|
||||
field(language === 'en' ? 'Scene Center Lat:' : '场景中心纬度:', dataInfo.scene_center_lat),
|
||||
field(language === 'en' ? 'Acquisition Time:' : '采集时间:', dataInfo.acquisition_time_utc),
|
||||
field(language === 'en' ? 'Product Type:' : '产品类型:', dataInfo.product_type),
|
||||
field(language === 'en' ? 'Polarization:' : '极化方式:', dataInfo.polarization),
|
||||
field(language === 'en' ? 'Product Level:' : '产品级别:', dataInfo.product_level),
|
||||
field(language === 'en' ? 'Product Unique ID:' : '产品唯一ID:', dataInfo.product_unique_id),
|
||||
field(language === 'en' ? 'Orbit Direction:' : '轨道方向:', dataInfo.orbit_direction),
|
||||
field(language === 'en' ? 'Has Orbit:' : '有精轨:', yesNo(dataInfo.has_orbit_data, language)),
|
||||
field(language === 'en' ? 'Orbit File:' : '轨道文件:', dataInfo.orbit_file_path, {
|
||||
valueStyle: { wordBreak: 'break-all' },
|
||||
},
|
||||
{
|
||||
label: language === 'en' ? 'ENVI Processed:' : 'ENVI已处理:',
|
||||
value: dataInfo.is_envi_processed ? (language === 'en' ? 'Yes' : '是') : (language === 'en' ? 'No' : '否'),
|
||||
},
|
||||
}),
|
||||
field(language === 'en' ? 'ENVI Processed:' : 'ENVI已处理:', yesNo(dataInfo.is_envi_processed, language)),
|
||||
];
|
||||
|
||||
const createRows = (dataInfo, language, formatYmd) => {
|
||||
if ((dataInfo.satellite_family || '').toUpperCase() === 'S1') {
|
||||
return createSentinelRows(dataInfo, language, formatYmd);
|
||||
}
|
||||
return createDefaultRows(dataInfo, language, formatYmd);
|
||||
};
|
||||
|
||||
export default function DataInfoModal({
|
||||
visible,
|
||||
dataInfo,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
buildDinsarEngineOptions,
|
||||
getDinsarEngineMeta,
|
||||
} from '../utils/dinsarEngines';
|
||||
import { formatSatelliteFamilyLabel, inferSatelliteFamilyFromResultLike } from '../utils/satelliteFamily';
|
||||
|
||||
const STATUS_TONE_MAP = {
|
||||
READY: 'ready',
|
||||
@@ -383,6 +384,7 @@ export default function DinsarCatalogPanel({
|
||||
{products.map((item) => {
|
||||
const tone = STATUS_TONE_MAP[item.status] || 'neutral';
|
||||
const engineMeta = getDinsarEngineMeta(item.engine_code);
|
||||
const satelliteFamily = inferSatelliteFamilyFromResultLike(item);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
@@ -396,6 +398,9 @@ export default function DinsarCatalogPanel({
|
||||
</div>
|
||||
<div className="dinsar-catalog-list-item-badges">
|
||||
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
|
||||
{satelliteFamily && (
|
||||
<span className="dinsar-engine-badge tone-unknown">{formatSatelliteFamilyLabel(satelliteFamily)}</span>
|
||||
)}
|
||||
<span>{formatDateTime(item.published_at)}</span>
|
||||
</div>
|
||||
<div className="dinsar-catalog-list-item-meta">
|
||||
@@ -434,6 +439,9 @@ export default function DinsarCatalogPanel({
|
||||
<div className="dinsar-catalog-empty error">{selectedProduct.error}</div>
|
||||
) : (
|
||||
<div className="dinsar-catalog-detail-body">
|
||||
{(() => {
|
||||
const satelliteFamily = inferSatelliteFamilyFromResultLike(selectedProduct?.profile || selectedProduct);
|
||||
return (
|
||||
<div className="dinsar-catalog-hero">
|
||||
<div className="dinsar-catalog-preview-frame">
|
||||
<img
|
||||
@@ -452,6 +460,11 @@ export default function DinsarCatalogPanel({
|
||||
<span className={`dinsar-engine-badge tone-${selectedProductEngine.tone}`}>
|
||||
{selectedProductEngine.shortLabel}
|
||||
</span>
|
||||
{satelliteFamily && (
|
||||
<span className="dinsar-engine-badge tone-unknown">
|
||||
{formatSatelliteFamilyLabel(satelliteFamily)}
|
||||
</span>
|
||||
)}
|
||||
<StatusPill label={selectedProduct.status || 'UNKNOWN'} tone={selectedStatusTone} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,6 +482,8 @@ export default function DinsarCatalogPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="dinsar-catalog-detail-grid">
|
||||
<div className="dinsar-catalog-section-card">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getLeftTabLabel } from '../../utils/appUiHelpers';
|
||||
import { PanelLoadingBody, PanelLoadingPanel } from './AppLoadingFallbacks';
|
||||
|
||||
const LazyDataMonitorPanel = lazy(() => import('../../DataMonitorPanel'));
|
||||
const LazyAssetInventoryPanel = lazy(() => import('../../AssetInventoryPanel'));
|
||||
const LazyDataCopierPanel = lazy(() => import('../../DataCopierPanel'));
|
||||
const LazyIDLAutomationPanel = lazy(() => import('../../IDLAutomationPanel'));
|
||||
const LazyHazardPointPanel = lazy(() => import('../../HazardPointPanel'));
|
||||
@@ -237,6 +238,17 @@ export default function AppSidePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{leftPanelTab === 'asset_inventory' && (
|
||||
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
|
||||
<Suspense fallback={<PanelLoadingBody message="正在加载资产库存..." />}>
|
||||
<LazyAssetInventoryPanel
|
||||
readOnly={isReadOnlyUser}
|
||||
onTaskStart={taskPanel.onTaskStart}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{leftPanelTab === 'pairing' && (
|
||||
<Suspense fallback={<PanelLoadingPanel message="正在加载组网规划面板..." />}>
|
||||
<LazyPairingPanel
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { parseDatesFromName, formatYmd } from '../../utils/appUiHelpers';
|
||||
import { getDinsarEngineMeta } from '../../utils/dinsarEngines';
|
||||
import { formatSatelliteFamilyLabel, inferSatelliteFamilyFromResultLike } from '../../utils/satelliteFamily';
|
||||
|
||||
function truncateMiddle(value, maxLength = 28) {
|
||||
const text = String(value || '').trim();
|
||||
@@ -23,6 +24,7 @@ function DinsarResultRow({
|
||||
}) {
|
||||
const dates = showDates ? parseDatesFromName(result.name, (value) => formatYmd(value, language)) : null;
|
||||
const engineMeta = getDinsarEngineMeta(result.engine_code);
|
||||
const satelliteFamily = inferSatelliteFamilyFromResultLike(result);
|
||||
const hasTrace = !!(
|
||||
result.selection_strategy ||
|
||||
result.network_run_id ||
|
||||
@@ -45,6 +47,14 @@ function DinsarResultRow({
|
||||
>
|
||||
{engineMeta.shortLabel}
|
||||
</span>
|
||||
{satelliteFamily && (
|
||||
<span
|
||||
className="dinsar-engine-badge tone-unknown"
|
||||
title={language === 'en' ? 'Satellite family' : '卫星系列'}
|
||||
>
|
||||
{formatSatelliteFamilyLabel(satelliteFamily)}
|
||||
</span>
|
||||
)}
|
||||
{result.ai_score !== null && (
|
||||
<span
|
||||
className={`ai-score ${result.ai_score > 0.7 ? 'good' : (result.ai_score < 0.4 ? 'bad' : 'medium')}`}
|
||||
|
||||
@@ -145,7 +145,7 @@ export const LEFT_GROUP_SECTIONS = {
|
||||
};
|
||||
|
||||
export const LEFT_GROUP_TABS = {
|
||||
data: ['ingest', 'data', 'hazard'],
|
||||
data: ['ingest', 'asset_inventory', 'data', 'hazard'],
|
||||
production_planning: LEFT_GROUP_SECTIONS.production_planning.flatMap(section => section.tabs),
|
||||
production_management: [PRODUCTION_WORKSPACE_TAB],
|
||||
insar_analysis: LEFT_GROUP_SECTIONS.insar_analysis.flatMap(section => section.tabs),
|
||||
@@ -180,6 +180,7 @@ export const FULL_WIDTH_LEFT_TABS = new Set([
|
||||
|
||||
export const ADMIN_ONLY_TABS = new Set([
|
||||
'ingest',
|
||||
'asset_inventory',
|
||||
'pairing',
|
||||
'pairs',
|
||||
'ps_results',
|
||||
@@ -199,6 +200,7 @@ export const BATCH_API_MAX_PAGES = 200;
|
||||
|
||||
export const SATELLITE_GROUPS = [
|
||||
{ key: 'LT-1', label: 'LT-1', prefixes: ['LT1'] },
|
||||
{ key: 'S1', label: 'Sentinel-1', prefixes: ['S1'] },
|
||||
{ key: 'GF-3', label: 'GF-3', prefixes: ['GF3'] },
|
||||
];
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { normalizePagePayload } from '../utils/appHelpers';
|
||||
import { normalizeTaskStatus } from '../utils/appUiHelpers';
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '../config/appConstants';
|
||||
|
||||
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'COPY_DATA']);
|
||||
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'UNPACK_SENTINEL1', 'SCAN_ASSET_INVENTORY', 'COPY_DATA']);
|
||||
|
||||
export default function useDinsarOperations({
|
||||
onCleanupDinsarLayers,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import apiClient from '../api/client';
|
||||
import { normalizeTaskStatus } from '../utils/appUiHelpers';
|
||||
|
||||
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'COPY_DATA']);
|
||||
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'UNPACK_SENTINEL1', 'SCAN_ASSET_INVENTORY', 'COPY_DATA']);
|
||||
|
||||
const isTaskNonBlocking = (taskId, taskType, nonBlockingTaskIds = []) => (
|
||||
NON_BLOCKING_TASK_TYPES.has(String(taskType || '').toUpperCase())
|
||||
|
||||
@@ -59,6 +59,19 @@ export default function useRadarSearch({
|
||||
clearRadarSearchResults,
|
||||
clearRadarMapLayers,
|
||||
}) {
|
||||
const getSatelliteCatalog = () => {
|
||||
const satellites = useRadarStore.getState().radarSearchOptions?.satellite;
|
||||
return Array.isArray(satellites) ? satellites.filter(Boolean) : [];
|
||||
};
|
||||
|
||||
const getSatellitesForGroup = (groupKey, satellites = getSatelliteCatalog()) => {
|
||||
const group = SATELLITE_GROUPS.find((item) => item.key === groupKey);
|
||||
if (!group) return [];
|
||||
return satellites.filter((sat) =>
|
||||
group.prefixes.some((prefix) => String(sat || '').startsWith(prefix))
|
||||
);
|
||||
};
|
||||
|
||||
const fetchRadarImagingDates = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/radar-data/imaging-dates');
|
||||
@@ -74,13 +87,31 @@ export default function useRadarSearch({
|
||||
try {
|
||||
setRadarSearchOptionsLoading(true);
|
||||
const params = {};
|
||||
if (Array.isArray(satelliteFilter) && satelliteFilter.length > 0) {
|
||||
params.satellite = satelliteFilter;
|
||||
const storeState = useRadarStore.getState();
|
||||
const satelliteCatalog = getSatelliteCatalog();
|
||||
const hasExplicitSatelliteFilter = Array.isArray(satelliteFilter);
|
||||
let resolvedSatelliteFilter = hasExplicitSatelliteFilter
|
||||
? satelliteFilter.filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!hasExplicitSatelliteFilter) {
|
||||
const groupKey = storeState.selectedSatelliteGroup;
|
||||
if (groupKey && groupKey !== 'all') {
|
||||
resolvedSatelliteFilter = getSatellitesForGroup(groupKey, satelliteCatalog);
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedSatelliteFilter.length > 0) {
|
||||
params.satellite = resolvedSatelliteFilter;
|
||||
}
|
||||
const response = await apiClient.get('/radar-data/search/options', { params });
|
||||
const payload = response?.data && typeof response.data === 'object' ? response.data : {};
|
||||
const payloadSatellites = Array.isArray(payload.satellite) ? payload.satellite.filter(Boolean) : [];
|
||||
const nextSatelliteCatalog = satelliteCatalog.length > payloadSatellites.length
|
||||
? satelliteCatalog
|
||||
: payloadSatellites;
|
||||
setRadarSearchOptions({
|
||||
satellite: Array.isArray(payload.satellite) ? payload.satellite : [],
|
||||
satellite: nextSatelliteCatalog,
|
||||
satellite_mode: Array.isArray(payload.satellite_mode) ? payload.satellite_mode : [],
|
||||
receiving_station: Array.isArray(payload.receiving_station) ? payload.receiving_station : [],
|
||||
imaging_mode: Array.isArray(payload.imaging_mode) ? payload.imaging_mode : [],
|
||||
@@ -119,14 +150,9 @@ export default function useRadarSearch({
|
||||
orbit_direction: '',
|
||||
}));
|
||||
if (groupKey === 'all') {
|
||||
fetchRadarSearchOptions();
|
||||
fetchRadarSearchOptions([]);
|
||||
} else {
|
||||
const group = SATELLITE_GROUPS.find((g) => g.key === groupKey);
|
||||
if (!group) return;
|
||||
const allSatellites = useRadarStore.getState().radarSearchOptions.satellite;
|
||||
const matched = allSatellites.filter((sat) =>
|
||||
group.prefixes.some((prefix) => sat.startsWith(prefix))
|
||||
);
|
||||
const matched = getSatellitesForGroup(groupKey);
|
||||
if (matched.length > 0) {
|
||||
fetchRadarSearchOptions(matched);
|
||||
}
|
||||
@@ -265,15 +291,9 @@ export default function useRadarSearch({
|
||||
const applyRadarSearch = useCallback(async () => {
|
||||
const draftWithSatelliteGroup = { ...radarSearchDraft };
|
||||
if (selectedSatelliteGroup && selectedSatelliteGroup !== 'all') {
|
||||
const group = SATELLITE_GROUPS.find((g) => g.key === selectedSatelliteGroup);
|
||||
if (group) {
|
||||
const allSatellites = useRadarStore.getState().radarSearchOptions.satellite;
|
||||
const matched = allSatellites.filter((sat) =>
|
||||
group.prefixes.some((prefix) => sat.startsWith(prefix))
|
||||
);
|
||||
if (matched.length > 0) {
|
||||
draftWithSatelliteGroup.satellite = matched.join(',');
|
||||
}
|
||||
const matched = getSatellitesForGroup(selectedSatelliteGroup);
|
||||
if (matched.length > 0) {
|
||||
draftWithSatelliteGroup.satellite = matched.join(',');
|
||||
}
|
||||
}
|
||||
const normalizedCriteria = normalizeRadarSearchCriteria(draftWithSatelliteGroup, RADAR_SEARCH_DEFAULTS);
|
||||
@@ -349,7 +369,7 @@ export default function useRadarSearch({
|
||||
radarSearchRequestSeqRef.current += 1;
|
||||
setHasRadarSearched(false);
|
||||
clearRadarSearchResults({ limit: radarPagination.limit });
|
||||
fetchRadarSearchOptions();
|
||||
fetchRadarSearchOptions([]);
|
||||
addLog('info', '已清除检索条件,请点击"搜索"或"搜索全部"获取数据。');
|
||||
}, [
|
||||
radarPagination.limit, radarSearchRequestSeqRef,
|
||||
@@ -377,7 +397,7 @@ export default function useRadarSearch({
|
||||
setRadarSearchRegionError('');
|
||||
setRadarSearchAoiToken('');
|
||||
setSelectedSatelliteGroup('all');
|
||||
fetchRadarSearchOptions();
|
||||
fetchRadarSearchOptions([]);
|
||||
|
||||
setHasRadarSearched(true);
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -20,8 +20,8 @@ export const usePairingStore = create((set) => ({
|
||||
spatial_baseline_max_meters: 3000,
|
||||
limit_footprint_center_distance: false,
|
||||
coverage_diversity_penalty: 0.3,
|
||||
require_same_imaging_mode: false,
|
||||
require_same_polarization: false,
|
||||
require_same_imaging_mode: true,
|
||||
require_same_polarization: true,
|
||||
aoi_overlap_threshold: 0,
|
||||
start_date: '',
|
||||
// === 新增字段 ===
|
||||
@@ -32,7 +32,7 @@ export const usePairingStore = create((set) => ({
|
||||
strategy: 'all',
|
||||
num_connections: 1,
|
||||
reference_image_id: null,
|
||||
allowed_satellites: ['LT1A', 'LT1B'],
|
||||
allowed_satellites: null,
|
||||
cross_satellite_pairing: false,
|
||||
},
|
||||
pairingAlert: { warnings: [], fallbackUsed: false },
|
||||
|
||||
@@ -33,6 +33,8 @@ export const getLeftTabLabel = (tabKey, metrics = {}) => {
|
||||
switch (tabKey) {
|
||||
case 'ingest':
|
||||
return '入库监控';
|
||||
case 'asset_inventory':
|
||||
return '资产库存';
|
||||
case 'data':
|
||||
return '数据列表';
|
||||
case 'hazard':
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export function normalizeSatelliteFamily(value) {
|
||||
const raw = String(value || '').trim().toUpperCase();
|
||||
if (!raw) return '';
|
||||
const compact = raw.replace(/[-_\s]+/g, '');
|
||||
if (['LT1', 'LT1A', 'LT1B', 'LUTAN1', 'LUTAN1A', 'LUTAN1B'].includes(compact)) return 'LT1';
|
||||
if (['S1', 'S1A', 'S1B', 'S1C', 'SENTINEL1', 'SENTINEL1A', 'SENTINEL1B', 'SENTINEL1C'].includes(compact)) return 'S1';
|
||||
if (['GF3', 'GAOFEN3'].includes(compact)) return 'GF3';
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function inferSatelliteFamilyFromResultLike(item) {
|
||||
const direct = normalizeSatelliteFamily(
|
||||
item?.satellite_family
|
||||
|| item?.master_satellite
|
||||
|| item?.slave_satellite
|
||||
|| item?.satellite
|
||||
);
|
||||
if (direct) return direct;
|
||||
|
||||
const pairKey = String(item?.pair_key || '').trim().toLowerCase();
|
||||
if (pairKey.startsWith('s1_')) return 'S1';
|
||||
if (pairKey.startsWith('lt1_')) return 'LT1';
|
||||
if (pairKey.startsWith('gf3_')) return 'GF3';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function formatSatelliteFamilyLabel(value) {
|
||||
const family = normalizeSatelliteFamily(value);
|
||||
if (family === 'S1') return 'Sentinel-1';
|
||||
if (family === 'LT1') return 'LT-1';
|
||||
if (family === 'GF3') return 'GF3';
|
||||
return family || '-';
|
||||
}
|
||||
Reference in New Issue
Block a user