chore: initialize insar management system v2
This commit is contained in:
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user