diff --git a/.gitignore b/.gitignore
index 24566e0..0f56992 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@ nginx/temp/
experiments/**/scratch/
backend/quality_model.pkl
/_par_win_*.xls
+.codex_tmp/
# Large local datasets / installers
Data/
diff --git a/INIT.md b/INIT.md
index 34ec84b..18a6f0f 100644
--- a/INIT.md
+++ b/INIT.md
@@ -1,5 +1,8 @@
# INIT
+> 说明:本文件是开发/运维工作笔记,不是当前系统架构、部署或运行事实的最高依据。
+> 优先阅读 `README.md`、`docs/INDEX.md` 与 `docs/DOCUMENTATION_GOVERNANCE.md`。
+
## 1. 当前开发机已确认的本机环境
- Windows 开发机 Python 解释器:`C:\ProgramData\anaconda3\envs\InSAR\python.exe`
@@ -33,23 +36,32 @@
- 修复 D-InSAR 运行时目录处理逻辑,避免继续依赖历史开发机 `Z:\` 路径。
- 当前原则:路径统一走 `.env`,不允许把开发机专用盘符写入跟踪代码。
-### 2.3 前端乱码修复
+### 2.3 前端编码与临时文件收口
本轮已清理以下面板中的残留乱码与混杂英文:
- `frontend/src/DinsarProductionPanel.jsx`
- `frontend/src/DinsarProductsPanel.jsx`
-日志管理原文件 `frontend/src/LogManagementPanel.jsx` 本身存在历史乱码内容,而且开发过程中目标文件一度被占用,当前采用的稳定方案是:
+此前前端存在一批过渡文件名,例如:
-- 新增干净版本:`frontend/src/LogManagementPanel.clean.jsx`
-- 由 `frontend/src/HealthCheckPanel.jsx` 暂时改为导入 `LogManagementPanel.clean`
+- `*.rewrite.jsx`
+- `LogManagementPanel.clean.jsx`
-这样做的目的:
+这些文件名现在已经完成收口,当前应以正式文件名作为事实来源:
-- 先保证页面可用、无乱码
-- 不在文件被占用时强行覆盖旧文件
-- 后续如果旧文件占用解除,再考虑把 clean 版本回收为正式文件名
+- `frontend/src/DinsarProductsPanel.jsx`
+- `frontend/src/components/DinsarCatalogPanel.jsx`
+- `frontend/src/panels/DinsarResultPanel.jsx`
+- `frontend/src/components/ResultExportModal.jsx`
+- `frontend/src/components/panels/DinsarResultRow.jsx`
+- `frontend/src/LogManagementPanel.jsx`
+
+当前原则:
+
+- 不再让“rewrite / clean”文件名长期停留在主分支
+- 当前被界面实际使用的实现,应恢复为正式文件名
+- 如后续再次出现临时过渡文件,必须在功能稳定后尽快收口
## 3. PowerShell 使用注意事项
@@ -99,4 +111,3 @@ $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
- 运行监控
- 处理模板
- D-InSAR 产物说明文案
-
diff --git a/docs/FRONTEND_NAVIGATION_ARCHITECTURE.md b/docs/FRONTEND_NAVIGATION_ARCHITECTURE.md
index 1d2b7e6..624614c 100644
--- a/docs/FRONTEND_NAVIGATION_ARCHITECTURE.md
+++ b/docs/FRONTEND_NAVIGATION_ARCHITECTURE.md
@@ -2,166 +2,205 @@
## 1. Purpose
-This document is the source of truth for the left-side navigation structure in the frontend.
+This document is the source of truth for the left-side navigation structure and the production workspace view model in the frontend.
It explains:
- the first-level menu groups
-- the second-level working domains
-- the leaf tabs bound to actual pages
-- the reserved entries for future modules
+- the sectioned groups and their leaf tabs
+- the special-case production workspace entry
+- the reserved entries and legacy route aliases
- the files that must be updated when navigation changes
-The goal is to keep module boundaries stable as the system expands beyond D-InSAR into PS-InSAR and broader AI analysis workflows.
+The goal is to keep module boundaries stable as the system expands across D-InSAR, time-series InSAR, AI analysis, and operations workflows.
## 2. Current First-Level Groups
The current first-level menu groups are:
- `data`: 数据管理
-- `production`: 生产规划
+- `production_planning`: 生产规划
+- `production_management`: 生产管理
- `insar_analysis`: InSAR形变分析
- `ai_analysis`: AI分析
- `water`: 水体监测
- `ops`: 运行维护
-Definition file:
+Definition files:
- `frontend/src/config/appConstants.js`
+- `frontend/src/utils/appUiHelpers.js`
-## 3. Three-Level Structure
+## 3. Navigation Model
-Only some first-level groups use second-level working domains.
+The current frontend uses two navigation patterns:
+
+1. Sectioned navigation:
+ first-level group -> second-level section -> leaf tab
+2. Workspace navigation:
+ first-level group -> single leaf tab -> internal workspace view switcher
### 3.1 生产规划
+This is a sectioned group.
+
```text
生产规划
├─ 规划编组
-│ ├─ 配对规划
-│ ├─ 任务规划
-│ ├─ PS时序栈
-│ └─ 任务批次
-├─ 数据分发
-│ └─ 数据分发
-├─ D-InSAR
-│ ├─ D-InSAR生产
-│ └─ D-InSAR产物
-└─ PS-InSAR
- ├─ PS-InSAR生产
- └─ PS-InSAR产物
+│ ├─ 配对规划 (`pairing`)
+│ ├─ 任务规划 (`pairs`)
+│ ├─ 时序候选栈 (`ps_results`)
+│ └─ 任务批次 (`batches`)
+└─ 数据分发
+ └─ 数据分发 (`copier`)
```
Notes:
-- `PS时序栈` belongs to production planning, not analysis.
-- `D-InSAR产物` is distinct from `D-InSAR结果`.
-- `PS-InSAR生产` and `PS-InSAR产物` are reserved entries for future implementation.
+- `ps_results` here means planning-stage candidate stacks, not analysis-facing result pages.
+- This group no longer hosts D-InSAR production or product pages.
-### 3.2 InSAR形变分析
+### 3.2 生产管理
+
+This is a workspace group, not a multi-tab planning tree.
+
+```text
+生产管理
+└─ 生产管理 (`production_management`)
+ ├─ D-InSAR运行 (`dinsar_runs`)
+ ├─ 时序InSAR运行 (`timeseries_runs`)
+ ├─ D-InSAR产物 (`dinsar_products`)
+ └─ 时序InSAR产物 (`timeseries_products`)
+```
+
+Notes:
+
+- The left navigation contains only one tab for this group: `production_management`.
+- Internal workspace views are controlled by `PRODUCTION_WORKSPACE_VIEWS`.
+- Route alias mapping is controlled by `PRODUCTION_WORKSPACE_ENTRY_TO_VIEW`.
+- Legacy route tabs such as `dinsar_production` and `ps_products` map into this workspace and should not be treated as standalone left-nav entries.
+
+### 3.3 InSAR形变分析
+
+This is a sectioned group.
```text
InSAR形变分析
├─ D-InSAR
-│ ├─ D-InSAR结果
-│ └─ D-InSAR分析
-└─ PS-InSAR
- ├─ PS-InSAR结果
- └─ PS-InSAR分析
+│ ├─ D-InSAR结果 (`dinsar_results`)
+│ └─ D-InSAR分析 (`dinsar_analysis`)
+└─ 时序InSAR
+ ├─ 时序InSAR结果 (`psinsar_results`)
+ └─ 时序InSAR分析 (`psinsar_analysis`)
```
Notes:
-- This group is for business-facing result browsing and deformation analysis.
-- AI diagnosis no longer belongs here.
-- `D-InSAR分析`, `PS-InSAR结果`, and `PS-InSAR分析` are currently reserved placeholders.
+- This group is for business-facing result browsing and interpretation.
+- AI diagnosis does not belong here.
+- `dinsar_analysis`, `psinsar_results`, and `psinsar_analysis` are currently reserved placeholders.
-### 3.3 AI分析
+### 3.4 AI分析
+
+This is a sectioned group.
```text
AI分析
├─ 形变智能分析
-│ ├─ AI质量评估
-│ └─ D-InSAR诊断
+│ ├─ AI质量评估 (`ai_quality`)
+│ └─ D-InSAR诊断 (`ai_diagnosis`)
└─ 遥感视觉分析
- ├─ 滑坡语义分割
- └─ 无人机影像分析
+ ├─ 滑坡语义分割 (`landslide_segmentation`)
+ └─ 无人机影像分析 (`uav_image_analysis`)
```
Notes:
-- This group owns model-centric and intelligent-analysis capabilities.
-- `D-InSAR诊断` is the renamed placement of the old AI diagnosis page.
-- `滑坡语义分割` and `无人机影像分析` are reserved placeholders for future AI modules.
+- `ai_diagnosis` is the actual tab key; its display label is `D-InSAR诊断`.
+- `landslide_segmentation` and `uav_image_analysis` remain reserved placeholders.
-## 4. Navigation Design Rules
+### 3.5 无二级分组的一级入口
+
+The following groups do not define second-level sections:
+
+- `data`
+ leaf tabs: `ingest`, `data`, `hazard`
+- `water`
+ leaf tabs: `water`
+- `ops`
+ leaf tabs: `health`, `users`, `audit`
+
+## 4. Source-Of-Truth Rules
The navigation follows these rules:
-- First-level groups represent stable business domains.
-- Second-level domains represent workflow clusters inside a domain.
-- Leaf tabs represent actual pages bound to `leftPanelTab`.
-- The state source of truth remains `leftPanelTab`; group and section are derived from the tab key.
-- New features should be added as leaf tabs under an existing domain whenever possible.
-- A new first-level group should be introduced only when the feature becomes a long-term standalone capability cluster.
+- `LEFT_GROUP_LABELS` defines the first-level group vocabulary.
+- `LEFT_GROUP_SECTIONS` defines second-level sections where they exist.
+- `LEFT_GROUP_TABS` defines which leaf tabs belong to each group.
+- `LEFT_TAB_GROUP` and `LEFT_TAB_SECTION` are derived maps and should not be edited manually.
+- `leftPanelTab` remains the route/state source of truth for the selected leaf entry.
+- `production_management` is a special case: one left-nav tab owns multiple internal workspace views.
+- New features should be added under an existing group whenever possible.
+- A new first-level group should be introduced only for a durable, independent capability area.
## 5. Naming Rules
-To avoid future ambiguity, follow these naming constraints:
+To avoid future ambiguity, use these naming constraints:
-- Use `结果` for result browsing, querying, and visualization pages.
-- Use `产物` for extraction, packaging, publishing, and catalog management pages.
-- Use `分析` for interpretation, statistics,专题分析, and human-facing analytical workflows.
-- Use `诊断` for model-assisted fault analysis or AI-driven case reasoning.
-- Do not reuse `PS结果` as a generic label.
- Use `PS时序栈` in production planning.
- Use `PS-InSAR结果` in analysis.
+- Use `结果` for browsing, querying, and result-facing visualization pages.
+- Use `产物` for extraction, publishing, packaging, and catalog-management pages.
+- Use `运行` for task submission, engine selection, execution control, and runtime monitoring views.
+- Use `分析` for interpretation, statistics, and analyst-facing thematic workflows.
+- Use `诊断` for model-assisted fault analysis or AI-driven reasoning pages.
+- Use `时序候选栈` only for planning-stage candidate stacks under `production_planning`.
+- Use `时序InSAR结果` for analysis-facing result pages under `insar_analysis`.
-## 6. Files to Update When Navigation Changes
+## 6. Files To Update When Navigation Changes
When adding or moving a tab, update these files together:
- `frontend/src/config/appConstants.js`
- Defines first-level groups, second-level sections, tab ownership, and visibility.
+ Defines first-level groups, sections, tab ownership, workspace view mappings, and admin-only visibility.
- `frontend/src/utils/appUiHelpers.js`
Defines display labels for leaf tabs.
+- `frontend/src/components/app/AppSidePanel.jsx`
+ Renders the side-panel navigation and group/section switching behavior.
- `frontend/src/App.jsx`
- Renders the group tabs, section tabs, and page content.
+ Connects route state with panel rendering.
+- `frontend/src/ProductionWorkspace.jsx`
+ Owns the internal production workspace view switcher.
- `frontend/src/App.css`
- Styles the first-level, second-level, and leaf-tab navigation.
+ Styles the navigation hierarchy and workspace entry state.
If the new tab is a real page instead of a placeholder, also add or update the corresponding panel component.
-## 7. Reserved Leaf Tabs
+## 7. Reserved Entries And Legacy Route Aliases
-The following leaf tabs are intentionally reserved for future work:
+Reserved leaf tabs:
-- `ps_production`
-- `ps_products`
- `dinsar_analysis`
- `psinsar_results`
- `psinsar_analysis`
- `landslide_segmentation`
- `uav_image_analysis`
-Reserved tabs should remain visible in the information architecture if they help stabilize the long-term module layout.
+Legacy route aliases mapped into `production_management`:
-## 8. Implementation Notes
+- `dinsar_production`
+- `dinsar_products`
+- `ps_production`
+- `ps_products`
-- Admin-only visibility is controlled by `ADMIN_ONLY_TABS` in `appConstants.js`.
-- Section ownership is derived by `LEFT_TAB_SECTION`.
-- Group ownership is derived by `LEFT_TAB_GROUP`.
-- Groups without second-level sections still render as a two-level navigation.
-- Groups with configured sections render as three-level navigation.
+These aliases exist for compatibility, but they are not first-class left-nav entries anymore.
-## 9. Future Extension Guidance
+## 8. Future Extension Guidance
Recommended future additions:
-- Put PS-InSAR processing forms, task submission, and runtime monitoring under `ps_production`.
-- Put PS-InSAR extraction, packaging, registration, and catalog maintenance under `ps_products`.
-- Put thematic deformation analysis and reporting under `dinsar_analysis` and `psinsar_analysis`.
-- Put optical, UAV, or segmentation-based AI modules under `ai_analysis`, not under `insar_analysis`.
+- Put new production execution or product-governance capability under `production_management` as an internal workspace view unless a separate first-level domain is clearly required.
+- Put planning, batching, pairing, and dispatch preparation capability under `production_planning`.
+- Put result browsing and analyst-facing deformation interpretation under `insar_analysis`.
+- Put intelligent interpretation, diagnosis, segmentation, and computer-vision modules under `ai_analysis`.
-If a new feature belongs to intelligent interpretation or computer vision, prefer adding it under `AI分析`.
-If a new feature belongs to result browsing or deformation business analysis, prefer adding it under `InSAR形变分析`.
+If a new feature belongs to intelligent interpretation or computer vision, prefer `AI分析`.
+If a new feature belongs to result browsing or deformation business analysis, prefer `InSAR形变分析`.
diff --git a/frontend/src/DinsarProductsPanel.jsx b/frontend/src/DinsarProductsPanel.jsx
index ebb47d2..cccb15c 100644
--- a/frontend/src/DinsarProductsPanel.jsx
+++ b/frontend/src/DinsarProductsPanel.jsx
@@ -1,18 +1,10 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { scanDinsarResults } from './api/dinsar';
import { extractDispResults } from './api/idl';
import { clearTaskLogs, deleteTaskLog, getActiveTasks, getRecentTasks, getTaskLogs } from './api/tasks';
import DinsarCatalogPanel from './components/DinsarCatalogPanel';
-const card = {
- background: '#fff',
- padding: '12px',
- borderRadius: '8px',
- border: '1px solid #e2e8f0',
- marginBottom: '12px',
-};
-
const PRODUCT_TASK_TYPES = [
'SCAN_DINSAR',
'PUBLISH_DINSAR_PRODUCTS',
@@ -20,9 +12,9 @@ const PRODUCT_TASK_TYPES = [
];
const TASK_TYPE_LABEL = {
- SCAN_DINSAR: 'D-InSAR结果扫描',
- PUBLISH_DINSAR_PRODUCTS: 'D-InSAR产物发布',
- REBUILD_DINSAR_CATALOG: 'D-InSAR目录重建',
+ SCAN_DINSAR: 'D-InSAR 结果扫描',
+ PUBLISH_DINSAR_PRODUCTS: 'D-InSAR 产物发布',
+ REBUILD_DINSAR_CATALOG: 'D-InSAR 目录重建',
};
const STATUS_LABEL = {
@@ -42,6 +34,18 @@ function formatStatus(status) {
return STATUS_LABEL[status] || status || '-';
}
+function getMessageTone(message, fallbackError = false) {
+ if (fallbackError) return 'error';
+ return /失败|error|Error|ERROR/.test(String(message || '')) ? 'error' : 'success';
+}
+
+function getLogTone(level) {
+ const normalized = String(level || '').toUpperCase();
+ if (normalized === 'ERROR') return 'error';
+ if (normalized === 'WARNING' || normalized === 'WARN') return 'warn';
+ return 'info';
+}
+
export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
const [extractRootDir, setExtractRootDir] = useState('');
const [extractDestDir, setExtractDestDir] = useState('');
@@ -60,12 +64,13 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
const monitoredTask = activeTask || recentTask;
const logTaskId = monitoredTask?.task_id || '';
const showingRecentTask = !activeTask && !!recentTask;
+ const actionTone = getMessageTone(actionMessage, actionError);
const loadActiveTask = useCallback(async () => {
try {
const data = await getActiveTasks();
const tasks = Array.isArray(data) ? data : (data?.tasks || []);
- const relevantTask = tasks.find(task => PRODUCT_TASK_TYPES.includes(task.task_type)) || null;
+ const relevantTask = tasks.find((task) => PRODUCT_TASK_TYPES.includes(task.task_type)) || null;
setActiveTask(relevantTask);
return relevantTask;
} catch {
@@ -86,7 +91,7 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
}
}, []);
- const loadTaskLogs = useCallback(async taskId => {
+ const loadTaskLogs = useCallback(async (taskId) => {
if (!taskId) {
setTaskLogs([]);
return;
@@ -102,7 +107,20 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
}
}, []);
- const handleDeleteTaskLog = useCallback(async logId => {
+ 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(() => {
+ refreshMonitor();
+ }, [refreshMonitor]);
+
+ const handleDeleteTaskLog = useCallback(async (logId) => {
const taskId = logTaskId;
if (!taskId || !logId || taskLogActionLoading) return;
if (!window.confirm('确定要删除这条任务日志吗?')) return;
@@ -138,19 +156,6 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
}
}, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
- 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(() => {
- refreshMonitor();
- }, [refreshMonitor]);
-
const handleExtract = async () => {
if (!extractRootDir.trim()) return;
setExtracting(true);
@@ -174,267 +179,203 @@ export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
setActionError(false);
try {
const result = await scanDinsarResults();
- setActionMessage(result?.message || `D-InSAR结果扫描任务已入队:${result?.task_id || '-'}`);
+ setActionMessage(result?.message || `D-InSAR 结果扫描任务已入队:${result?.task_id || '-'}`);
if (result?.task_id) {
onJobQueued?.(result.task_id);
}
await refreshMonitor();
} catch (err) {
setActionError(true);
- setActionMessage(err?.response?.data?.detail || err.message || 'D-InSAR结果扫描失败');
+ setActionMessage(err?.response?.data?.detail || err.message || 'D-InSAR 结果扫描失败');
} finally {
setScanning(false);
}
};
+ const monitorTone = useMemo(() => {
+ if (!monitoredTask) return 'neutral';
+ if (showingRecentTask) return 'info';
+ return String(monitoredTask.status || '').toUpperCase() === 'RUNNING' ? 'warn' : 'neutral';
+ }, [monitoredTask, showingRecentTask]);
+
return (
-
-
-
D-InSAR 产物提取与重扫
-
-
- 这里负责把生产目录中的位移结果提取为标准成果包,并触发结果重扫、发布和编目。生产运行与参数配置已独立放到“D-InSAR生产”选项卡。
+
+
+
+
D-InSAR 结果提取与标准目录
+
+ 这里负责把生产目录中的位移结果提取为标准成果包,并触发统一扫描、发布和编目。
+ 生产运行与参数配置现已收口到“生产管理”工作台中的 “D-InSAR 运行” 子视图。
+
-
-
-
setExtractRootDir(event.target.value)}
- placeholder="结果根目录"
- style={{ flex: 2, minWidth: 220, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }}
- />
-
setExtractDestDir(event.target.value)}
- placeholder="目标目录(可选)"
- style={{ flex: 1, minWidth: 180, padding: '5px 8px', borderRadius: 4, border: '1px solid #e2e8f0', fontSize: 13 }}
- />
-
- {extracting ? '提取中...' : '提取位移结果'}
-
-
- {scanning ? '重扫中...' : '重扫结果'}
-
+
+
+ {readOnly ? '只读模式' : '可执行写操作'}
+
+ 日志改为手动刷新
-
- {actionMessage && (
-
- {actionMessage}
-
- )}
-
- {extractResult && (
-
- {extractResult.error ? (
-
提取失败:{extractResult.error}
- ) : (
-
-
- 提取完成:复制 {extractResult.copied || 0} 个文件,覆盖 {extractResult.overwritten || 0} 个文件。
-
- {extractResult.catalog?.attempted && extractResult.catalog?.status === 'ok' && (
-
- 成果目录已同步:发布 {extractResult.catalog?.publish?.processed || 0} 项,重建登记 {extractResult.catalog?.rebuild?.registered || 0} 项。
-
- )}
- {extractResult.catalog?.attempted && extractResult.catalog?.status === 'error' && (
-
- 标准成果包目录同步失败:{extractResult.catalog?.message}
-
- )}
-
- )}
-
- )}
-
-
- 产物任务监控
-
- 刷新
-
-
-
- 任务状态与日志改为手动刷新,避免页面持续轮询。
-
+
+
+
+
+ 结果提取与重扫
+ 先提取标准结果包,再按统一目录登记
+
+
- {!monitoredTask ? (
- 当前没有正在执行的产物处理任务。
- ) : (
-
-
+
+ 结果根目录
+ setExtractRootDir(event.target.value)}
+ placeholder="例如:D:\\Task_Pool\\DInSAR"
+ />
+
+
+ 目标目录(可选)
+ setExtractDestDir(event.target.value)}
+ placeholder="留空则使用系统默认"
+ />
+
+
+
+
+
- {showingRecentTask ? '最近一次任务' : '当前任务'}
+ {extracting ? '提取中...' : '提取位移结果'}
+
+
+ {scanning ? '重扫中...' : '重扫结果'}
+
+
+
+ {actionMessage && (
+
+ {actionMessage}
-
- {monitoredTask.task_id} - {formatTaskType(monitoredTask.task_type)} - {formatStatus(monitoredTask.status)} - {monitoredTask.message}
+ )}
+
+ {extractResult && (
+
+ {extractResult.error ? (
+
提取失败:{extractResult.error}
+ ) : (
+ <>
+
提取完成:复制 {extractResult.copied || 0} 个文件,覆盖 {extractResult.overwritten || 0} 个文件。
+ {extractResult.catalog?.attempted && extractResult.catalog?.status === 'ok' && (
+
+ 标准结果目录已同步:发布 {extractResult.catalog?.publish?.processed || 0} 项,
+ 重建登记 {extractResult.catalog?.rebuild?.registered || 0} 项。
+
+ )}
+ {extractResult.catalog?.attempted && extractResult.catalog?.status === 'error' && (
+
标准结果目录同步失败:{extractResult.catalog?.message}
+ )}
+ >
+ )}
- {monitoredTask.progress != null && (
-
-
-
+ )}
+
+
+
+
+
+ 产物任务监控
+ 当前不轮询,按需手动刷新
+
+
刷新
+
+
+ {!monitoredTask ? (
+ 当前没有正在执行的产物处理任务。
+ ) : (
+
+
+
+ {showingRecentTask ? '最近一次任务' : '当前任务'}
+ {formatTaskType(monitoredTask.task_type)}
-
{monitoredTask.progress}%
+
- )}
-
-
-
- {showingRecentTask ? '最近一次任务日志' : '当前任务日志'}
+
{monitoredTask.task_id}
+
{monitoredTask.message || '-'}
+
+ {monitoredTask.progress != null && (
+
+
+
{monitoredTask.progress}%
+ )}
+
+
+ {showingRecentTask ? '最近一次任务日志' : '当前任务日志'}
{!readOnly && (
{taskLogActionLoading && taskLogDeletingId == null ? '清空中...' : '清空日志'}
)}
+
{taskLogsLoading ? (
-
加载中...
+
日志加载中...
) : taskLogs.length === 0 ? (
-
暂无日志。
+
暂无日志。
) : (
-
- {taskLogs.map((log, index) => (
-
+
+ {taskLogs.map((log, index) => {
+ const tone = getLogTone(log.level);
+ return (
-
- {(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
+
+
+ {(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
+
+
{log.message}
-
{log.message}
+ {!readOnly && (
+
handleDeleteTaskLog(log.id)}
+ disabled={taskLogActionLoading || !log.id}
+ >
+ {taskLogDeletingId === log.id ? '删除中...' : '删除'}
+
+ )}
- {!readOnly && (
-
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 ? '删除中...' : '删除'}
-
- )}
-
- ))}
+ );
+ })}
)}
-
- )}
+ )}
+
+ {formatStatus(status)}
+
+ );
+}
diff --git a/frontend/src/DinsarProductsPanel.rewrite.jsx b/frontend/src/DinsarProductsPanel.rewrite.jsx
deleted file mode 100644
index 87d95a1..0000000
--- a/frontend/src/DinsarProductsPanel.rewrite.jsx
+++ /dev/null
@@ -1,405 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-
-import { scanDinsarResults } from './api/dinsar';
-import { extractDispResults } from './api/idl';
-import { clearTaskLogs, deleteTaskLog, getActiveTasks, getRecentTasks, getTaskLogs } from './api/tasks';
-import DinsarCatalogPanel from './components/DinsarCatalogPanel.rewrite';
-
-const PRODUCT_TASK_TYPES = [
- 'SCAN_DINSAR',
- 'PUBLISH_DINSAR_PRODUCTS',
- 'REBUILD_DINSAR_CATALOG',
-];
-
-const TASK_TYPE_LABEL = {
- SCAN_DINSAR: 'D-InSAR 结果扫描',
- PUBLISH_DINSAR_PRODUCTS: 'D-InSAR 产物发布',
- REBUILD_DINSAR_CATALOG: 'D-InSAR 目录重建',
-};
-
-const STATUS_LABEL = {
- PENDING: '等待中',
- RUNNING: '运行中',
- COMPLETED: '已完成',
- FAILED: '失败',
- CANCELLED: '已取消',
- CANCELED: '已取消',
-};
-
-function formatTaskType(taskType) {
- return TASK_TYPE_LABEL[taskType] || taskType || '-';
-}
-
-function formatStatus(status) {
- return STATUS_LABEL[status] || status || '-';
-}
-
-function getMessageTone(message, fallbackError = false) {
- if (fallbackError) return 'error';
- return /失败|error|Error|ERROR/.test(String(message || '')) ? 'error' : 'success';
-}
-
-function getLogTone(level) {
- const normalized = String(level || '').toUpperCase();
- if (normalized === 'ERROR') return 'error';
- if (normalized === 'WARNING' || normalized === 'WARN') return 'warn';
- return 'info';
-}
-
-export default function DinsarProductsPanel({ readOnly = false, onJobQueued }) {
- const [extractRootDir, setExtractRootDir] = useState('');
- const [extractDestDir, setExtractDestDir] = useState('');
- const [extractResult, setExtractResult] = useState(null);
- const [extracting, setExtracting] = useState(false);
- const [actionMessage, setActionMessage] = useState('');
- const [actionError, setActionError] = useState(false);
- const [scanning, setScanning] = useState(false);
-
- const [activeTask, setActiveTask] = useState(null);
- const [recentTask, setRecentTask] = useState(null);
- const [taskLogs, setTaskLogs] = useState([]);
- const [taskLogsLoading, setTaskLogsLoading] = useState(false);
- const [taskLogActionLoading, setTaskLogActionLoading] = useState(false);
- const [taskLogDeletingId, setTaskLogDeletingId] = useState(null);
- const monitoredTask = activeTask || recentTask;
- const logTaskId = monitoredTask?.task_id || '';
- const showingRecentTask = !activeTask && !!recentTask;
- const actionTone = getMessageTone(actionMessage, actionError);
-
- const loadActiveTask = useCallback(async () => {
- try {
- const data = await getActiveTasks();
- 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);
- const nextTask = Array.isArray(tasks) ? (tasks[0] || null) : null;
- setRecentTask(nextTask);
- return nextTask;
- } catch {
- setRecentTask(null);
- return null;
- }
- }, []);
-
- const loadTaskLogs = useCallback(async (taskId) => {
- if (!taskId) {
- setTaskLogs([]);
- return;
- }
- setTaskLogsLoading(true);
- try {
- const data = await getTaskLogs(taskId, 50, 0);
- setTaskLogs(data?.logs || []);
- } catch {
- setTaskLogs([]);
- } finally {
- setTaskLogsLoading(false);
- }
- }, []);
-
- const 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(() => {
- refreshMonitor();
- }, [refreshMonitor]);
-
- const handleDeleteTaskLog = useCallback(async (logId) => {
- const taskId = logTaskId;
- if (!taskId || !logId || taskLogActionLoading) return;
- if (!window.confirm('确定要删除这条任务日志吗?')) return;
-
- setTaskLogDeletingId(logId);
- setTaskLogActionLoading(true);
- try {
- await deleteTaskLog(taskId, logId);
- await loadTaskLogs(taskId);
- } catch (error) {
- setActionMessage(`删除日志失败:${error?.response?.data?.detail || error.message}`);
- setActionError(true);
- } finally {
- setTaskLogDeletingId(null);
- setTaskLogActionLoading(false);
- }
- }, [logTaskId, loadTaskLogs, taskLogActionLoading]);
-
- const handleClearTaskLogs = useCallback(async () => {
- const taskId = logTaskId;
- if (!taskId || taskLogActionLoading || taskLogs.length === 0) return;
- if (!window.confirm(`确定要清空任务 ${taskId} 的全部日志吗?`)) return;
-
- setTaskLogActionLoading(true);
- try {
- await clearTaskLogs(taskId);
- await loadTaskLogs(taskId);
- } catch (error) {
- setActionMessage(`清空日志失败:${error?.response?.data?.detail || error.message}`);
- setActionError(true);
- } finally {
- setTaskLogActionLoading(false);
- }
- }, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
-
- const handleExtract = async () => {
- if (!extractRootDir.trim()) return;
- setExtracting(true);
- setExtractResult(null);
- setActionMessage('');
- setActionError(false);
- try {
- const result = await extractDispResults(extractRootDir.trim(), extractDestDir.trim() || null);
- setExtractResult(result);
- } catch (err) {
- setExtractResult({ error: err?.response?.data?.detail || err.message });
- } finally {
- setExtracting(false);
- }
- };
-
- const handleScan = async () => {
- if (readOnly) return;
- setScanning(true);
- setActionMessage('');
- setActionError(false);
- try {
- const result = await scanDinsarResults();
- setActionMessage(result?.message || `D-InSAR 结果扫描任务已入队:${result?.task_id || '-'}`);
- if (result?.task_id) {
- onJobQueued?.(result.task_id);
- }
- await refreshMonitor();
- } catch (err) {
- setActionError(true);
- setActionMessage(err?.response?.data?.detail || err.message || 'D-InSAR 结果扫描失败');
- } finally {
- setScanning(false);
- }
- };
-
- const monitorTone = useMemo(() => {
- if (!monitoredTask) return 'neutral';
- if (showingRecentTask) return 'info';
- return String(monitoredTask.status || '').toUpperCase() === 'RUNNING' ? 'warn' : 'neutral';
- }, [monitoredTask, showingRecentTask]);
-
- return (
-
-
-
-
D-InSAR 结果提取与标准目录
-
- 这里负责把生产目录中的位移结果提取为标准成果包,并触发统一扫描、发布和编目。
- 生产运行与参数配置现已收口到“生产管理”工作台中的 “D-InSAR 运行” 子视图。
-
-
-
-
- {readOnly ? '只读模式' : '可执行写操作'}
-
- 日志改为手动刷新
-
-
-
-
-
-
-
- 结果提取与重扫
- 先提取标准结果包,再按统一目录登记
-
-
-
-
-
- 结果根目录
- setExtractRootDir(event.target.value)}
- placeholder="例如:D:\\Task_Pool\\DInSAR"
- />
-
-
- 目标目录(可选)
- setExtractDestDir(event.target.value)}
- placeholder="留空则使用系统默认"
- />
-
-
-
-
-
- {extracting ? '提取中...' : '提取位移结果'}
-
-
- {scanning ? '重扫中...' : '重扫结果'}
-
-
-
- {actionMessage && (
-
- {actionMessage}
-
- )}
-
- {extractResult && (
-
- {extractResult.error ? (
-
提取失败:{extractResult.error}
- ) : (
- <>
-
提取完成:复制 {extractResult.copied || 0} 个文件,覆盖 {extractResult.overwritten || 0} 个文件。
- {extractResult.catalog?.attempted && extractResult.catalog?.status === 'ok' && (
-
- 标准结果目录已同步:发布 {extractResult.catalog?.publish?.processed || 0} 项,
- 重建登记 {extractResult.catalog?.rebuild?.registered || 0} 项。
-
- )}
- {extractResult.catalog?.attempted && extractResult.catalog?.status === 'error' && (
-
标准结果目录同步失败:{extractResult.catalog?.message}
- )}
- >
- )}
-
- )}
-
-
-
-
-
- 产物任务监控
- 当前不轮询,按需手动刷新
-
-
刷新
-
-
- {!monitoredTask ? (
- 当前没有正在执行的产物处理任务。
- ) : (
-
-
-
- {showingRecentTask ? '最近一次任务' : '当前任务'}
- {formatTaskType(monitoredTask.task_type)}
-
-
-
-
-
{monitoredTask.task_id}
-
{monitoredTask.message || '-'}
-
- {monitoredTask.progress != null && (
-
-
-
{monitoredTask.progress}%
-
- )}
-
-
- {showingRecentTask ? '最近一次任务日志' : '当前任务日志'}
- {!readOnly && (
-
- {taskLogActionLoading && taskLogDeletingId == null ? '清空中...' : '清空日志'}
-
- )}
-
-
- {taskLogsLoading ? (
-
日志加载中...
- ) : taskLogs.length === 0 ? (
-
暂无日志。
- ) : (
-
- {taskLogs.map((log, index) => {
- const tone = getLogTone(log.level);
- return (
-
-
-
- {(log.timestamp || '').replace('T', ' ').replace('Z', '')} [{log.level}]
-
-
{log.message}
-
- {!readOnly && (
-
handleDeleteTaskLog(log.id)}
- disabled={taskLogActionLoading || !log.id}
- >
- {taskLogDeletingId === log.id ? '删除中...' : '删除'}
-
- )}
-
- );
- })}
-
- )}
-
- )}
-
-
-
-
-
- );
-}
-
-function StatusSummary({ status }) {
- const normalized = String(status || '').toUpperCase();
- const tone = normalized === 'RUNNING'
- ? 'warn'
- : normalized === 'FAILED'
- ? 'error'
- : normalized === 'COMPLETED'
- ? 'ready'
- : 'neutral';
-
- return (
-
- {formatStatus(status)}
-
- );
-}
diff --git a/frontend/src/HealthCheckPanel.jsx b/frontend/src/HealthCheckPanel.jsx
index 892bf79..17bf681 100644
--- a/frontend/src/HealthCheckPanel.jsx
+++ b/frontend/src/HealthCheckPanel.jsx
@@ -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.clean';
+import LogManagementPanel from './LogManagementPanel';
const toNumber = (value) => {
const parsed = Number(value);
diff --git a/frontend/src/LogManagementPanel.clean.jsx b/frontend/src/LogManagementPanel.clean.jsx
deleted file mode 100644
index 5433242..0000000
--- a/frontend/src/LogManagementPanel.clean.jsx
+++ /dev/null
@@ -1,372 +0,0 @@
-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 (
-
-
-
日志管理
-
- 类型筛选:
- setFilterType(event.target.value)}
- style={{ padding: '5px 10px', borderRadius: '4px', border: '1px solid #ddd' }}
- >
- 全部
- 应用日志
- 任务日志
- 错误日志
-
-
- {loading ? '加载中...' : '刷新'}
-
-
-
-
- {logs.length === 0 ? (
-
暂无日志文件
- ) : (
-
-
-
- 文件名
- 类型
- 大小
- 修改时间
- 操作
-
-
-
- {logs.map((log, index) => (
-
- {log.name}
-
-
- {getTypeLabel(log.type)}
-
-
-
- {formatSize(log.size)}
-
- {log.modified_at}
-
-
- handleViewLog(log)}
- style={{
- padding: '4px 12px',
- backgroundColor: '#3b82f6',
- color: 'white',
- border: 'none',
- borderRadius: '4px',
- cursor: 'pointer',
- fontSize: '13px',
- whiteSpace: 'nowrap',
- }}
- >
- 查看
-
- {isAdmin && (
- handleDeleteLog(log)}
- style={{
- padding: '4px 12px',
- backgroundColor: '#ef4444',
- color: 'white',
- border: 'none',
- borderRadius: '4px',
- cursor: 'pointer',
- fontSize: '13px',
- whiteSpace: 'nowrap',
- }}
- >
- 删除
-
- )}
-
-
-
- ))}
-
-
- )}
-
- {showModal && selectedLog && (
-
-
-
-
-
{selectedLog.name}
-
- 大小:{formatSize(selectedLog.size)} | 修改时间:{selectedLog.modified_at} | 总行数:{totalLines}
-
-
-
setShowModal(false)}
- style={{
- padding: '8px 16px',
- backgroundColor: '#6b7280',
- color: 'white',
- border: 'none',
- borderRadius: '4px',
- cursor: 'pointer',
- }}
- >
- 关闭
-
-
-
-
-
setSearchTerm(event.target.value)}
- style={{
- flex: 1,
- padding: '6px 12px',
- border: '1px solid #d1d5db',
- borderRadius: '4px',
- fontSize: '13px',
- }}
- />
-
- 显示行 {totalLines === 0 ? 0 : currentOffset + 1} - {Math.min(currentOffset + PAGE_SIZE, totalLines)}
-
-
- 上一页
-
-
= 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',
- }}
- >
- 下一页
-
-
-
-
-
- {filteredContent || '(空日志)'}
-
-
-
-
- )}
-
- );
-};
-
-export default LogManagementPanel;
diff --git a/frontend/src/LogManagementPanel.jsx b/frontend/src/LogManagementPanel.jsx
index 94cf303..5433242 100644
--- a/frontend/src/LogManagementPanel.jsx
+++ b/frontend/src/LogManagementPanel.jsx
@@ -1,6 +1,8 @@
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);
@@ -19,17 +21,29 @@ const LogManagementPanel = ({ isAdmin }) => {
setLogs(data);
} catch (error) {
console.error('加载日志列表失败:', error);
- alert(`加载日志列表失败: ${error.response?.data?.detail || error.message}`);
+ 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) => {
+ const handleViewLog = async log => {
setSelectedLog(log);
setShowModal(true);
setCurrentOffset(0);
@@ -37,83 +51,78 @@ const LogManagementPanel = ({ isAdmin }) => {
await loadLogContent(log.path, 0);
};
- const loadLogContent = async (logPath, offset = 0) => {
- try {
- const data = await getLogContent(logPath, offset, 1000);
- setLogContent(data.content);
- setTotalLines(data.total_lines);
- setCurrentOffset(offset);
- } catch (error) {
- console.error('加载日志内容失败:', error);
- alert(`加载日志内容失败: ${error.response?.data?.detail || error.message}`);
- }
- };
-
- const handleDeleteLog = async (log) => {
+ const handleDeleteLog = async log => {
if (!isAdmin) {
- alert('只有管理员可以删除日志');
+ alert('只有管理员可以删除日志。');
return;
}
- if (!window.confirm(`确定要删除日志文件 "${log.name}" 吗?\n\n此操作不可恢复!`)) {
+ if (!window.confirm(`确定要删除日志文件“${log.name}”吗?\n\n此操作不可恢复。`)) {
return;
}
try {
await deleteLog(log.path);
- alert('日志文件已删除');
- loadLogs();
+ 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}`);
+ alert(`删除日志失败:${error.response?.data?.detail || error.message}`);
}
};
const handlePrevPage = () => {
- if (currentOffset > 0) {
- const newOffset = Math.max(0, currentOffset - 1000);
+ if (selectedLog && currentOffset > 0) {
+ const newOffset = Math.max(0, currentOffset - PAGE_SIZE);
loadLogContent(selectedLog.path, newOffset);
}
};
const handleNextPage = () => {
- if (currentOffset + 1000 < totalLines) {
- const newOffset = currentOffset + 1000;
+ if (selectedLog && currentOffset + PAGE_SIZE < totalLines) {
+ const newOffset = currentOffset + PAGE_SIZE;
loadLogContent(selectedLog.path, newOffset);
}
};
- const formatSize = (bytes) => {
+ 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 getTypeLabel = type => {
const labels = {
app: '应用日志',
task: '任务日志',
error: '错误日志',
- other: '其他'
+ other: '其他',
};
return labels[type] || type;
};
- const getTypeColor = (type) => {
+ const getTypeColor = type => {
const colors = {
app: '#3b82f6',
task: '#10b981',
error: '#ef4444',
- other: '#6b7280'
+ other: '#6b7280',
};
return colors[type] || '#6b7280';
};
const filteredContent = searchTerm
- ? logContent.split('\n').filter(line => line.toLowerCase().includes(searchTerm.toLowerCase())).join('\n')
+ ? logContent
+ .split('\n')
+ .filter(line => line.toLowerCase().includes(searchTerm.toLowerCase()))
+ .join('\n')
: logContent;
return (
@@ -121,10 +130,10 @@ const LogManagementPanel = ({ isAdmin }) => {
日志管理
- 类型过滤:
+ 类型筛选:
setFilterType(e.target.value)}
+ onChange={event => setFilterType(event.target.value)}
style={{ padding: '5px 10px', borderRadius: '4px', border: '1px solid #ddd' }}
>
全部
@@ -141,7 +150,7 @@ const LogManagementPanel = ({ isAdmin }) => {
color: 'white',
border: 'none',
borderRadius: '4px',
- cursor: loading ? 'not-allowed' : 'pointer'
+ cursor: loading ? 'not-allowed' : 'pointer',
}}
>
{loading ? '加载中...' : '刷新'}
@@ -150,11 +159,16 @@ const LogManagementPanel = ({ isAdmin }) => {
{logs.length === 0 ? (
-
- 暂无日志文件
-
+
暂无日志文件
) : (
-
+
文件名
@@ -169,13 +183,15 @@ const LogManagementPanel = ({ isAdmin }) => {
{log.name}
-
+
{getTypeLabel(log.type)}
@@ -195,7 +211,7 @@ const LogManagementPanel = ({ isAdmin }) => {
borderRadius: '4px',
cursor: 'pointer',
fontSize: '13px',
- whiteSpace: 'nowrap'
+ whiteSpace: 'nowrap',
}}
>
查看
@@ -211,7 +227,7 @@ const LogManagementPanel = ({ isAdmin }) => {
borderRadius: '4px',
cursor: 'pointer',
fontSize: '13px',
- whiteSpace: 'nowrap'
+ whiteSpace: 'nowrap',
}}
>
删除
@@ -225,42 +241,46 @@ const LogManagementPanel = ({ isAdmin }) => {
)}
- {/* 日志查看 Modal */}
{showModal && selectedLog && (
-
-
- {/* Modal Header */}
-
+
+ flexDirection: 'column',
+ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
+ }}
+ >
+
{selectedLog.name}
- 大小: {formatSize(selectedLog.size)} | 修改时间: {selectedLog.modified_at} | 总行数: {totalLines}
+ 大小:{formatSize(selectedLog.size)} | 修改时间:{selectedLog.modified_at} | 总行数:{totalLines}
{
color: 'white',
border: 'none',
borderRadius: '4px',
- cursor: 'pointer'
+ cursor: 'pointer',
}}
>
关闭
- {/* Search Bar */}
setSearchTerm(e.target.value)}
+ onChange={event => setSearchTerm(event.target.value)}
style={{
flex: 1,
padding: '6px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
- fontSize: '13px'
+ fontSize: '13px',
}}
/>
- 显示行: {currentOffset + 1} - {Math.min(currentOffset + 1000, totalLines)}
+ 显示行 {totalLines === 0 ? 0 : currentOffset + 1} - {Math.min(currentOffset + PAGE_SIZE, totalLines)}
{
border: 'none',
borderRadius: '4px',
cursor: currentOffset === 0 ? 'not-allowed' : 'pointer',
- fontSize: '13px'
+ fontSize: '13px',
}}
>
上一页
= totalLines}
+ disabled={currentOffset + PAGE_SIZE >= totalLines}
style={{
padding: '6px 12px',
- backgroundColor: currentOffset + 1000 >= totalLines ? '#e5e7eb' : '#3b82f6',
- color: currentOffset + 1000 >= totalLines ? '#9ca3af' : 'white',
+ backgroundColor: currentOffset + PAGE_SIZE >= totalLines ? '#e5e7eb' : '#3b82f6',
+ color: currentOffset + PAGE_SIZE >= totalLines ? '#9ca3af' : 'white',
border: 'none',
borderRadius: '4px',
- cursor: currentOffset + 1000 >= totalLines ? 'not-allowed' : 'pointer',
- fontSize: '13px'
+ cursor: currentOffset + PAGE_SIZE >= totalLines ? 'not-allowed' : 'pointer',
+ fontSize: '13px',
}}
>
下一页
- {/* Log Content */}
-
- {filteredContent || '(空日志)'}
+
+ {filteredContent || '(空日志)'}
diff --git a/frontend/src/ProductionWorkspace.jsx b/frontend/src/ProductionWorkspace.jsx
index d2cbea1..b98bb4f 100644
--- a/frontend/src/ProductionWorkspace.jsx
+++ b/frontend/src/ProductionWorkspace.jsx
@@ -9,7 +9,7 @@ import { PanelLoadingBody } from './components/app/AppLoadingFallbacks';
const LazyDinsarProductionPanel = lazy(() => import('./DinsarProductionPanel'));
const LazyTimeseriesProductionPanel = lazy(() => import('./TimeseriesProductionPanel'));
-const LazyDinsarProductsPanel = lazy(() => import('./DinsarProductsPanel.rewrite'));
+const LazyDinsarProductsPanel = lazy(() => import('./DinsarProductsPanel'));
const LazyPsinsarCatalogPanel = lazy(() => import('./components/PsinsarCatalogPanel'));
const shellStyle = {
diff --git a/frontend/src/components/DinsarCatalogPanel.jsx b/frontend/src/components/DinsarCatalogPanel.jsx
index e2a154c..707a83b 100644
--- a/frontend/src/components/DinsarCatalogPanel.jsx
+++ b/frontend/src/components/DinsarCatalogPanel.jsx
@@ -8,21 +8,19 @@ import {
queueDinsarCatalogRebuild,
queueDinsarProductPublish,
} from '../api/dinsarProducts';
+import {
+ DINSAR_ENGINE_ALL,
+ buildDinsarEngineOptions,
+ getDinsarEngineMeta,
+} from '../utils/dinsarEngines';
-const panelCardStyle = {
- background: '#fff',
- padding: '12px',
- borderRadius: '8px',
- border: '1px solid #e2e8f0',
-};
-
-const statusColorMap = {
- READY: '#16a34a',
- PARTIAL: '#b45309',
- QUARANTINED: '#dc2626',
- WARN: '#b45309',
- ERROR: '#dc2626',
- REBUILDING: '#2563eb',
+const STATUS_TONE_MAP = {
+ READY: 'ready',
+ PARTIAL: 'warn',
+ QUARANTINED: 'error',
+ WARN: 'warn',
+ ERROR: 'error',
+ REBUILDING: 'info',
};
function formatDateTime(value) {
@@ -38,37 +36,26 @@ function parseDirectoryList(value) {
return [...new Set(
String(value || '')
.split(/[\r\n,;]+/)
- .map(item => item.trim())
+ .map((item) => item.trim())
.filter(Boolean)
)];
}
-function StatusPill({ label, color }) {
+function getMessageTone(message) {
+ return /失败|error|Error|ERROR/.test(String(message || '')) ? 'error' : 'success';
+}
+
+function StatusPill({ label, tone = 'neutral' }) {
+ return
{label} ;
+}
+
+function MetaField({ label, value, multiline = false }) {
+ const displayValue = value === null || value === undefined || value === '' ? '-' : value;
return (
-
-
- {label}
-
+
+ {label}
+ {displayValue}
+
);
}
@@ -88,13 +75,16 @@ export default function DinsarCatalogPanel({
const [actionMessage, setActionMessage] = useState('');
const [sourceDirectoriesText, setSourceDirectoriesText] = useState(initialSourceDir || '');
const [publishRoot, setPublishRoot] = useState('');
+ const [engineFilter, setEngineFilter] = useState(DINSAR_ENGINE_ALL);
+ const [queryDraft, setQueryDraft] = useState('');
+ const [queryApplied, setQueryApplied] = useState('');
- const listLimit = compact ? 6 : 12;
+ const listLimit = compact ? 8 : 24;
const previewBaseUrl = apiClient.defaults.baseURL || '/api';
useEffect(() => {
if (!initialSourceDir) return;
- setSourceDirectoriesText(current => (current.trim() ? current : initialSourceDir));
+ setSourceDirectoriesText((current) => (current.trim() ? current : initialSourceDir));
}, [initialSourceDir]);
const sourceDirectories = useMemo(
@@ -102,18 +92,39 @@ export default function DinsarCatalogPanel({
[sourceDirectoriesText]
);
+ const engineOptions = useMemo(
+ () => buildDinsarEngineOptions(products, { includeKnown: true }),
+ [products]
+ );
+ const selectedEngineMeta = useMemo(
+ () => (engineFilter === DINSAR_ENGINE_ALL ? null : getDinsarEngineMeta(engineFilter)),
+ [engineFilter]
+ );
+
+ useEffect(() => {
+ if (engineFilter === DINSAR_ENGINE_ALL) return;
+ if (!engineOptions.some((option) => option.value === engineFilter)) {
+ setEngineFilter(DINSAR_ENGINE_ALL);
+ }
+ }, [engineFilter, engineOptions]);
+
const loadCatalog = useCallback(async () => {
setLoading(true);
try {
const [statusData, productData] = await Promise.all([
getDinsarCatalogStatus(),
- listDinsarProducts({ limit: listLimit, offset: 0 }),
+ listDinsarProducts({
+ limit: listLimit,
+ offset: 0,
+ engine_code: engineFilter === DINSAR_ENGINE_ALL ? undefined : engineFilter,
+ query: queryApplied || undefined,
+ }),
]);
setCatalogStatus(statusData);
const nextItems = Array.isArray(productData?.items) ? productData.items : [];
setProducts(nextItems);
- setSelectedProductId(current => {
- if (current && nextItems.some(item => item.id === current)) {
+ setSelectedProductId((current) => {
+ if (current && nextItems.some((item) => item.id === current)) {
return current;
}
return nextItems[0]?.id ?? null;
@@ -126,7 +137,7 @@ export default function DinsarCatalogPanel({
} finally {
setLoading(false);
}
- }, [listLimit]);
+ }, [engineFilter, listLimit, queryApplied]);
const loadProductDetail = useCallback(async (productId) => {
if (!productId) {
@@ -155,6 +166,16 @@ export default function DinsarCatalogPanel({
loadProductDetail(selectedProductId);
}, [loadProductDetail, selectedProductId]);
+ const handleApplyFilters = useCallback(() => {
+ setQueryApplied(queryDraft.trim());
+ }, [queryDraft]);
+
+ const handleResetFilters = useCallback(() => {
+ setEngineFilter(DINSAR_ENGINE_ALL);
+ setQueryDraft('');
+ setQueryApplied('');
+ }, []);
+
const handleQueuePublish = async () => {
if (readOnly || sourceDirectories.length === 0) return;
setActionLoading(true);
@@ -194,7 +215,8 @@ export default function DinsarCatalogPanel({
}
};
- const catalogColor = statusColorMap[catalogStatus?.status] || '#64748b';
+ const catalogTone = STATUS_TONE_MAP[catalogStatus?.status] || 'neutral';
+ const actionTone = getMessageTone(actionMessage);
const selectedIssues = Array.isArray(selectedProduct?.issues) ? selectedProduct.issues : [];
const selectedAssets = Array.isArray(selectedProduct?.assets) ? selectedProduct.assets : [];
const selectedPairingTrace = selectedProduct?.pairing_trace || null;
@@ -202,191 +224,190 @@ export default function DinsarCatalogPanel({
const selectedPairingRun = selectedPairingNetwork?.run || null;
const selectedPairingEdge = selectedPairingNetwork?.edge || null;
const selectedPairingMetric = selectedPairingNetwork?.metric || null;
+ const selectedProductEngine = getDinsarEngineMeta(selectedProduct?.engine_code);
+ const selectedStatusTone = STATUS_TONE_MAP[selectedProduct?.status] || 'neutral';
return (
-
-
-
-
{compact ? '结果目录状态' : '标准结果包目录'}
-
- {compact ? '展示结果包目录与数据库索引状态' : '结果文件以结果包目录为真源,数据库仅保存索引与检索信息'}
-
+
+
+
+
{compact ? '结果目录状态' : '标准结果包目录'}
+
+ {compact
+ ? '查看结果包目录与数据库索引是否一致。'
+ : '统一结果目录按 engine + pair + run 管理,便于同一对影像保留多套生产结果并行对比。'}
+
-
- {loading ? '刷新中...' : '刷新'}
-
-
-
-
-
-
-
需要重建
-
- {catalogStatus?.needs_rebuild ? '是' : '否'}
-
-
-
-
Manifest / 数据库
-
- {(catalogStatus?.manifest_count ?? 0)} / {(catalogStatus?.db_count ?? 0)}
-
-
-
-
问题数量
-
0 ? '#b45309' : '#16a34a' }}>
- {catalogStatus?.issue_count ?? 0}
-
+
+ {selectedEngineMeta && (
+
+ {selectedEngineMeta.shortLabel}
+
+ )}
+
+ {loading ? '刷新中...' : '刷新'}
+
-
+
+
+ 目录状态
+ {catalogStatus?.status || '未知'}
+
+
+
+ 需要重建
+ {catalogStatus?.needs_rebuild ? '是' : '否'}
+ {catalogStatus?.needs_rebuild ? 'Manifest 与数据库存在漂移' : '目录登记正常'}
+
+
+ Manifest / 数据库
+ {catalogStatus?.manifest_count ?? 0} / {catalogStatus?.db_count ?? 0}
+ 已登记结果包总量
+
+
+ 问题数量
+ {catalogStatus?.issue_count ?? 0}
+ 含缺失文件与健康异常
+
+
+
+
结果包根目录: {catalogStatus?.storage_root || '-'}
最近消息: {catalogStatus?.last_message || '-'}
-
最近重建: {formatDateTime(catalogStatus?.last_full_rebuild_at)}
+
最近全量重建: {formatDateTime(catalogStatus?.last_full_rebuild_at)}
- {compact && actionMessage && (
-
+ {actionMessage && (
+
{actionMessage}
)}
{!compact && (
-
-
手动发布与重建
-
- 旧的“提取位移结果”入口已经会自动尝试发布标准结果包。这里保留显式入口,方便你对任意目录重新发布和重建索引。
+
+
+
手动发布与目录重建
+
+ 这里用于把既有结果目录重新发布为标准结果包,并按最新规则重建目录索引。
+ 如果同一对影像存在 ENVI 与 ISCE2 两套结果,它们会依赖 `engine_code` 与 `run_key` 分别登记,不会互相覆盖。
+
-
)}
-
-
-
- 最新结果包 ({products.length})
+
+
+
+
+ 结果包列表
+
+ {loading ? '加载中...' : `当前展示 ${products.length} 条`}
+
+
+ {queryApplied &&
}
+
+
+
+ 生产引擎
+ setEngineFilter(event.target.value)}>
+ 全部引擎
+ {engineOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ 检索
+ setQueryDraft(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ handleApplyFilters();
+ }
+ }}
+ placeholder="搜索任务名 / pair / run / 引擎"
+ />
+
+
+
+ 查询
+ 重置
+
+
+
{products.length === 0 ? (
-
- {loading ? '正在加载结果包...' : '当前没有已注册的结果包。'}
+
+ {loading ? '正在加载结果包...' : '当前筛选条件下没有结果包。'}
) : (
-
- {products.map(item => {
- const color = statusColorMap[item.status] || '#64748b';
+
+ {products.map((item) => {
+ const tone = STATUS_TONE_MAP[item.status] || 'neutral';
+ const engineMeta = getDinsarEngineMeta(item.engine_code);
return (
setSelectedProductId(item.id)}
- style={{
- display: 'block',
- width: '100%',
- textAlign: 'left',
- border: 'none',
- borderTop: '1px solid #f1f5f9',
- background: selectedProductId === item.id ? '#eff6ff' : '#fff',
- padding: '10px 12px',
- cursor: 'pointer',
- }}
>
-
-
{item.display_name || item.product_id}
-
{item.status}
+
+ {item.display_name || item.product_id}
+
-
- {item.engine_code || '-'} · {formatDateTime(item.published_at)}
+
+ {engineMeta.shortLabel}
+ {formatDateTime(item.published_at)}
-
+
{(item.task_alias || item.task_name || '-')}{item.run_key ? ` / ${item.run_key}` : ''}
- {(item.selection_strategy || item.network_run_id || item.network_edge_id) && (
-
+
+ {item.pair_key || '-'}
+
+ {(item.selection_strategy || item.network_run_id || item.network_edge_id != null) && (
+
{(item.selection_strategy || 'trace')}
- {item.network_edge_id ? ` / edge ${item.network_edge_id}` : ''}
+ {item.network_edge_id != null ? ` / edge ${item.network_edge_id}` : ''}
{item.network_run_id ? ` / ${item.network_run_id}` : ''}
)}
@@ -395,167 +416,165 @@ export default function DinsarCatalogPanel({
})}
)}
-
+
-
-
- 结果包详情
+
+
+
+ 结果包详情
+ 查看选中结果的发布信息、配对溯源与资产健康
+
+
{!selectedProductId ? (
- 请选择一个结果包查看详情。
+ 请选择一个结果包查看详情。
) : detailLoading || !selectedProduct ? (
- 正在加载详情...
+ 正在加载详情...
) : selectedProduct?.error ? (
- {selectedProduct.error}
+ {selectedProduct.error}
) : (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
{selectedProduct.display_name || selectedProduct.product_id}
+
{selectedProduct.task_alias || selectedProduct.task_name || '未命名任务'}
+
+
+
+ {selectedProductEngine.shortLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
名称: {selectedProduct.display_name || '-'}
-
产品编号: {selectedProduct.product_id || '-'}
-
任务别名: {selectedProduct.task_alias || selectedProduct.task_name || '-'}
-
配对标识: {selectedProduct.pair_key || '-'}
-
场景对 UID: {selectedProduct.pair_uid || '-'}
-
运行标识: {selectedProduct.run_key || '-'}
-
生产配置: {selectedProduct.profile_code || '-'}
-
引擎: {selectedProduct.engine_code || '-'}
-
状态: {selectedProduct.status || '-'} / {selectedProduct.health_status || '-'}
-
主文件: {selectedProduct.primary_asset_path || '-'}
-
来源文件: {selectedProduct.source_primary_path || '-'}
-
结果包目录: {selectedProduct.publish_dir || '-'}
+
+
+
-
-
-
主影像日期: {selectedProduct.profile?.master_imaging_date || '-'}
-
辅影像日期: {selectedProduct.profile?.slave_imaging_date || '-'}
-
时间基线: {selectedProduct.profile?.time_baseline_days ?? '-'}
-
空间基线: {selectedProduct.profile?.spatial_baseline_meters ?? '-'}
-
-
-
BBox:
-
{selectedProduct.min_lon ?? '-'}, {selectedProduct.min_lat ?? '-'}
-
{selectedProduct.max_lon ?? '-'}, {selectedProduct.max_lat ?? '-'}
-
注册时间: {formatDateTime(selectedProduct.registered_at)}
-
-
-
-
-
配对追踪
+
+
配对追踪
{!selectedPairingTrace?.network_run_id ? (
-
当前结果未携带配对网络追踪信息。
+
当前结果未携带完整的配对网络追踪信息。
) : (
-
-
-
-
network_run_id: {selectedPairingTrace.network_run_id || '-'}
-
network_edge_id: {selectedPairingTrace.network_edge_id ?? '-'}
-
pair_uid: {selectedPairingTrace.pair_uid || '-'}
-
策略: {selectedPairingTrace.selection_strategy || '-'}
-
策略版本: {selectedPairingTrace.policy_version || '-'}
-
-
-
网络记录: {selectedPairingNetwork?.run_found ? '已找到' : '未找到'}
-
边记录: {selectedPairingNetwork?.edge_found ? '已找到' : '未找到'}
-
运行状态: {selectedPairingRun?.status || '-'}
-
候选边数: {selectedPairingRun?.candidate_count ?? '-'}
-
入选边数: {selectedPairingRun?.selected_edge_count ?? '-'}
-
告警数: {selectedPairingRun?.warning_count ?? '-'}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- {(selectedPairingEdge || selectedPairingMetric) && (
-
-
-
网络边
-
edge_rank: {selectedPairingEdge?.edge_rank ?? '-'}
-
selection_reason: {selectedPairingEdge?.selection_reason || '-'}
-
selection_score: {selectedPairingEdge?.selection_score ?? '-'}
-
reference_edge: {selectedPairingEdge?.is_reference_edge ? '是' : '否'}
-
metric_cache_ref_id: {selectedPairingEdge?.metric_cache_ref_id ?? '-'}
-
-
-
度量快照
-
主从日期: {selectedPairingMetric?.master_imaging_date || '-'} / {selectedPairingMetric?.slave_imaging_date || '-'}
-
主从卫星: {selectedPairingMetric?.master_satellite || '-'} / {selectedPairingMetric?.slave_satellite || '-'}
-
主从模式: {selectedPairingMetric?.master_imaging_mode || '-'} / {selectedPairingMetric?.slave_imaging_mode || '-'}
-
主从极化: {selectedPairingMetric?.master_polarization || '-'} / {selectedPairingMetric?.slave_polarization || '-'}
-
时间基线: {selectedPairingMetric?.time_baseline_days ?? '-'}
-
空间基线: {selectedPairingMetric?.spatial_baseline_meters ?? '-'}
-
重叠率: {selectedPairingMetric?.scene_overlap_ratio ?? '-'}
-
-
- )}
)}
-
-
资产列表 ({selectedAssets.length})
- {selectedAssets.length === 0 ? (
-
暂无资产记录。
- ) : (
- selectedAssets.map(asset => (
-
-
{asset.asset_role} · {asset.asset_name}
-
- {asset.exists_flag ? '文件存在' : '文件缺失'}
-
-
{asset.absolute_path}
-
- ))
- )}
-
-
-
-
问题列表 ({selectedIssues.length})
- {selectedIssues.length === 0 ? (
-
当前没有登记问题。
- ) : (
- selectedIssues.map(issue => (
-
-
{issue.issue_code} · {issue.severity}
-
{issue.message}
- {issue.repair_action && (
-
- 建议修复动作:{issue.repair_action}
+
+
+
资产列表 ({selectedAssets.length})
+ {selectedAssets.length === 0 ? (
+
暂无资产记录。
+ ) : (
+
+ {selectedAssets.map((asset) => (
+
+
+ {asset.asset_role}
+ {asset.exists_flag ? '文件存在' : '文件缺失'}
+
+
{asset.asset_name}
+
{asset.absolute_path}
- )}
+ ))}
- ))
- )}
+ )}
+
+
+
+
问题列表 ({selectedIssues.length})
+ {selectedIssues.length === 0 ? (
+
当前没有登记问题。
+ ) : (
+
+ {selectedIssues.map((issue) => (
+
+
+ {issue.issue_code}
+ {issue.severity}
+
+
{issue.message}
+ {issue.repair_action && (
+
+ 建议修复动作:{issue.repair_action}
+
+ )}
+
+ ))}
+
+ )}
+
)}
-
+
);
diff --git a/frontend/src/components/DinsarCatalogPanel.rewrite.jsx b/frontend/src/components/DinsarCatalogPanel.rewrite.jsx
deleted file mode 100644
index 707a83b..0000000
--- a/frontend/src/components/DinsarCatalogPanel.rewrite.jsx
+++ /dev/null
@@ -1,581 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-
-import apiClient from '../api/client';
-import {
- getDinsarCatalogStatus,
- getDinsarProductDetail,
- listDinsarProducts,
- queueDinsarCatalogRebuild,
- queueDinsarProductPublish,
-} from '../api/dinsarProducts';
-import {
- DINSAR_ENGINE_ALL,
- buildDinsarEngineOptions,
- getDinsarEngineMeta,
-} from '../utils/dinsarEngines';
-
-const STATUS_TONE_MAP = {
- READY: 'ready',
- PARTIAL: 'warn',
- QUARANTINED: 'error',
- WARN: 'warn',
- ERROR: 'error',
- REBUILDING: 'info',
-};
-
-function formatDateTime(value) {
- if (!value) return '-';
- try {
- return new Date(value).toLocaleString();
- } catch {
- return String(value);
- }
-}
-
-function parseDirectoryList(value) {
- return [...new Set(
- String(value || '')
- .split(/[\r\n,;]+/)
- .map((item) => item.trim())
- .filter(Boolean)
- )];
-}
-
-function getMessageTone(message) {
- return /失败|error|Error|ERROR/.test(String(message || '')) ? 'error' : 'success';
-}
-
-function StatusPill({ label, tone = 'neutral' }) {
- return
{label} ;
-}
-
-function MetaField({ label, value, multiline = false }) {
- const displayValue = value === null || value === undefined || value === '' ? '-' : value;
- return (
-
- {label}
- {displayValue}
-
- );
-}
-
-export default function DinsarCatalogPanel({
- readOnly = false,
- compact = false,
- initialSourceDir = '',
- onTaskQueued,
-}) {
- const [catalogStatus, setCatalogStatus] = useState(null);
- const [products, setProducts] = useState([]);
- const [selectedProductId, setSelectedProductId] = useState(null);
- const [selectedProduct, setSelectedProduct] = useState(null);
- const [loading, setLoading] = useState(false);
- const [detailLoading, setDetailLoading] = useState(false);
- const [actionLoading, setActionLoading] = useState(false);
- const [actionMessage, setActionMessage] = useState('');
- const [sourceDirectoriesText, setSourceDirectoriesText] = useState(initialSourceDir || '');
- const [publishRoot, setPublishRoot] = useState('');
- const [engineFilter, setEngineFilter] = useState(DINSAR_ENGINE_ALL);
- const [queryDraft, setQueryDraft] = useState('');
- const [queryApplied, setQueryApplied] = useState('');
-
- const listLimit = compact ? 8 : 24;
- const previewBaseUrl = apiClient.defaults.baseURL || '/api';
-
- useEffect(() => {
- if (!initialSourceDir) return;
- setSourceDirectoriesText((current) => (current.trim() ? current : initialSourceDir));
- }, [initialSourceDir]);
-
- const sourceDirectories = useMemo(
- () => parseDirectoryList(sourceDirectoriesText),
- [sourceDirectoriesText]
- );
-
- const engineOptions = useMemo(
- () => buildDinsarEngineOptions(products, { includeKnown: true }),
- [products]
- );
- const selectedEngineMeta = useMemo(
- () => (engineFilter === DINSAR_ENGINE_ALL ? null : getDinsarEngineMeta(engineFilter)),
- [engineFilter]
- );
-
- useEffect(() => {
- if (engineFilter === DINSAR_ENGINE_ALL) return;
- if (!engineOptions.some((option) => option.value === engineFilter)) {
- setEngineFilter(DINSAR_ENGINE_ALL);
- }
- }, [engineFilter, engineOptions]);
-
- const loadCatalog = useCallback(async () => {
- setLoading(true);
- try {
- const [statusData, productData] = await Promise.all([
- getDinsarCatalogStatus(),
- listDinsarProducts({
- limit: listLimit,
- offset: 0,
- engine_code: engineFilter === DINSAR_ENGINE_ALL ? undefined : engineFilter,
- query: queryApplied || undefined,
- }),
- ]);
- setCatalogStatus(statusData);
- const nextItems = Array.isArray(productData?.items) ? productData.items : [];
- setProducts(nextItems);
- setSelectedProductId((current) => {
- if (current && nextItems.some((item) => item.id === current)) {
- return current;
- }
- return nextItems[0]?.id ?? null;
- });
- } catch (error) {
- setActionMessage(`结果目录状态加载失败:${error?.response?.data?.detail || error.message}`);
- setCatalogStatus(null);
- setProducts([]);
- setSelectedProductId(null);
- } finally {
- setLoading(false);
- }
- }, [engineFilter, listLimit, queryApplied]);
-
- const loadProductDetail = useCallback(async (productId) => {
- if (!productId) {
- setSelectedProduct(null);
- return;
- }
- setSelectedProduct(null);
- setDetailLoading(true);
- try {
- const detail = await getDinsarProductDetail(productId);
- setSelectedProduct(detail);
- } catch (error) {
- setSelectedProduct({
- error: error?.response?.data?.detail || error.message || '结果详情加载失败',
- });
- } finally {
- setDetailLoading(false);
- }
- }, []);
-
- useEffect(() => {
- loadCatalog();
- }, [loadCatalog]);
-
- useEffect(() => {
- loadProductDetail(selectedProductId);
- }, [loadProductDetail, selectedProductId]);
-
- const handleApplyFilters = useCallback(() => {
- setQueryApplied(queryDraft.trim());
- }, [queryDraft]);
-
- const handleResetFilters = useCallback(() => {
- setEngineFilter(DINSAR_ENGINE_ALL);
- setQueryDraft('');
- setQueryApplied('');
- }, []);
-
- const handleQueuePublish = async () => {
- if (readOnly || sourceDirectories.length === 0) return;
- setActionLoading(true);
- setActionMessage('');
- try {
- const result = await queueDinsarProductPublish({
- source_directories: sourceDirectories,
- publish_root: publishRoot.trim() || null,
- rebuild_catalog: true,
- });
- setActionMessage(`结果包发布任务已入队:${result.task_id}`);
- onTaskQueued?.(result.task_id);
- await loadCatalog();
- } catch (error) {
- setActionMessage(`结果包发布失败:${error?.response?.data?.detail || error.message}`);
- } finally {
- setActionLoading(false);
- }
- };
-
- const handleQueueRebuild = async () => {
- if (readOnly) return;
- setActionLoading(true);
- setActionMessage('');
- try {
- const result = await queueDinsarCatalogRebuild({
- publish_root: publishRoot.trim() || null,
- full_rebuild: true,
- });
- setActionMessage(`结果目录重建任务已入队:${result.task_id}`);
- onTaskQueued?.(result.task_id);
- await loadCatalog();
- } catch (error) {
- setActionMessage(`结果目录重建失败:${error?.response?.data?.detail || error.message}`);
- } finally {
- setActionLoading(false);
- }
- };
-
- const catalogTone = STATUS_TONE_MAP[catalogStatus?.status] || 'neutral';
- const actionTone = getMessageTone(actionMessage);
- const selectedIssues = Array.isArray(selectedProduct?.issues) ? selectedProduct.issues : [];
- const selectedAssets = Array.isArray(selectedProduct?.assets) ? selectedProduct.assets : [];
- const selectedPairingTrace = selectedProduct?.pairing_trace || null;
- const selectedPairingNetwork = selectedProduct?.pairing_network || null;
- const selectedPairingRun = selectedPairingNetwork?.run || null;
- const selectedPairingEdge = selectedPairingNetwork?.edge || null;
- const selectedPairingMetric = selectedPairingNetwork?.metric || null;
- const selectedProductEngine = getDinsarEngineMeta(selectedProduct?.engine_code);
- const selectedStatusTone = STATUS_TONE_MAP[selectedProduct?.status] || 'neutral';
-
- return (
-
-
-
-
{compact ? '结果目录状态' : '标准结果包目录'}
-
- {compact
- ? '查看结果包目录与数据库索引是否一致。'
- : '统一结果目录按 engine + pair + run 管理,便于同一对影像保留多套生产结果并行对比。'}
-
-
-
- {selectedEngineMeta && (
-
- {selectedEngineMeta.shortLabel}
-
- )}
-
- {loading ? '刷新中...' : '刷新'}
-
-
-
-
-
-
- 目录状态
- {catalogStatus?.status || '未知'}
-
-
-
- 需要重建
- {catalogStatus?.needs_rebuild ? '是' : '否'}
- {catalogStatus?.needs_rebuild ? 'Manifest 与数据库存在漂移' : '目录登记正常'}
-
-
- Manifest / 数据库
- {catalogStatus?.manifest_count ?? 0} / {catalogStatus?.db_count ?? 0}
- 已登记结果包总量
-
-
- 问题数量
- {catalogStatus?.issue_count ?? 0}
- 含缺失文件与健康异常
-
-
-
-
-
结果包根目录: {catalogStatus?.storage_root || '-'}
-
最近消息: {catalogStatus?.last_message || '-'}
-
最近全量重建: {formatDateTime(catalogStatus?.last_full_rebuild_at)}
-
-
- {actionMessage && (
-
- {actionMessage}
-
- )}
-
- {!compact && (
-
-
-
手动发布与目录重建
-
- 这里用于把既有结果目录重新发布为标准结果包,并按最新规则重建目录索引。
- 如果同一对影像存在 ENVI 与 ISCE2 两套结果,它们会依赖 `engine_code` 与 `run_key` 分别登记,不会互相覆盖。
-
-
-
-
- )}
-
-
-
-
-
- 结果包列表
-
- {loading ? '加载中...' : `当前展示 ${products.length} 条`}
-
-
- {queryApplied &&
}
-
-
-
-
- 生产引擎
- setEngineFilter(event.target.value)}>
- 全部引擎
- {engineOptions.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
- 检索
- setQueryDraft(event.target.value)}
- onKeyDown={(event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- handleApplyFilters();
- }
- }}
- placeholder="搜索任务名 / pair / run / 引擎"
- />
-
-
-
- 查询
- 重置
-
-
-
- {products.length === 0 ? (
-
- {loading ? '正在加载结果包...' : '当前筛选条件下没有结果包。'}
-
- ) : (
-
- {products.map((item) => {
- const tone = STATUS_TONE_MAP[item.status] || 'neutral';
- const engineMeta = getDinsarEngineMeta(item.engine_code);
- return (
-
setSelectedProductId(item.id)}
- >
-
- {item.display_name || item.product_id}
-
-
-
- {engineMeta.shortLabel}
- {formatDateTime(item.published_at)}
-
-
- {(item.task_alias || item.task_name || '-')}{item.run_key ? ` / ${item.run_key}` : ''}
-
-
- {item.pair_key || '-'}
-
- {(item.selection_strategy || item.network_run_id || item.network_edge_id != null) && (
-
- {(item.selection_strategy || 'trace')}
- {item.network_edge_id != null ? ` / edge ${item.network_edge_id}` : ''}
- {item.network_run_id ? ` / ${item.network_run_id}` : ''}
-
- )}
-
- );
- })}
-
- )}
-
-
-
-
-
- 结果包详情
- 查看选中结果的发布信息、配对溯源与资产健康
-
-
-
- {!selectedProductId ? (
- 请选择一个结果包查看详情。
- ) : detailLoading || !selectedProduct ? (
- 正在加载详情...
- ) : selectedProduct?.error ? (
- {selectedProduct.error}
- ) : (
-
-
-
-
-
-
-
-
-
-
{selectedProduct.display_name || selectedProduct.product_id}
-
{selectedProduct.task_alias || selectedProduct.task_name || '未命名任务'}
-
-
-
- {selectedProductEngine.shortLabel}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
配对追踪
- {!selectedPairingTrace?.network_run_id ? (
-
当前结果未携带完整的配对网络追踪信息。
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
资产列表 ({selectedAssets.length})
- {selectedAssets.length === 0 ? (
-
暂无资产记录。
- ) : (
-
- {selectedAssets.map((asset) => (
-
-
- {asset.asset_role}
- {asset.exists_flag ? '文件存在' : '文件缺失'}
-
-
{asset.asset_name}
-
{asset.absolute_path}
-
- ))}
-
- )}
-
-
-
-
问题列表 ({selectedIssues.length})
- {selectedIssues.length === 0 ? (
-
当前没有登记问题。
- ) : (
-
- {selectedIssues.map((issue) => (
-
-
- {issue.issue_code}
- {issue.severity}
-
-
{issue.message}
- {issue.repair_action && (
-
- 建议修复动作:{issue.repair_action}
-
- )}
-
- ))}
-
- )}
-
-
-
- )}
-
-
-
- );
-}
diff --git a/frontend/src/components/ResultExportModal.jsx b/frontend/src/components/ResultExportModal.jsx
index 4fdd756..b94c4fe 100644
--- a/frontend/src/components/ResultExportModal.jsx
+++ b/frontend/src/components/ResultExportModal.jsx
@@ -1,5 +1,6 @@
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { exportDinsarResults } from '../api/dinsar';
+import { getDinsarEngineMeta } from '../utils/dinsarEngines';
const EXAMPLE_TARGET_DIR = String.raw`例如: D:\Export\Results 或 \\server\share\results`;
@@ -10,6 +11,9 @@ export default function ResultExportModal({ results = [], onClose }) {
const [exportResult, setExportResult] = useState(null);
const [error, setError] = useState('');
+ const selectedCount = selectedIds.size;
+ const sortedResults = useMemo(() => [...results], [results]);
+
const toggleSelect = (id) => {
setSelectedIds((prev) => {
const next = new Set(prev);
@@ -86,7 +90,7 @@ export default function ResultExportModal({ results = [], onClose }) {
onChange={toggleAll}
disabled={exporting || results.length === 0}
/>
- 全选 ({selectedIds.size}/{results.length})
+ 全选 ({selectedCount}/{results.length})
@@ -95,21 +99,27 @@ export default function ResultExportModal({ results = [], onClose }) {
diff --git a/frontend/src/components/ResultExportModal.rewrite.jsx b/frontend/src/components/ResultExportModal.rewrite.jsx
deleted file mode 100644
index b94c4fe..0000000
--- a/frontend/src/components/ResultExportModal.rewrite.jsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import { useMemo, useState } from 'react';
-import { exportDinsarResults } from '../api/dinsar';
-import { getDinsarEngineMeta } from '../utils/dinsarEngines';
-
-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((result) => result.id)));
- const [exporting, setExporting] = useState(false);
- const [exportResult, setExportResult] = useState(null);
- const [error, setError] = useState('');
-
- const selectedCount = selectedIds.size;
- const sortedResults = useMemo(() => [...results], [results]);
-
- const toggleSelect = (id) => {
- setSelectedIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) {
- next.delete(id);
- } else {
- next.add(id);
- }
- return next;
- });
- };
-
- const toggleAll = () => {
- if (selectedIds.size === results.length) {
- setSelectedIds(new Set());
- return;
- }
- setSelectedIds(new Set(results.map((result) => result.id)));
- };
-
- const handleExport = async () => {
- const dir = targetDir.trim();
- if (!dir) {
- setError('请输入目标路径。');
- return;
- }
- if (selectedIds.size === 0) {
- setError('请至少选择一个结果。');
- return;
- }
-
- setError('');
- setExporting(true);
- setExportResult(null);
- try {
- const response = await exportDinsarResults([...selectedIds], dir);
- setExportResult(response);
- } catch (eventualError) {
- setError(eventualError.response?.data?.detail || eventualError.message || '提取失败。');
- } finally {
- setExporting(false);
- }
- };
-
- return (
-
-
event.stopPropagation()}>
-
-
提取 D-InSAR 结果
-
- ×
-
-
-
-
-
- 目标路径,支持本地盘符或 UNC 路径,例如 `D:\Export\Results`。
- setTargetDir(event.target.value)}
- placeholder={EXAMPLE_TARGET_DIR}
- disabled={exporting}
- className="export-path-input"
- />
-
-
-
-
-
- 0}
- onChange={toggleAll}
- disabled={exporting || results.length === 0}
- />
- 全选 ({selectedCount}/{results.length})
-
-
-
-
- 导出时会优先按任务名创建子目录;如果同名结果已存在且内容不同,会自动追加后缀避免覆盖。
-
-
-
-
-
- {error &&
{error}
}
-
- {exportResult && (
-
-
提取完成
-
- 复制: {exportResult.copied}
- 跳过: {exportResult.skipped}
- {exportResult.failed > 0 && (
- 失败: {exportResult.failed}
- )}
-
-
- 目标目录: {exportResult.target_dir}
-
-
- )}
-
-
-
-
- 关闭
-
-
- {exporting ? '提取中...' : `确定提取 ${selectedIds.size} 个结果`}
-
-
-
-
- );
-}
diff --git a/frontend/src/components/app/AppSidePanel.jsx b/frontend/src/components/app/AppSidePanel.jsx
index 53d1333..a52ccca 100644
--- a/frontend/src/components/app/AppSidePanel.jsx
+++ b/frontend/src/components/app/AppSidePanel.jsx
@@ -24,7 +24,7 @@ const LazyAuditLogPanel = lazy(() => import('../../AuditLogPanel'));
const LazyAiQualityPanel = lazy(() => import('../../panels/AiQualityPanel'));
const LazyAiAnalysisPanel = lazy(() => import('../../AiAnalysisPanel'));
const LazyPairingPanel = lazy(() => import('../../panels/PairPlanningPanel'));
-const LazyDinsarResultPanel = lazy(() => import('../../panels/DinsarResultPanel.rewrite'));
+const LazyDinsarResultPanel = lazy(() => import('../../panels/DinsarResultPanel'));
const LazyBatchPanel = lazy(() => import('../../panels/BatchPanel'));
const LazyPairsListPanel = lazy(() => import('../../panels/PairsListPanel'));
const LazyPsResultsPanel = lazy(() => import('../../panels/PsResultsPanel'));
diff --git a/frontend/src/components/panels/DinsarResultRow.jsx b/frontend/src/components/panels/DinsarResultRow.jsx
index 3a459ac..17bc893 100644
--- a/frontend/src/components/panels/DinsarResultRow.jsx
+++ b/frontend/src/components/panels/DinsarResultRow.jsx
@@ -1,7 +1,8 @@
import { memo } from 'react';
import { parseDatesFromName, formatYmd } from '../../utils/appUiHelpers';
+import { getDinsarEngineMeta } from '../../utils/dinsarEngines';
-function truncateMiddle(value, maxLength = 26) {
+function truncateMiddle(value, maxLength = 28) {
const text = String(value || '').trim();
if (!text || text.length <= maxLength) {
return text || '-';
@@ -21,49 +22,104 @@ function DinsarResultRow({
onToggleVisibility,
}) {
const dates = showDates ? parseDatesFromName(result.name, (value) => formatYmd(value, language)) : null;
- const hasTrace = !!(result.selection_strategy || result.network_run_id || result.network_edge_id || result.pair_uid);
+ const engineMeta = getDinsarEngineMeta(result.engine_code);
+ const hasTrace = !!(
+ result.selection_strategy ||
+ result.network_run_id ||
+ result.network_edge_id ||
+ result.pair_uid ||
+ result.pair_key ||
+ result.run_key
+ );
return (
-
+
{result.name}
- {result.ai_score !== null && (
+
0.7 ? 'good' : (result.ai_score < 0.4 ? 'bad' : 'medium')}`}
- title={language === 'en' ? 'AI quality score' : 'AI 质量评分'}
+ className={`dinsar-engine-badge tone-${engineMeta.tone}`}
+ title={`${language === 'en' ? 'Engine' : '生产引擎'}: ${engineMeta.label}`}
>
- AI: {(result.ai_score * 100).toFixed(0)}
+ {engineMeta.shortLabel}
- )}
+ {result.ai_score !== null && (
+ 0.7 ? 'good' : (result.ai_score < 0.4 ? 'bad' : 'medium')}`}
+ title={language === 'en' ? 'AI quality score' : 'AI 质量评分'}
+ >
+ AI {Math.round(result.ai_score * 100)}
+
+ )}
+
{hasTrace && (
-
+
{result.selection_strategy || 'legacy'}
-
- edge {result.network_edge_id ?? '-'}
-
-
- {truncateMiddle(result.network_run_id, 24)}
-
+ {result.run_key && (
+
+ {language === 'en' ? 'run' : '运行'}
+ {truncateMiddle(result.run_key, 24)}
+
+ )}
+ {result.network_edge_id != null && (
+
+ edge
+ {result.network_edge_id}
+
+ )}
+ {result.network_run_id && (
+
+ {language === 'en' ? 'network' : '网络'}
+ {truncateMiddle(result.network_run_id, 22)}
+
+ )}
-
-
{truncateMiddle(result.pair_uid || result.pair_key, 30)}
- {result.policy_version &&
{result.policy_version} }
+
+
+ {language === 'en' ? 'pair' : '配对'}
+ {truncateMiddle(result.pair_uid || result.pair_key, 36)}
+
+ {result.policy_version && (
+
+ {language === 'en' ? 'policy' : '策略版本'}
+ {result.policy_version}
+
+ )}
)}
{dates && (
- {dates.master}
- →
- {dates.slave}
+
+ {dates.master}
+
+ ->
+
+ {dates.slave}
+
)}
@@ -83,29 +139,34 @@ function DinsarResultRow({
title={language === 'en' ? 'Mark as low quality' : '标记为低质量'}
disabled={isReadOnlyUser}
>
- {language === 'en' ? 'Poor' : '欠佳'}
+ {language === 'en' ? 'Poor' : '较差'}
-
{
- event.stopPropagation();
- onAnalyze(result.id);
- }}
- disabled={isLoading || isReadOnlyUser}
- title={language === 'en' ? 'Use AI to analyze this result' : '使用 AI 自动分析此结果'}
- >
- {language === 'en' ? 'AI Diagnose' : 'AI 诊断'}
-
-
{
- event.stopPropagation();
- onToggleVisibility(result.id);
- }}
- title={language === 'en' ? 'Show or hide on map' : '在地图上显示/隐藏'}
- />
+
+
+ {
+ event.stopPropagation();
+ onAnalyze(result.id);
+ }}
+ disabled={isLoading || isReadOnlyUser}
+ title={language === 'en' ? 'Use AI to analyze this result' : '使用 AI 分析该结果'}
+ >
+ {language === 'en' ? 'AI Diagnose' : 'AI 诊断'}
+
+
+ {
+ event.stopPropagation();
+ onToggleVisibility(result.id);
+ }}
+ />
+ {language === 'en' ? 'Map' : '地图'}
+
+
);
diff --git a/frontend/src/components/panels/DinsarResultRow.rewrite.jsx b/frontend/src/components/panels/DinsarResultRow.rewrite.jsx
deleted file mode 100644
index 17bc893..0000000
--- a/frontend/src/components/panels/DinsarResultRow.rewrite.jsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import { memo } from 'react';
-import { parseDatesFromName, formatYmd } from '../../utils/appUiHelpers';
-import { getDinsarEngineMeta } from '../../utils/dinsarEngines';
-
-function truncateMiddle(value, maxLength = 28) {
- const text = String(value || '').trim();
- if (!text || text.length <= maxLength) {
- return text || '-';
- }
- const sideLength = Math.max(6, Math.floor((maxLength - 3) / 2));
- return `${text.slice(0, sideLength)}...${text.slice(-sideLength)}`;
-}
-
-function DinsarResultRow({
- result,
- language,
- showDates,
- isLoading,
- isReadOnlyUser,
- onLabel,
- onAnalyze,
- onToggleVisibility,
-}) {
- const dates = showDates ? parseDatesFromName(result.name, (value) => formatYmd(value, language)) : null;
- const engineMeta = getDinsarEngineMeta(result.engine_code);
- const hasTrace = !!(
- result.selection_strategy ||
- result.network_run_id ||
- result.network_edge_id ||
- result.pair_uid ||
- result.pair_key ||
- result.run_key
- );
-
- return (
-
-
-
- {result.name}
-
-
-
- {engineMeta.shortLabel}
-
- {result.ai_score !== null && (
- 0.7 ? 'good' : (result.ai_score < 0.4 ? 'bad' : 'medium')}`}
- title={language === 'en' ? 'AI quality score' : 'AI 质量评分'}
- >
- AI {Math.round(result.ai_score * 100)}
-
- )}
-
-
-
- {hasTrace && (
-
-
-
- {result.selection_strategy || 'legacy'}
-
- {result.run_key && (
-
- {language === 'en' ? 'run' : '运行'}
- {truncateMiddle(result.run_key, 24)}
-
- )}
- {result.network_edge_id != null && (
-
- edge
- {result.network_edge_id}
-
- )}
- {result.network_run_id && (
-
- {language === 'en' ? 'network' : '网络'}
- {truncateMiddle(result.network_run_id, 22)}
-
- )}
-
-
-
- {language === 'en' ? 'pair' : '配对'}
- {truncateMiddle(result.pair_uid || result.pair_key, 36)}
-
- {result.policy_version && (
-
- {language === 'en' ? 'policy' : '策略版本'}
- {result.policy_version}
-
- )}
-
-
- )}
-
- {dates && (
-
-
- {dates.master}
-
- ->
-
- {dates.slave}
-
-
- )}
-
-
-
- onLabel(result.id, result.user_label === 1 ? null : 1)}
- title={language === 'en' ? 'Mark as high quality' : '标记为高质量'}
- disabled={isReadOnlyUser}
- >
- {language === 'en' ? 'Good' : '良好'}
-
- onLabel(result.id, result.user_label === 0 ? null : 0)}
- title={language === 'en' ? 'Mark as low quality' : '标记为低质量'}
- disabled={isReadOnlyUser}
- >
- {language === 'en' ? 'Poor' : '较差'}
-
-
-
-
- {
- event.stopPropagation();
- onAnalyze(result.id);
- }}
- disabled={isLoading || isReadOnlyUser}
- title={language === 'en' ? 'Use AI to analyze this result' : '使用 AI 分析该结果'}
- >
- {language === 'en' ? 'AI Diagnose' : 'AI 诊断'}
-
-
- {
- event.stopPropagation();
- onToggleVisibility(result.id);
- }}
- />
- {language === 'en' ? 'Map' : '地图'}
-
-
-
-
- );
-}
-
-export default memo(DinsarResultRow);
diff --git a/frontend/src/panels/DinsarResultPanel.jsx b/frontend/src/panels/DinsarResultPanel.jsx
index 1e51e89..1b051ad 100644
--- a/frontend/src/panels/DinsarResultPanel.jsx
+++ b/frontend/src/panels/DinsarResultPanel.jsx
@@ -158,7 +158,7 @@ export default function DinsarResultPanel({
) : (
<>
-
+
{focusedHazardPoint && (
@@ -168,7 +168,7 @@ export default function DinsarResultPanel({
>
) : (
<>
- 正在查看覆盖点 {focusedHazardPoint.hazard_name} 的结果
+ 当前仅显示覆盖隐患点 {focusedHazardPoint.hazard_name} 的结果
>
)}
@@ -178,144 +178,252 @@ export default function DinsarResultPanel({
)}
-
-
onSetAllVisibility(true)}>
- {language === 'en' ? 'Show All' : '显示全部'}
-
-
onSetAllVisibility(false)}>
- {language === 'en' ? 'Hide All' : '隐藏全部'}
-
-
setShowDates(!showDates)}>
- {showDates
- ? (language === 'en' ? 'Hide Dates' : '隐藏日期')
- : (language === 'en' ? 'Show Dates' : '显示日期')}
-
-
setShowExportModal(true)}
- disabled={isLoading || dinsarResults.length === 0}
- title={language === 'en' ? 'Export selected results to a directory' : '将结果文件提取到指定目录'}
- >
- {language === 'en' ? 'Export...' : '导出...'}
-
+
+
+
+ {language === 'en' ? 'Current page' : '当前页'}
+
+
+ {filteredResults.length} / {dinsarResults.length}
+
+
+ {language === 'en'
+ ? 'Results after local filtering on the current page'
+ : '当前页本地筛选后的结果数量'}
+
+
+
+ {language === 'en' ? 'AI score' : 'AI 分数'}
+ {' >= '}
+ {scorePercent}
+
+
+ {language === 'en' ? 'Dates' : '日期'}
+ {showDates
+ ? (language === 'en' ? ': visible' : ':已展开')
+ : (language === 'en' ? ': hidden' : ':已收起')}
+
+
+
+
+
+
+ {language === 'en' ? 'Engine focus' : '当前引擎'}
+
+
+ {filteredEngineMeta
+ ? filteredEngineMeta.shortLabel
+ : (language === 'en' ? 'All' : '全部')}
+
+
+ {filteredEngineMeta
+ ? filteredEngineMeta.label
+ : (language === 'en'
+ ? 'Compare outputs from all registered engines'
+ : '同时查看所有登记引擎的结果')}
+
+
+
+ {language === 'en' ? 'Strategy' : '策略'}:
+ {' '}
+ {strategyFilter === DINSAR_STRATEGY_ALL
+ ? (language === 'en' ? 'All' : '全部')
+ : strategyFilter}
+
+
+ {language === 'en' ? 'Trace search' : '检索词'}:
+ {' '}
+ {traceSearch.trim() || (language === 'en' ? 'None' : '未设置')}
+
+
+
+
+
+
+ {language === 'en' ? 'Page control' : '分页控制'}
+
+ {pageSummaryText}
+
+ {language === 'en'
+ ? 'Use page size and jump controls below for large catalogs'
+ : '大规模结果集请结合页大小和跳页控制使用'}
+
+
+ onSetAllVisibility(true)}>
+ {language === 'en' ? 'Show All' : '全部显示'}
+
+ onSetAllVisibility(false)}>
+ {language === 'en' ? 'Hide All' : '全部隐藏'}
+
+ setShowDates(!showDates)}>
+ {showDates
+ ? (language === 'en' ? 'Hide Dates' : '收起日期')
+ : (language === 'en' ? 'Show Dates' : '显示日期')}
+
+ setShowExportModal(true)}
+ disabled={isLoading || filteredResults.length === 0}
+ title={language === 'en'
+ ? 'Export visible results in the current filter scope'
+ : '按当前筛选范围导出结果文件'}
+ >
+ {language === 'en' ? 'Export...' : '提取结果...'}
+
+
+
-
-
- {language === 'en' ? 'AI Score Filter' : 'AI 评分过滤'}: {(scoreFilter * 100).toFixed(0)}
+
-
- {language === 'en' ? 'Pairing strategy' : '配对策略'}:
- setStrategyFilter(event.target.value)}
- disabled={isLoading}
- >
-
- {language === 'en' ? 'All strategies' : '全部策略'}
-
- {strategyOptions
- .filter((value) => value !== DINSAR_STRATEGY_ALL)
- .map((value) => (
- {value}
- ))}
-
- setTraceSearch(event.target.value)}
- placeholder={language === 'en' ? 'Search pair/run trace' : '搜索 pair/run trace'}
- disabled={isLoading}
- style={{ minWidth: '180px', flex: 1 }}
- />
-
-
-
-
- {language === 'en'
- ? `Filtered ${filteredResults.length} / ${dinsarResults.length} results`
- : `当前筛出 ${filteredResults.length} / ${dinsarResults.length} 条结果`}
-
-
-
-
- onPageChange(-1)} disabled={isLoading || dinsarPagination.offset <= 0}>
- {language === 'en' ? 'Previous' : '上一页'}
-
-
- {language === 'en'
- ? `Page ${dinsarCurrentPage}/${dinsarTotalPages} (Total ${dinsarPagination.total} items)`
- : `第 ${dinsarCurrentPage}/${dinsarTotalPages} 页(共 ${dinsarPagination.total} 条)`}
-
- onPageChange(1)} disabled={isLoading || !dinsarPagination.hasMore}>
- {language === 'en' ? 'Next' : '下一页'}
-
-
-
-
-
- {language === 'en' ? 'Per page' : '每页'}
+
+ {language === 'en' ? 'Pairing strategy' : '配对策略'}
setStrategyFilter(event.target.value)}
disabled={isLoading}
- style={{ marginLeft: '6px', marginRight: '6px' }}
>
- {PAGE_SIZE_OPTIONS.map((size) => (
- {size}
+
+ {language === 'en' ? 'All strategies' : '全部策略'}
+
+ {strategyOptions
+ .filter((value) => value !== DINSAR_STRATEGY_ALL)
+ .map((value) => (
+ {value}
+ ))}
+
+
+
+
+ {language === 'en' ? 'Production engine' : '生产引擎'}
+ setEngineFilter(event.target.value)}
+ disabled={isLoading}
+ >
+ {engineFilterOptions.map((option) => (
+
+ {option.label}
+
))}
- {language === 'en' ? 'items' : '条'}
-
- {language === 'en' ? 'Go to' : '跳到'}
+
+
+ {language === 'en' ? 'Trace search' : 'Trace 检索'}
{
- setDinsarPageInput(event.target.value);
- setDinsarPageInputTouched(false);
- }}
- onBlur={() => setDinsarPageInputTouched(true)}
- onKeyDown={(event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- onGoToPage();
- }
- }}
+ type="text"
+ value={traceSearch}
+ onChange={(event) => setTraceSearch(event.target.value)}
+ placeholder={language === 'en'
+ ? 'Search pair / run / policy / engine'
+ : '搜索 pair / run / policy / engine'}
disabled={isLoading}
- style={{
- width: '70px',
- marginLeft: '6px',
- marginRight: '6px',
- borderColor: showDinsarPageInputError ? '#e53e3e' : undefined,
- boxShadow: showDinsarPageInputError ? '0 0 0 1px rgba(229,62,62,0.25)' : undefined,
- }}
/>
- {language === 'en' ? 'page' : '页'}
-
- {language === 'en' ? 'Jump' : '跳转'}
-
-
-
+
+ setEngineFilter(DINSAR_ENGINE_ALL)}
+ disabled={isLoading}
+ >
+ {language === 'en' ? 'All engines' : '全部引擎'}
+ {dinsarResults.length}
+
+ {engineCounts.map((option) => (
+ setEngineFilter(option.code)}
+ disabled={isLoading}
+ >
+ {option.shortLabel}
+ {option.count}
+
+ ))}
+
+
+
+
+
onPageChange(-1)}
+ disabled={isLoading || dinsarPagination.offset <= 0}
+ >
+ {language === 'en' ? 'Previous' : '上一页'}
+
+
onPageChange(1)}
+ disabled={isLoading || !dinsarPagination.hasMore}
+ >
+ {language === 'en' ? 'Next' : '下一页'}
+
+
+
+ {language === 'en' ? 'Per page' : '每页条数'}
+
+ {PAGE_SIZE_OPTIONS.map((size) => (
+ {size}
+ ))}
+
+
+
+
+ {language === 'en' ? 'Jump to page' : '跳转页码'}
+
+ {
+ setDinsarPageInput(event.target.value);
+ setDinsarPageInputTouched(false);
+ }}
+ onBlur={() => setDinsarPageInputTouched(true)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ onGoToPage();
+ }
+ }}
+ disabled={isLoading}
+ className={showDinsarPageInputError ? 'has-error' : ''}
+ />
+
+ {language === 'en' ? 'Jump' : '跳转'}
+
+
+
+
+
+
{showDinsarPageInputError
? dinsarPageInputValidationError
: getPageHintText(dinsarTotalPages, language)}
-
+
@@ -344,7 +452,7 @@ export default function DinsarResultPanel({
{showExportModal && (
setShowExportModal(false)}
/>
)}
diff --git a/frontend/src/panels/DinsarResultPanel.rewrite.jsx b/frontend/src/panels/DinsarResultPanel.rewrite.jsx
deleted file mode 100644
index edbb75c..0000000
--- a/frontend/src/panels/DinsarResultPanel.rewrite.jsx
+++ /dev/null
@@ -1,461 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { useShallow } from 'zustand/react/shallow';
-import { useDinsarStore, useHazardStore, useUiStore, useAuthStore } from '../store';
-import { useI18n } from '../i18n/I18nContext';
-import VirtualizedList from '../components/common/VirtualizedList';
-import DinsarResultRow from '../components/panels/DinsarResultRow.rewrite';
-import ResultExportModal from '../components/ResultExportModal.rewrite';
-import { PAGE_SIZE_OPTIONS } from '../config/appConstants';
-import { getPageHintText } from '../utils/appUiHelpers';
-import {
- DINSAR_ENGINE_ALL,
- buildDinsarEngineOptions,
- getDinsarEngineMeta,
-} from '../utils/dinsarEngines';
-import {
- DINSAR_STRATEGY_ALL,
- buildDinsarStrategyOptions,
- filterDinsarResults,
-} from '../utils/dinsarResultFilters';
-
-const DINSAR_ROW_HEIGHT = {
- compact: 136,
- expanded: 166,
-};
-
-export default function DinsarResultPanel({
- dinsarCurrentPage,
- dinsarTotalPages,
- showDinsarPageInputError,
- dinsarPageInputValidationError,
- onSetAllVisibility,
- onScoreFilterChange,
- onPageChange,
- onPageSizeChange,
- onGoToPage,
- onToggleVisibility,
- onLabel,
- onAnalyze,
-}) {
- const { language } = useI18n();
- const {
- dinsarResults,
- dinsarPagination,
- scoreFilter,
- engineFilter,
- traceSearch,
- strategyFilter,
- dinsarPageInput,
- setEngineFilter,
- setTraceSearch,
- setStrategyFilter,
- setDinsarPageInput,
- setDinsarPageInputTouched,
- } = useDinsarStore(useShallow((state) => ({
- dinsarResults: state.dinsarResults,
- dinsarPagination: state.dinsarPagination,
- scoreFilter: state.scoreFilter,
- engineFilter: state.engineFilter,
- traceSearch: state.traceSearch,
- strategyFilter: state.strategyFilter,
- dinsarPageInput: state.dinsarPageInput,
- setEngineFilter: state.setEngineFilter,
- setTraceSearch: state.setTraceSearch,
- setStrategyFilter: state.setStrategyFilter,
- setDinsarPageInput: state.setDinsarPageInput,
- setDinsarPageInputTouched: state.setDinsarPageInputTouched,
- })));
- const { focusedHazardPoint, setFocusedHazardPoint } = useHazardStore(useShallow((state) => ({
- focusedHazardPoint: state.focusedHazardPoint,
- setFocusedHazardPoint: state.setFocusedHazardPoint,
- })));
- const { isLoading, showDates, setShowDates } = useUiStore(useShallow((state) => ({
- isLoading: state.isLoading,
- showDates: state.showDates,
- setShowDates: state.setShowDates,
- })));
- const { currentUser } = useAuthStore();
- const isReadOnlyUser = !!currentUser && currentUser.role !== 'admin';
- const [showExportModal, setShowExportModal] = useState(false);
-
- const strategyOptions = useMemo(
- () => buildDinsarStrategyOptions(dinsarResults),
- [dinsarResults]
- );
- const engineOptions = useMemo(
- () => buildDinsarEngineOptions(dinsarResults),
- [dinsarResults]
- );
- const engineFilterOptions = useMemo(
- () => [
- {
- value: DINSAR_ENGINE_ALL,
- label: language === 'en' ? 'All engines' : '全部引擎',
- },
- ...engineOptions.map((option) => ({
- value: option.value,
- label: option.label,
- })),
- ],
- [engineOptions, language]
- );
- const filteredEngineMeta = useMemo(
- () => (engineFilter === DINSAR_ENGINE_ALL ? null : getDinsarEngineMeta(engineFilter)),
- [engineFilter]
- );
- const engineCounts = useMemo(() => {
- const counts = new Map();
- dinsarResults.forEach((result) => {
- const meta = getDinsarEngineMeta(result?.engine_code);
- counts.set(meta.code, (counts.get(meta.code) || 0) + 1);
- });
- return engineOptions.map((option) => ({
- ...option,
- count: counts.get(option.code) || 0,
- }));
- }, [dinsarResults, engineOptions]);
-
- useEffect(() => {
- if (strategyFilter === DINSAR_STRATEGY_ALL) {
- return;
- }
- if (!strategyOptions.includes(strategyFilter)) {
- setStrategyFilter(DINSAR_STRATEGY_ALL);
- }
- }, [setStrategyFilter, strategyFilter, strategyOptions]);
-
- useEffect(() => {
- if (engineFilter === DINSAR_ENGINE_ALL) {
- return;
- }
- if (!engineOptions.some((option) => option.value === engineFilter)) {
- setEngineFilter(DINSAR_ENGINE_ALL);
- }
- }, [engineFilter, engineOptions, setEngineFilter]);
-
- const filteredResults = useMemo(
- () => filterDinsarResults(dinsarResults, {
- scoreFilter,
- engineFilter,
- strategyFilter,
- traceSearch,
- focusedHazardPoint,
- }),
- [dinsarResults, engineFilter, focusedHazardPoint, scoreFilter, strategyFilter, traceSearch]
- );
-
- const scorePercent = Math.round(Number(scoreFilter || 0) * 100);
- const virtualRowHeight = showDates ? DINSAR_ROW_HEIGHT.expanded : DINSAR_ROW_HEIGHT.compact;
- const pageSummaryText = language === 'en'
- ? `Page ${dinsarCurrentPage}/${dinsarTotalPages} · ${dinsarPagination.total} total`
- : `第 ${dinsarCurrentPage}/${dinsarTotalPages} 页 · 共 ${dinsarPagination.total} 条`;
-
- return (
-
- {dinsarPagination.total === 0 ? (
-
- {language === 'en' ? 'No D-InSAR results found.' : '未找到 D-InSAR 结果。'}
-
- ) : (
- <>
-
- {focusedHazardPoint && (
-
-
- {language === 'en' ? (
- <>
- Viewing results covering {focusedHazardPoint.hazard_name}
- >
- ) : (
- <>
- 当前仅显示覆盖隐患点 {focusedHazardPoint.hazard_name} 的结果
- >
- )}
-
- setFocusedHazardPoint(null)}>
- {language === 'en' ? 'Clear Filter' : '清除筛选'}
-
-
- )}
-
-
-
-
- {language === 'en' ? 'Current page' : '当前页'}
-
-
- {filteredResults.length} / {dinsarResults.length}
-
-
- {language === 'en'
- ? 'Results after local filtering on the current page'
- : '当前页本地筛选后的结果数量'}
-
-
-
- {language === 'en' ? 'AI score' : 'AI 分数'}
- {' >= '}
- {scorePercent}
-
-
- {language === 'en' ? 'Dates' : '日期'}
- {showDates
- ? (language === 'en' ? ': visible' : ':已展开')
- : (language === 'en' ? ': hidden' : ':已收起')}
-
-
-
-
-
-
- {language === 'en' ? 'Engine focus' : '当前引擎'}
-
-
- {filteredEngineMeta
- ? filteredEngineMeta.shortLabel
- : (language === 'en' ? 'All' : '全部')}
-
-
- {filteredEngineMeta
- ? filteredEngineMeta.label
- : (language === 'en'
- ? 'Compare outputs from all registered engines'
- : '同时查看所有登记引擎的结果')}
-
-
-
- {language === 'en' ? 'Strategy' : '策略'}:
- {' '}
- {strategyFilter === DINSAR_STRATEGY_ALL
- ? (language === 'en' ? 'All' : '全部')
- : strategyFilter}
-
-
- {language === 'en' ? 'Trace search' : '检索词'}:
- {' '}
- {traceSearch.trim() || (language === 'en' ? 'None' : '未设置')}
-
-
-
-
-
-
- {language === 'en' ? 'Page control' : '分页控制'}
-
- {pageSummaryText}
-
- {language === 'en'
- ? 'Use page size and jump controls below for large catalogs'
- : '大规模结果集请结合页大小和跳页控制使用'}
-
-
- onSetAllVisibility(true)}>
- {language === 'en' ? 'Show All' : '全部显示'}
-
- onSetAllVisibility(false)}>
- {language === 'en' ? 'Hide All' : '全部隐藏'}
-
- setShowDates(!showDates)}>
- {showDates
- ? (language === 'en' ? 'Hide Dates' : '收起日期')
- : (language === 'en' ? 'Show Dates' : '显示日期')}
-
- setShowExportModal(true)}
- disabled={isLoading || filteredResults.length === 0}
- title={language === 'en'
- ? 'Export visible results in the current filter scope'
- : '按当前筛选范围导出结果文件'}
- >
- {language === 'en' ? 'Export...' : '提取结果...'}
-
-
-
-
-
-
-
- {language === 'en' ? 'AI score floor' : 'AI 分数下限'}
-
-
- {scorePercent}
-
-
-
-
- {language === 'en' ? 'Pairing strategy' : '配对策略'}
- setStrategyFilter(event.target.value)}
- disabled={isLoading}
- >
-
- {language === 'en' ? 'All strategies' : '全部策略'}
-
- {strategyOptions
- .filter((value) => value !== DINSAR_STRATEGY_ALL)
- .map((value) => (
- {value}
- ))}
-
-
-
-
- {language === 'en' ? 'Production engine' : '生产引擎'}
- setEngineFilter(event.target.value)}
- disabled={isLoading}
- >
- {engineFilterOptions.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
- {language === 'en' ? 'Trace search' : 'Trace 检索'}
- setTraceSearch(event.target.value)}
- placeholder={language === 'en'
- ? 'Search pair / run / policy / engine'
- : '搜索 pair / run / policy / engine'}
- disabled={isLoading}
- />
-
-
-
-
- setEngineFilter(DINSAR_ENGINE_ALL)}
- disabled={isLoading}
- >
- {language === 'en' ? 'All engines' : '全部引擎'}
- {dinsarResults.length}
-
- {engineCounts.map((option) => (
- setEngineFilter(option.code)}
- disabled={isLoading}
- >
- {option.shortLabel}
- {option.count}
-
- ))}
-
-
-
-
-
onPageChange(-1)}
- disabled={isLoading || dinsarPagination.offset <= 0}
- >
- {language === 'en' ? 'Previous' : '上一页'}
-
-
onPageChange(1)}
- disabled={isLoading || !dinsarPagination.hasMore}
- >
- {language === 'en' ? 'Next' : '下一页'}
-
-
-
- {language === 'en' ? 'Per page' : '每页条数'}
-
- {PAGE_SIZE_OPTIONS.map((size) => (
- {size}
- ))}
-
-
-
-
- {language === 'en' ? 'Jump to page' : '跳转页码'}
-
- {
- setDinsarPageInput(event.target.value);
- setDinsarPageInputTouched(false);
- }}
- onBlur={() => setDinsarPageInputTouched(true)}
- onKeyDown={(event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- onGoToPage();
- }
- }}
- disabled={isLoading}
- className={showDinsarPageInputError ? 'has-error' : ''}
- />
-
- {language === 'en' ? 'Jump' : '跳转'}
-
-
-
-
-
-
- {showDinsarPageInputError
- ? dinsarPageInputValidationError
- : getPageHintText(dinsarTotalPages, language)}
-
-
-
-
-
- result.id}
- renderItem={(result, index, key) => (
-
- )}
- />
-
- >
- )}
-
- {showExportModal && (
-
setShowExportModal(false)}
- />
- )}
-
- );
-}