From 5a0623fd9b3cf9b143f75f5fc669b44b7e511a4d Mon Sep 17 00:00:00 2001 From: Harmon Date: Mon, 27 Apr 2026 08:05:10 +0800 Subject: [PATCH] refactor(frontend): canonicalize active panel files - replace rewrite/clean transitional panel files with canonical filenames - update imports and navigation architecture notes to match runtime truth - ignore local .codex_tmp workspace artifacts --- .gitignore | 1 + INIT.md | 29 +- docs/FRONTEND_NAVIGATION_ARCHITECTURE.md | 195 +++-- frontend/src/DinsarProductsPanel.jsx | 449 ++++++------ frontend/src/DinsarProductsPanel.rewrite.jsx | 405 ---------- frontend/src/HealthCheckPanel.jsx | 2 +- frontend/src/LogManagementPanel.clean.jsx | 372 ---------- frontend/src/LogManagementPanel.jsx | 216 +++--- frontend/src/ProductionWorkspace.jsx | 2 +- .../src/components/DinsarCatalogPanel.jsx | 689 +++++++++--------- .../components/DinsarCatalogPanel.rewrite.jsx | 581 --------------- frontend/src/components/ResultExportModal.jsx | 44 +- .../components/ResultExportModal.rewrite.jsx | 161 ---- frontend/src/components/app/AppSidePanel.jsx | 2 +- .../src/components/panels/DinsarResultRow.jsx | 145 ++-- .../panels/DinsarResultRow.rewrite.jsx | 175 ----- frontend/src/panels/DinsarResultPanel.jsx | 352 +++++---- .../src/panels/DinsarResultPanel.rewrite.jsx | 461 ------------ 18 files changed, 1176 insertions(+), 3105 deletions(-) delete mode 100644 frontend/src/DinsarProductsPanel.rewrite.jsx delete mode 100644 frontend/src/LogManagementPanel.clean.jsx delete mode 100644 frontend/src/components/DinsarCatalogPanel.rewrite.jsx delete mode 100644 frontend/src/components/ResultExportModal.rewrite.jsx delete mode 100644 frontend/src/components/panels/DinsarResultRow.rewrite.jsx delete mode 100644 frontend/src/panels/DinsarResultPanel.rewrite.jsx 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 }} - /> - - +
+ + {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 ? ( -
当前没有正在执行的产物处理任务。
- ) : ( -
-
+ + +
+ +
+ + +
+ + {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 && ( )}
+ {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 && ( + + )}
- {!readOnly && ( - - )} -
- ))} + ); + })}
)}
-
- )} + )} +
+ {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 ? '只读模式' : '可执行写操作'} - - 日志改为手动刷新 -
-
- -
-
-
-
- 结果提取与重扫 - 先提取标准结果包,再按统一目录登记 -
-
- -
- - -
- -
- - -
- - {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 && ( - - )} -
- - {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 && ( - - )} -
- ); - })} -
- )} -
- )} -
-
- - -
- ); -} - -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 ( -
-
-

日志管理

-
- - - -
-
- - {logs.length === 0 ? ( -
暂无日志文件
- ) : ( - - - - - - - - - - - - {logs.map((log, index) => ( - - - - - - - - ))} - -
文件名类型大小修改时间操作
{log.name} - - {getTypeLabel(log.type)} - - - {formatSize(log.size)} - {log.modified_at} -
- - {isAdmin && ( - - )} -
-
- )} - - {showModal && selectedLog && ( -
-
-
-
-

{selectedLog.name}

-
- 大小:{formatSize(selectedLog.size)} | 修改时间:{selectedLog.modified_at} | 总行数:{totalLines} -
-
- -
- -
- 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)} -
- - -
- -
-
-                {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 }) => {

日志管理

- + 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)}
- {/* 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 管理,便于同一对影像保留多套生产结果并行对比。'} +

- -
- -
-
-
目录状态
-
- -
-
-
-
需要重建
-
- {catalogStatus?.needs_rebuild ? '是' : '否'} -
-
-
-
Manifest / 数据库
-
- {(catalogStatus?.manifest_count ?? 0)} / {(catalogStatus?.db_count ?? 0)} -
-
-
-
问题数量
-
0 ? '#b45309' : '#16a34a' }}> - {catalogStatus?.issue_count ?? 0} -
+
+ {selectedEngineMeta && ( + + {selectedEngineMeta.shortLabel} + + )} +
-
+
+
+ 目录状态 + {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` 分别登记,不会互相覆盖。 +

-