chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
@@ -0,0 +1,44 @@
export function PanelLoadingBody({ message = '正在加载面板...' }) {
return (
<div style={{ padding: '16px' }}>
<p className="empty-state">{message}</p>
</div>
);
}
export function PanelLoadingPanel({ message = '正在加载面板...' }) {
return (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<PanelLoadingBody message={message} />
</div>
);
}
export function ModalLoadingFallback({ message = '正在加载内容...' }) {
return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(15, 23, 42, 0.4)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2000,
}}
>
<div
style={{
background: '#fff',
borderRadius: '12px',
border: '1px solid #e2e8f0',
padding: '24px 28px',
minWidth: '280px',
boxShadow: '0 20px 45px rgba(15, 23, 42, 0.18)',
}}
>
<p className="empty-state" style={{ margin: 0 }}>{message}</p>
</div>
</div>
);
}
@@ -0,0 +1,27 @@
import { useUiStore } from '../../store';
import { useI18n } from '../../i18n/I18nContext';
export default function AppLogPanel({ width }) {
const logs = useUiStore((state) => state.logs);
const { t } = useI18n();
return (
<aside className="panel right-panel" style={{ width }}>
<header>
<h3>日志</h3>
</header>
<div className="panel-content log-entries">
{logs.length === 0 ? (
<p className="empty-state">暂无日志</p>
) : (
logs.map((log, index) => (
<div key={index} className="log-entry">
<span className="log-time">[{log.time}]</span>
<span className="log-message" data-log-type={log.type}>{t(log.message)}</span>
</div>
))
)}
</div>
</aside>
);
}
@@ -0,0 +1,117 @@
import { memo } from 'react';
import { BASE_LAYERS } from '../../config/appConstants';
import { getSelectedRegionTreeId } from '../../utils/appUiHelpers';
function AppMapWorkspace({
language,
showMapRegionLocator,
toggleMapRegionLocator,
mapRegionOptions,
mapRegionSelection,
mapRegionLoading,
mapRegionLocating,
mapRegionError,
mapRegionLocatedName,
onMapRegionProvinceChange,
onMapRegionCityChange,
onLocateSelectedRegion,
onClearMapRegionHighlight,
baseLayerKey,
setBaseLayerKey,
onOpenExportModal,
}) {
const en = language === 'en';
return (
<div className="center-container">
<main id="map-container">
<div id="map"></div>
<div className="map-region-locator">
<button
type="button"
className={`map-region-toggle-btn ${showMapRegionLocator ? 'active' : ''}`}
onClick={toggleMapRegionLocator}
>
{showMapRegionLocator ? (en ? 'Collapse' : '收起') : (en ? 'Region Locator' : '区域定位')}
</button>
{showMapRegionLocator && (
<div className="map-region-locator-panel">
<div className="map-region-locator-title">{en ? 'Region Locator' : '区域定位'}</div>
<div className="aoi-region-select-grid map-region-grid">
<select
value={mapRegionSelection.province}
onChange={(e) => onMapRegionProvinceChange(e.target.value)}
disabled={mapRegionLoading || mapRegionLocating}
>
<option value="">{en ? '-- Province --' : '-- 省级 --'}</option>
{mapRegionOptions.provinces.map((item) => (
<option key={item.tree_id} value={item.tree_id}>{item.name}</option>
))}
</select>
<select
value={mapRegionSelection.city}
onChange={(e) => onMapRegionCityChange(e.target.value)}
disabled={mapRegionLoading || mapRegionLocating || !mapRegionSelection.province}
>
<option value="">{en ? '-- City --' : '-- 地市 --'}</option>
{mapRegionOptions.cities.map((item) => (
<option key={item.tree_id} value={item.tree_id}>{item.name}</option>
))}
</select>
</div>
<div className="map-region-locator-hint">
{en ? 'You can locate by province or city level.' : '可只选到省/市级进行定位。'}
</div>
{mapRegionLocatedName && (
<div className="map-region-locator-current">
{en ? 'Current: ' : '当前定位:'}{mapRegionLocatedName}
</div>
)}
{mapRegionError && (
<div className="map-region-locator-error">{mapRegionError}</div>
)}
<div className="map-region-locator-actions">
<button
type="button"
className="map-region-locate-btn"
onClick={onLocateSelectedRegion}
disabled={mapRegionLoading || mapRegionLocating || !getSelectedRegionTreeId(mapRegionSelection)}
>
{mapRegionLocating ? (en ? 'Locating...' : '定位中...') : (en ? 'Locate Selected Region' : '定位到选中区域')}
</button>
<button
type="button"
className="map-region-clear-btn"
onClick={onClearMapRegionHighlight}
disabled={mapRegionLocating || !mapRegionLocatedName}
>
{en ? 'Clear Highlight' : '清除定位高亮'}
</button>
</div>
</div>
)}
</div>
<div className="map-layer-switch">
<div className="map-layer-title">{en ? 'Base Map' : '底图'}</div>
<div className="map-layer-buttons">
{Object.entries(BASE_LAYERS).map(([key, layer]) => (
<button
key={key}
type="button"
className={baseLayerKey === key ? 'active' : ''}
onClick={() => setBaseLayerKey(key)}
>
{layer.label}
</button>
))}
</div>
</div>
<button type="button" className="map-export-btn" onClick={onOpenExportModal}>
{en ? 'Export' : '导出'}
</button>
</main>
</div>
);
}
export default memo(AppMapWorkspace);
+153
View File
@@ -0,0 +1,153 @@
import { Suspense, lazy } from 'react';
import { useShallow } from 'zustand/react/shallow';
import LicenseOverlay from '../LicenseOverlay';
import { useDinsarStore, usePairingStore, useUiStore } from '../../store';
import { useI18n } from '../../i18n/I18nContext';
import { formatYmd } from '../../utils/appUiHelpers';
import { ModalLoadingFallback } from './AppLoadingFallbacks';
const LazyPairingModal = lazy(() => import('../PairingModal'));
const LazyPsStackModal = lazy(() => import('../PsStackModal'));
const LazyDataInfoModal = lazy(() => import('../DataInfoModal'));
const LazyActiveTasksOverlay = lazy(() => import('../ActiveTasksOverlay'));
const LazyStatisticsDashboard = lazy(() => import('../../StatisticsDashboard'));
const LazyAiReportModal = lazy(() => import('../AiReportModal'));
const LazyMapExportModal = lazy(() => import('../MapExportModal'));
export default function AppOverlays({
onPairingSubmit,
onPairingAoiModeChange,
onPairingProvinceChange,
onPairingCityChange,
onPsSubmit,
onPsAoiModeChange,
onPsProvinceChange,
onPsCityChange,
licenseLoading,
licenseStatus,
isAdmin,
licenseFileRef,
onUploadFile,
onRefreshLicenseStatus,
licenseFileName,
licenseUploadStatus,
isGlobalLocked,
activeTasks,
showForceUnlock,
forceUnlockPwd,
onShowForceUnlock,
onForceUnlockPwdChange,
onForceUnlockConfirm,
onCancelForceUnlock,
mapExport,
}) {
const { language, t } = useI18n();
const { showPairingModal, showPsModal } = usePairingStore(useShallow((state) => ({
showPairingModal: state.showPairingModal,
showPsModal: state.showPsModal,
})));
const {
showStats,
setShowStats,
showDataInfo,
setShowDataInfo,
selectedDataInfo,
} = useUiStore(useShallow((state) => ({
showStats: state.showStats,
setShowStats: state.setShowStats,
showDataInfo: state.showDataInfo,
setShowDataInfo: state.setShowDataInfo,
selectedDataInfo: state.selectedDataInfo,
})));
const { activeAiReport, setActiveAiReport } = useDinsarStore(useShallow((state) => ({
activeAiReport: state.activeAiReport,
setActiveAiReport: state.setActiveAiReport,
})));
return (
<>
{showPairingModal && (
<Suspense fallback={<ModalLoadingFallback message="正在加载组网参数弹窗..." />}>
<LazyPairingModal
onSubmit={onPairingSubmit}
onAoiModeChange={onPairingAoiModeChange}
onProvinceChange={onPairingProvinceChange}
onCityChange={onPairingCityChange}
/>
</Suspense>
)}
{showPsModal && (
<Suspense fallback={<ModalLoadingFallback message="正在加载 PS 参数弹窗..." />}>
<LazyPsStackModal
onSubmit={onPsSubmit}
onAoiModeChange={onPsAoiModeChange}
onProvinceChange={onPsProvinceChange}
onCityChange={onPsCityChange}
/>
</Suspense>
)}
{activeAiReport && (
<Suspense fallback={<ModalLoadingFallback message="正在加载 AI 报告..." />}>
<LazyAiReportModal
report={activeAiReport}
onClose={() => setActiveAiReport(null)}
/>
</Suspense>
)}
{showStats && (
<Suspense fallback={<ModalLoadingFallback message="正在加载统计看板..." />}>
<LazyStatisticsDashboard onClose={() => setShowStats(false)} />
</Suspense>
)}
<LicenseOverlay
licenseLoading={licenseLoading}
licenseStatus={licenseStatus}
isAdmin={isAdmin}
licenseFileRef={licenseFileRef}
onUploadFile={onUploadFile}
onRefreshStatus={onRefreshLicenseStatus}
licenseFileName={licenseFileName}
licenseUploadStatus={licenseUploadStatus}
/>
{showDataInfo && (
<Suspense fallback={<ModalLoadingFallback message="正在加载数据详情..." />}>
<LazyDataInfoModal
visible={showDataInfo}
dataInfo={selectedDataInfo}
language={language}
formatYmd={(value) => formatYmd(value, language)}
onClose={() => setShowDataInfo(false)}
/>
</Suspense>
)}
{isGlobalLocked && (
<Suspense fallback={<ModalLoadingFallback message="正在加载任务控制面板..." />}>
<LazyActiveTasksOverlay
isVisible={isGlobalLocked}
activeTasks={activeTasks}
t={t}
isAdmin={isAdmin}
showForceUnlock={showForceUnlock}
forceUnlockPwd={forceUnlockPwd}
onShowForceUnlock={onShowForceUnlock}
onForceUnlockPwdChange={onForceUnlockPwdChange}
onForceUnlockConfirm={onForceUnlockConfirm}
onCancelForceUnlock={onCancelForceUnlock}
/>
</Suspense>
)}
{mapExport.showExportModal && (
<Suspense fallback={<ModalLoadingFallback message="正在加载地图导出工具..." />}>
<LazyMapExportModal {...mapExport} />
</Suspense>
)}
</>
);
}
@@ -0,0 +1,479 @@
import { Suspense, lazy } from 'react';
import apiClient from '../../api/client';
import RadarDataPanel from '../../panels/RadarDataPanel';
import {
ADMIN_ONLY_TABS,
LEFT_GROUP_LABELS,
LEFT_GROUP_SECTIONS,
LEFT_GROUP_TABS,
LEFT_TAB_GROUP,
LEFT_TAB_SECTION,
} from '../../config/appConstants';
import { getLeftTabLabel } from '../../utils/appUiHelpers';
import { PanelLoadingBody, PanelLoadingPanel } from './AppLoadingFallbacks';
const LazyDataMonitorPanel = lazy(() => import('../../DataMonitorPanel'));
const LazyDataCopierPanel = lazy(() => import('../../DataCopierPanel'));
const LazyIDLAutomationPanel = lazy(() => import('../../IDLAutomationPanel'));
const LazyDinsarProductionPanel = lazy(() => import('../../DinsarProductionPanel'));
const LazyDinsarProductsPanel = lazy(() => import('../../DinsarProductsPanel'));
const LazyHazardPointPanel = lazy(() => import('../../HazardPointPanel'));
const LazyHealthCheckPanel = lazy(() => import('../../HealthCheckPanel'));
const LazyTimeseriesProductionPanel = lazy(() => import('../../TimeseriesProductionPanel'));
const LazyWaterMonitorPanel = lazy(() => import('../../WaterMonitorPanel'));
const LazyUserAdminPanel = lazy(() => import('../../UserAdminPanel'));
const LazyAuditLogPanel = lazy(() => import('../../AuditLogPanel'));
const LazyAiQualityPanel = lazy(() => import('../../panels/AiQualityPanel'));
const LazyAiAnalysisPanel = lazy(() => import('../../AiAnalysisPanel'));
const LazyPairingPanel = lazy(() => import('../../panels/PairingPanel'));
const LazyDinsarResultPanel = lazy(() => import('../../panels/DinsarResultPanel'));
const LazyBatchPanel = lazy(() => import('../../panels/BatchPanel'));
const LazyPairsListPanel = lazy(() => import('../../panels/PairsListPanel'));
const LazyPsResultsPanel = lazy(() => import('../../panels/PsResultsPanel'));
const LazyPsinsarCatalogPanel = lazy(() => import('../PsinsarCatalogPanel'));
export default function AppSidePanel({
leftPanelWidth,
leftPanelTab,
setLeftPanelTab,
isAdmin,
isReadOnlyUser,
currentUser,
language,
apiEndpoint,
licenseOk,
foundPairs,
psResults,
dinsarTotal,
selectedPairsCount,
hasEnoughRadarScenesForPlanning,
isLoading,
hasRadarSearched,
showHazardPoints,
hazardPoints,
aiStatus,
radarPanel,
pairingPanel,
taskPanel,
hazardPanel,
waterPanel,
dinsarPanel,
aiPanel,
pairsPanel,
psPanel,
}) {
const activeLeftGroup = LEFT_TAB_GROUP[leftPanelTab] || 'data';
const getVisibleTabs = (tabs = []) => tabs.filter((tab) => isAdmin || !ADMIN_ONLY_TABS.has(tab));
const getVisibleSections = (groupKey) => (
(LEFT_GROUP_SECTIONS[groupKey] || [])
.map((section) => ({
...section,
tabs: getVisibleTabs(section.tabs || []),
}))
.filter((section) => section.tabs.length > 0)
);
const getDefaultGroupTab = (groupKey) => {
const visibleSections = getVisibleSections(groupKey);
if (visibleSections.length > 0) {
return visibleSections[0]?.tabs?.[0] || '';
}
return getVisibleTabs(LEFT_GROUP_TABS[groupKey] || [])[0] || '';
};
const activeGroupSections = getVisibleSections(activeLeftGroup);
const hasSectionNav = activeGroupSections.length > 0;
const preferredActiveSection = LEFT_TAB_SECTION[leftPanelTab];
const activeLeftSection = hasSectionNav && activeGroupSections.some((section) => section.key === preferredActiveSection)
? preferredActiveSection
: (activeGroupSections[0]?.key || null);
const activeLeafTabs = hasSectionNav
? (activeGroupSections.find((section) => section.key === activeLeftSection)?.tabs || [])
: getVisibleTabs(LEFT_GROUP_TABS[activeLeftGroup] || []);
const psResultCount = psResults ? Object.keys(psResults).length : 0;
return (
<aside className="panel data-panel" style={{ display: 'flex', flexDirection: 'column', width: leftPanelWidth }}>
<div className="panel-tabs">
<div className="tabs-header group-tabs">
{Object.entries(LEFT_GROUP_LABELS)
.filter(([groupKey]) => {
if (isAdmin) return true;
return !!getDefaultGroupTab(groupKey);
})
.map(([groupKey, label]) => (
<button
key={groupKey}
className={activeLeftGroup === groupKey ? 'active-tab' : ''}
onClick={() => {
const nextTab = getDefaultGroupTab(groupKey);
if (nextTab) setLeftPanelTab(nextTab);
}}
>
{label}
</button>
))}
</div>
{hasSectionNav && (
<div className="tabs-header section-tabs">
{activeGroupSections.map((section) => (
<button
key={section.key}
className={activeLeftSection === section.key ? 'active-tab' : ''}
onClick={() => setLeftPanelTab(section.tabs[0])}
>
{section.label}
</button>
))}
</div>
)}
<div className="tabs-header left-tabs sub-tabs">
{activeLeafTabs.map((tabKey) => (
<button
key={tabKey}
className={leftPanelTab === tabKey ? 'active-tab' : ''}
onClick={() => setLeftPanelTab(tabKey)}
>
{getLeftTabLabel(tabKey, {
pairCount: foundPairs.length,
psResultCount,
dinsarTotal,
})}
</button>
))}
</div>
</div>
{leftPanelTab === 'data' && (
<RadarDataPanel
radarCurrentPage={radarPanel.radarCurrentPage}
radarTotalPages={radarPanel.radarTotalPages}
showRadarPageInputError={radarPanel.showRadarPageInputError}
radarPageInputValidationError={radarPanel.radarPageInputValidationError}
onSearchAll={radarPanel.onSearchAll}
onShowStats={radarPanel.onShowStats}
onSearch={radarPanel.onSearch}
onReset={radarPanel.onReset}
onAoiModeChange={radarPanel.onAoiModeChange}
onProvinceChange={radarPanel.onProvinceChange}
onCityChange={radarPanel.onCityChange}
onSetRadarSearchFiles={radarPanel.onSetRadarSearchFiles}
updateDraft={radarPanel.updateDraft}
onPageChange={radarPanel.onPageChange}
onPageSizeChange={radarPanel.onPageSizeChange}
onGoToPage={radarPanel.onGoToPage}
onSelectAllVisibility={radarPanel.onSelectAllVisibility}
onSetAllPreviewVisibility={radarPanel.onSetAllPreviewVisibility}
onToggleLayer={radarPanel.onToggleLayer}
onTogglePreview={radarPanel.onTogglePreview}
onRebuildPreview={radarPanel.onRebuildPreview}
onShowDataInfo={radarPanel.onShowDataInfo}
onFlyTo={radarPanel.onFlyTo}
onChangeSatelliteGroup={radarPanel.onChangeSatelliteGroup}
/>
)}
{leftPanelTab === 'ingest' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载数据接入面板..." />}>
<LazyDataMonitorPanel
apiEndpoint={apiEndpoint}
onTaskStart={taskPanel.onTaskStart}
readOnly={isReadOnlyUser}
enabled={!!currentUser && licenseOk}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'pairing' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载组网规划面板..." />}>
<LazyPairingPanel
foundPairs={foundPairs}
selectedPairsCount={selectedPairsCount}
isLoading={isLoading}
isReadOnlyUser={isReadOnlyUser}
hasEnoughRadarScenesForPlanning={hasEnoughRadarScenesForPlanning}
onOpenPairingModal={pairingPanel.onOpenPairingModal}
onOpenPsModal={pairingPanel.onOpenPsModal}
hasRadarSearched={hasRadarSearched}
onRefreshRadarSearch={pairingPanel.onRefreshRadarSearch}
onSearchAll={radarPanel.onSearchAll}
onRefreshDinsar={pairingPanel.onRefreshDinsar}
language={language}
/>
</Suspense>
)}
{leftPanelTab === 'copier' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载数据分发面板..." />}>
<LazyDataCopierPanel
apiEndpoint={apiEndpoint}
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, '数据分发任务已入队,正在处理...')}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'idl' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 IDL 面板..." />}>
<LazyIDLAutomationPanel
apiEndpoint={apiEndpoint}
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, '任务已入队,等待处理...')}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'dinsar_production' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 D-InSAR 生产面板..." />}>
<LazyDinsarProductionPanel
readOnly={isReadOnlyUser}
currentUser={currentUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, 'D-InSAR 任务已入队,等待处理...')}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'dinsar_products' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 D-InSAR 产物面板..." />}>
<LazyDinsarProductsPanel
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, 'D-InSAR 产物任务已入队,等待处理...')}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'ps_production' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 PS-InSAR 生产面板..." />}>
<LazyTimeseriesProductionPanel
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, 'SBAS 运行已入队,正在执行 prepare...')}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'ps_products' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 PS-InSAR 目录面板..." />}>
<div style={{ padding: '16px' }}>
<LazyPsinsarCatalogPanel
readOnly={isReadOnlyUser}
showActions
onTaskQueued={(taskId) => taskPanel.onTaskStart(taskId, 'PS-InSAR 结果目录任务已入队,等待处理...')}
/>
</div>
</Suspense>
</div>
)}
{leftPanelTab === 'hazard' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载隐患点面板..." />}>
<LazyHazardPointPanel
apiEndpoint={apiEndpoint}
onPointClick={hazardPanel.onPointClick}
isVisible={showHazardPoints}
onToggleVisibility={hazardPanel.onToggleVisibility}
onScanComplete={hazardPanel.onScanComplete}
points={hazardPoints}
onTaskStart={taskPanel.onTaskStart}
readOnly={isReadOnlyUser}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'water' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载水体监测面板..." />}>
<LazyWaterMonitorPanel
readOnly={isReadOnlyUser}
onShowOnMap={waterPanel.onShowOnMap}
onShowFloodOnMap={waterPanel.onShowFloodOnMap}
onToggleFloodLayer={waterPanel.onToggleFloodLayer}
onTaskStart={taskPanel.onTaskStart}
language={language}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'health' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载运维自检面板..." />}>
<LazyHealthCheckPanel
apiEndpoint={apiEndpoint}
language={language}
currentUser={currentUser}
/>
</Suspense>
</div>
)}
{leftPanelTab === 'users' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
{isAdmin ? (
<Suspense fallback={<PanelLoadingBody message="正在加载用户管理面板..." />}>
<LazyUserAdminPanel apiClient={apiClient} currentUser={currentUser} />
</Suspense>
) : (
<div style={{ padding: '16px' }}>
<p className="empty-state">仅管理员可访问用户管理</p>
</div>
)}
</div>
)}
{leftPanelTab === 'audit' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
{isAdmin ? (
<Suspense fallback={<PanelLoadingBody message="正在加载审计日志面板..." />}>
<LazyAuditLogPanel apiClient={apiClient} />
</Suspense>
) : (
<div style={{ padding: '16px' }}>
<p className="empty-state">仅管理员可访问审计日志</p>
</div>
)}
</div>
)}
{leftPanelTab === 'dinsar_results' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载 D-InSAR 结果面板..." />}>
<LazyDinsarResultPanel
dinsarCurrentPage={dinsarPanel.dinsarCurrentPage}
dinsarTotalPages={dinsarPanel.dinsarTotalPages}
showDinsarPageInputError={dinsarPanel.showDinsarPageInputError}
dinsarPageInputValidationError={dinsarPanel.dinsarPageInputValidationError}
onSetAllVisibility={dinsarPanel.onSetAllVisibility}
onScoreFilterChange={dinsarPanel.onScoreFilterChange}
onPageChange={dinsarPanel.onPageChange}
onPageSizeChange={dinsarPanel.onPageSizeChange}
onGoToPage={dinsarPanel.onGoToPage}
onToggleVisibility={dinsarPanel.onToggleVisibility}
onLabel={dinsarPanel.onLabel}
onAnalyze={dinsarPanel.onAnalyze}
/>
</Suspense>
)}
{leftPanelTab === 'dinsar_analysis' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<div style={{ padding: '16px' }}>
<div className="empty-state">
D-InSAR 分析页已预留
<br />
后续可在这里承接专题筛选人工判读统计汇总和分析报告能力
</div>
</div>
</div>
)}
{leftPanelTab === 'psinsar_results' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<Suspense fallback={<PanelLoadingBody message="正在加载 PS-InSAR 结果目录..." />}>
<div style={{ padding: '16px' }}>
<LazyPsinsarCatalogPanel
readOnly
showActions={false}
/>
</div>
</Suspense>
</div>
)}
{leftPanelTab === 'psinsar_analysis' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<div style={{ padding: '16px' }}>
<div className="empty-state">
PS-InSAR 分析页已预留
<br />
后续可以在这里放置时序分析速率分级热点识别和专题统计能力
</div>
</div>
</div>
)}
{leftPanelTab === 'ai_quality' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载 AI 质量面板..." />}>
<LazyAiQualityPanel
aiStatus={aiStatus}
isLoading={isLoading}
isReadOnlyUser={isReadOnlyUser}
onTrain={aiPanel.onTrain}
onPredictAll={aiPanel.onPredictAll}
language={language}
/>
</Suspense>
)}
{leftPanelTab === 'ai_diagnosis' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载 AI 诊断面板..." />}>
<LazyAiAnalysisPanel
readOnly={isReadOnlyUser}
onJobQueued={(taskId) => taskPanel.onTaskStart(taskId, '任务已入队,等待处理...')}
/>
</Suspense>
)}
{leftPanelTab === 'landslide_segmentation' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<div style={{ padding: '16px' }}>
<div className="empty-state">
滑坡语义分割模块已预留
<br />
后续可在这里接入光学影像分割模型结果预览批处理提交和专题输出
</div>
</div>
</div>
)}
{leftPanelTab === 'uav_image_analysis' && (
<div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'auto' }}>
<div style={{ padding: '16px' }}>
<div className="empty-state">
无人机影像分析模块已预留
<br />
后续可在这里集成无人机正射影像解译目标识别和变化检测能力
</div>
</div>
</div>
)}
{leftPanelTab === 'pairs' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载配对结果面板..." />}>
<LazyPairsListPanel
onVisualizePair={pairsPanel.onVisualizePair}
onTogglePairVisibility={pairsPanel.onTogglePairVisibility}
onCreateDinsarBatch={pairsPanel.onCreateDinsarBatch}
/>
</Suspense>
)}
{leftPanelTab === 'ps_results' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载 PS 候选结果面板..." />}>
<LazyPsResultsPanel
onPreviewPsStack={psPanel.onPreviewPsStack}
onCreatePsBatch={psPanel.onCreatePsBatch}
onClearPsResults={psPanel.onClearPsResults}
/>
</Suspense>
)}
{leftPanelTab === 'batches' && (
<Suspense fallback={<PanelLoadingPanel message="正在加载批处理面板..." />}>
<LazyBatchPanel />
</Suspense>
)}
</aside>
);
}
@@ -0,0 +1,112 @@
import { memo } from 'react';
import { formatUtc, getStatusClass } from '../../utils/appUiHelpers';
function AppStatusHeader({
language,
setLanguage,
currentUser,
isAdmin,
isReadOnlyUser,
activeTasks,
avgTaskProgress,
licenseStatus,
healthStatus,
healthLoading,
healthError,
onRefreshHealth,
onLogout,
}) {
const hasHealthStatus = !!healthStatus;
const licenseOk = !!licenseStatus?.ok;
const dbOk = !!(healthStatus?.database?.ok && healthStatus?.database?.schema_ok && healthStatus?.database?.postgis_ok);
const workerOk = !!healthStatus?.worker?.ok;
const idlOk = !!healthStatus?.idl?.ok;
const aiOk = !!healthStatus?.ollama?.ok;
const nginxOk = !!healthStatus?.nginx?.ok;
return (
<>
<div className="top-status-bar">
<div className="status-brand">
<div className="brand-title">InSAR 自动化管理系统</div>
<div className="brand-subtitle">科研工程模式 · {licenseOk ? '已授权' : '未授权'}</div>
</div>
<div className="status-items">
<div className="status-item">
<span className={`status-dot ${getStatusClass(dbOk, hasHealthStatus)}`}></span>DB
</div>
<div className="status-item">
<span className={`status-dot ${getStatusClass(workerOk, hasHealthStatus)}`}></span>Worker
</div>
<div className="status-item">
<span className={`status-dot ${getStatusClass(idlOk, hasHealthStatus)}`}></span>IDL
</div>
<div className="status-item">
<span className={`status-dot ${getStatusClass(aiOk, hasHealthStatus)}`}></span>Ollama
</div>
<div className="status-item">
<span className={`status-dot ${getStatusClass(nginxOk, hasHealthStatus)}`}></span>Nginx
</div>
</div>
<div className="status-actions">
<div className="status-lang-switch" data-no-i18n="true">
<button
className={`status-lang-btn ${language === 'zh' ? 'active' : ''}`}
onClick={() => setLanguage('zh')}
title="切换到中文"
>
中文
</button>
<button
className={`status-lang-btn ${language === 'en' ? 'active' : ''}`}
onClick={() => setLanguage('en')}
title="Switch to English"
>
EN
</button>
</div>
<div className={`user-role-chip ${isAdmin ? 'admin' : 'viewer'}`}>
<span className="user-role-name">{currentUser.username}</span>
<span className="user-role-divider">·</span>
<span>{isAdmin ? '管理员' : '只读账号'}</span>
</div>
<div className={`status-task ${activeTasks.length > 0 ? 'has-active-tasks' : ''}`}>
<span>{activeTasks.length > 0 ? `⚙️ 运行中 ${activeTasks.length}` : '✓ 空闲'}</span>
{activeTasks.length > 0 && (
<div className="status-task-bar">
<div className="status-task-fill" style={{ width: `${avgTaskProgress}%` }}></div>
</div>
)}
</div>
{licenseStatus?.expires_at && (
<div className="status-license">
授权至 {formatUtc(licenseStatus.expires_at, language)}
</div>
)}
<button
className="status-refresh"
onClick={onRefreshHealth}
disabled={healthLoading}
title={healthError || '刷新自检状态'}
>
{healthLoading ? '自检中...' : '刷新自检'}
</button>
<button
className="status-refresh"
onClick={onLogout}
title="退出登录"
>
退出登录
</button>
</div>
</div>
{isReadOnlyUser && (
<div className="read-only-banner">
当前账号为只读权限可查看数据与状态但不能执行写操作
</div>
)}
</>
);
}
export default memo(AppStatusHeader);