import { useEffect, useRef, useCallback } from 'react'; import L from 'leaflet'; import { useShallow } from 'zustand/react/shallow'; import 'leaflet/dist/leaflet.css'; import './App.css'; import LoginPage from './LoginPage'; import AppLogPanel from './components/app/AppLogPanel'; import AppMapWorkspace from './components/app/AppMapWorkspace'; import AppOverlays from './components/app/AppOverlays'; import AppSidePanel from './components/app/AppSidePanel'; import AppStatusHeader from './components/app/AppStatusHeader'; import { useI18n } from './i18n/I18nContext'; import apiClient from './api/client'; import { getSbasInsarProductAssetUrl } from './api/sbasInsarProducts'; import { useAuthStore, useTaskStore, useUiStore, useRadarStore, useDinsarStore, useBatchStore, usePairingStore, useHazardStore, useMapStore, } from './store'; import useAppAuthLifecycle from './hooks/useAppAuthLifecycle'; import useGlobalTaskControl from './hooks/useGlobalTaskControl'; import usePanelResize from './hooks/usePanelResize'; import useRegionAoiHandlers from './hooks/useRegionAoiHandlers'; import usePaginationControls from './hooks/usePaginationControls'; import useRadarSearch from './hooks/useRadarSearch'; import useBatchOperations from './hooks/useBatchOperations'; import useDinsarOperations from './hooks/useDinsarOperations'; import usePairingLogic from './hooks/usePairingLogic'; import useMapExport from './hooks/useMapExport'; import { TILE_LAYER_OPTIONS, TILE_LAYER_BOUNDS, NATIONAL_BOUNDARY_GEOJSON_URL, getBaseLayerConfig, DEFAULT_LIST_PAGE_SIZE, FULL_WIDTH_LEFT_TABS, } from './config/appConstants'; import { escapeHtml, formatCoordinate } from './utils/appHelpers'; import { normalizePreviewStatus, formatYmd, parseDatesFromName, } from './utils/appUiHelpers'; import { DINSAR_STRATEGY_ALL, filterDinsarResults, } from './utils/dinsarResultFilters'; import { DINSAR_ENGINE_ALL, getDinsarEngineMeta } from './utils/dinsarEngines'; const NATIONAL_BOUNDARY_STATIC_URL = '/geojson/\u5168\u56fd\u884c\u653f\u533a.geojson'; const toFiniteNumber = (value) => { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; }; const formatMapNumber = (value, digits = 2) => { const numeric = Number(value); return Number.isFinite(numeric) ? numeric.toFixed(digits) : '-'; }; const getSbasProductBounds = (product) => { const coverage = product?.geographic_coverage || {}; const bbox = coverage.bbox || {}; const minLon = toFiniteNumber(product?.min_lon ?? bbox.min_lon); const minLat = toFiniteNumber(product?.min_lat ?? bbox.min_lat); const maxLon = toFiniteNumber(product?.max_lon ?? bbox.max_lon); const maxLat = toFiniteNumber(product?.max_lat ?? bbox.max_lat); if ([minLon, minLat, maxLon, maxLat].some(value => value === null)) return null; if (minLon >= maxLon || minLat >= maxLat) return null; return [[minLat, minLon], [maxLat, maxLon]]; }; const findSbasAsset = (detail, roles) => { const roleSet = new Set(roles); return (detail?.assets || []).find(asset => roleSet.has(asset.asset_role) && asset.exists_flag); }; const sbasAssetCacheKey = (asset) => ( [asset?.id, asset?.file_size, asset?.updated_at || asset?.created_at || asset?.relative_path] .filter(Boolean) .join(':') ); const sbasRateColor = (rate) => { const numeric = Number(rate); if (!Number.isFinite(numeric)) return '#64748b'; if (numeric <= -30) return '#1d4ed8'; if (numeric < -5) return '#38bdf8'; if (numeric <= 5) return '#16a34a'; if (numeric < 30) return '#f59e0b'; return '#dc2626'; }; const SBAS_OVERVIEW_COLORS = ['#1d4ed8', '#dc2626', '#059669', '#7c3aed', '#d97706', '#0f766e']; const normalizeSbasDisplacements = (rows) => (Array.isArray(rows) ? rows : []) .map((item) => { const date = String(item?.date || '').trim(); const time = Date.parse(`${date}T00:00:00Z`); const displacement = Number(item?.displacement_mm ?? item?.displacement ?? item?.value); if (!date || !Number.isFinite(time) || !Number.isFinite(displacement)) return null; return { date, time, displacement }; }) .filter(Boolean) .sort((left, right) => left.time - right.time); const buildSbasSparklineSvg = (rows) => { const values = normalizeSbasDisplacements(rows); if (values.length < 2) return ''; const width = 220; const height = 72; const padX = 12; const padY = 10; const minTime = Math.min(...values.map(item => item.time)); const maxTime = Math.max(...values.map(item => item.time)); const minValue = Math.min(...values.map(item => item.displacement), 0); const maxValue = Math.max(...values.map(item => item.displacement), 0); const timeSpan = Math.max(1, maxTime - minTime); const valueSpan = Math.max(1e-9, maxValue - minValue); const xScale = (time) => padX + ((time - minTime) / timeSpan) * (width - padX * 2); const yScale = (value) => height - padY - ((value - minValue) / valueSpan) * (height - padY * 2); const points = values.map(item => `${xScale(item.time).toFixed(1)},${yScale(item.displacement).toFixed(1)}`).join(' '); const zeroY = yScale(0).toFixed(1); const minLabel = escapeHtml(formatMapNumber(minValue, 1)); const maxLabel = escapeHtml(formatMapNumber(maxValue, 1)); return ` ${values.map(item => ``).join('')} ${maxLabel} mm ${minLabel} mm `; }; const buildSbasPointPopupHtml = (point, options = {}) => { const matched = point?.matched || {}; const pointId = escapeHtml(point?.point_id || options.pointId || 'SBAS point'); const label = escapeHtml(point?.selection_label || options.label || pointId); const rate = point?.deformation_rate_mm_per_year ?? point?.los_rate_mm_per_year ?? matched.los_rate_mm_per_year; const lon = point?.lon ?? matched.lon; const lat = point?.lat ?? matched.lat; const nearestNote = matched.used_nearest ? `
匹配: 最近有效像元,距离 ${escapeHtml(formatMapNumber(matched.distance_m, 1))} m
` : ''; return `
${label}
ID: ${pointId}
经纬度: ${escapeHtml(formatMapNumber(lon, 6))}, ${escapeHtml(formatMapNumber(lat, 6))}
LOS速率: ${escapeHtml(formatMapNumber(rate, 2))} mm/yr
${nearestNote}
${buildSbasSparklineSvg(point?.displacements || options.displacements || [])}
`; }; const buildSbasOverviewPopupHtml = (product) => { const title = escapeHtml(product?.display_name || product?.stack_key || product?.run_key || `SBAS #${product?.id ?? '-'}`); const dateStart = escapeHtml(String(product?.date_start || '-').slice(0, 10)); const dateEnd = escapeHtml(String(product?.date_end || '-').slice(0, 10)); const stackSize = escapeHtml(product?.stack_size ?? product?.stack_dates?.length ?? '-'); const status = escapeHtml(product?.status || '-'); const health = escapeHtml(product?.health_status || '-'); const runKey = escapeHtml(product?.run_key || '-'); const stackKey = escapeHtml(product?.stack_key || '-'); const region = product?.admin_region?.display_name || product?.admin_region?.name || product?.admin_region?.tree_id || '-'; return `
${title}
时间: ${dateStart} → ${dateEnd}
栈期数: ${stackSize}
状态: ${status} / ${health}
区域: ${escapeHtml(region)}
stack: ${stackKey}
run: ${runKey}
`; }; function App() { const { language, setLanguage } = useI18n(); // --- Zustand stores --- const { currentUser, setCurrentUser, authChecked, setAuthChecked, licenseStatus, setLicenseStatus, licenseLoading, setLicenseLoading, licenseUploadStatus, setLicenseUploadStatus, licenseFileName, setLicenseFileName, healthStatus, setHealthStatus, healthLoading, setHealthLoading, healthError, setHealthError, } = useAuthStore(useShallow((state) => ({ currentUser: state.currentUser, setCurrentUser: state.setCurrentUser, authChecked: state.authChecked, setAuthChecked: state.setAuthChecked, licenseStatus: state.licenseStatus, setLicenseStatus: state.setLicenseStatus, licenseLoading: state.licenseLoading, setLicenseLoading: state.setLicenseLoading, licenseUploadStatus: state.licenseUploadStatus, setLicenseUploadStatus: state.setLicenseUploadStatus, licenseFileName: state.licenseFileName, setLicenseFileName: state.setLicenseFileName, healthStatus: state.healthStatus, setHealthStatus: state.setHealthStatus, healthLoading: state.healthLoading, setHealthLoading: state.setHealthLoading, healthError: state.healthError, setHealthError: state.setHealthError, }))); const { activeTasks, setActiveTasks, isCheckingTasks, setIsCheckingTasks, pendingTaskIds, setPendingTaskIds, } = useTaskStore(useShallow((state) => ({ activeTasks: state.activeTasks, setActiveTasks: state.setActiveTasks, isCheckingTasks: state.isCheckingTasks, setIsCheckingTasks: state.setIsCheckingTasks, pendingTaskIds: state.pendingTaskIds, setPendingTaskIds: state.setPendingTaskIds, }))); const { leftPanelTab, setLeftPanelTab, leftPanelWidth, setLeftPanelWidth, rightPanelWidth, setRightPanelWidth, isResizing, setIsResizing, setShowStats, setShowDataInfo, setSelectedDataInfo, showDates, baseLayerKey, setBaseLayerKey, isLoading, setIsLoading, addLog, } = useUiStore(useShallow((state) => ({ leftPanelTab: state.leftPanelTab, setLeftPanelTab: state.setLeftPanelTab, leftPanelWidth: state.leftPanelWidth, setLeftPanelWidth: state.setLeftPanelWidth, rightPanelWidth: state.rightPanelWidth, setRightPanelWidth: state.setRightPanelWidth, isResizing: state.isResizing, setIsResizing: state.setIsResizing, setShowStats: state.setShowStats, setShowDataInfo: state.setShowDataInfo, setSelectedDataInfo: state.setSelectedDataInfo, showDates: state.showDates, baseLayerKey: state.baseLayerKey, setBaseLayerKey: state.setBaseLayerKey, isLoading: state.isLoading, setIsLoading: state.setIsLoading, addLog: state.addLog, }))); const { allData, setAllData, radarPagination, setRadarPagination, radarPageInput, setRadarPageInput, radarPageInputTouched, setRadarPageInputTouched, hasRadarSearched, setHasRadarSearched, radarImagingDates, setRadarImagingDates, radarSearchDraft, setRadarSearchDraft, radarSearchApplied, setRadarSearchApplied, setRadarSearchOptions, setRadarSearchOptionsLoading, radarSearchAoiMode, setRadarSearchAoiMode, radarSearchAppliedAoiMode, setRadarSearchAppliedAoiMode, radarSearchFiles, setRadarSearchFiles, radarSearchRegionOptions, setRadarSearchRegionOptions, radarSearchRegionSelection, setRadarSearchRegionSelection, radarSearchAppliedRegionTreeId, setRadarSearchAppliedRegionTreeId, setRadarSearchRegionLoading, setRadarSearchRegionError, radarSearchAoiToken, setRadarSearchAoiToken, selectedSatelliteGroup, setSelectedSatelliteGroup, rebuildingPreviewIds, setRebuildingPreviewIds, } = useRadarStore(useShallow((state) => ({ allData: state.allData, setAllData: state.setAllData, radarPagination: state.radarPagination, setRadarPagination: state.setRadarPagination, radarPageInput: state.radarPageInput, setRadarPageInput: state.setRadarPageInput, radarPageInputTouched: state.radarPageInputTouched, setRadarPageInputTouched: state.setRadarPageInputTouched, hasRadarSearched: state.hasRadarSearched, setHasRadarSearched: state.setHasRadarSearched, radarImagingDates: state.radarImagingDates, setRadarImagingDates: state.setRadarImagingDates, radarSearchDraft: state.radarSearchDraft, setRadarSearchDraft: state.setRadarSearchDraft, radarSearchApplied: state.radarSearchApplied, setRadarSearchApplied: state.setRadarSearchApplied, setRadarSearchOptions: state.setRadarSearchOptions, setRadarSearchOptionsLoading: state.setRadarSearchOptionsLoading, radarSearchAoiMode: state.radarSearchAoiMode, setRadarSearchAoiMode: state.setRadarSearchAoiMode, radarSearchAppliedAoiMode: state.radarSearchAppliedAoiMode, setRadarSearchAppliedAoiMode: state.setRadarSearchAppliedAoiMode, radarSearchFiles: state.radarSearchFiles, setRadarSearchFiles: state.setRadarSearchFiles, radarSearchRegionOptions: state.radarSearchRegionOptions, setRadarSearchRegionOptions: state.setRadarSearchRegionOptions, radarSearchRegionSelection: state.radarSearchRegionSelection, setRadarSearchRegionSelection: state.setRadarSearchRegionSelection, radarSearchAppliedRegionTreeId: state.radarSearchAppliedRegionTreeId, setRadarSearchAppliedRegionTreeId: state.setRadarSearchAppliedRegionTreeId, setRadarSearchRegionLoading: state.setRadarSearchRegionLoading, setRadarSearchRegionError: state.setRadarSearchRegionError, radarSearchAoiToken: state.radarSearchAoiToken, setRadarSearchAoiToken: state.setRadarSearchAoiToken, selectedSatelliteGroup: state.selectedSatelliteGroup, setSelectedSatelliteGroup: state.setSelectedSatelliteGroup, rebuildingPreviewIds: state.rebuildingPreviewIds, setRebuildingPreviewIds: state.setRebuildingPreviewIds, }))); const { dinsarResults, setDinsarResults, dinsarPagination, dinsarPageInput, setDinsarPageInput, dinsarPageInputTouched, setDinsarPageInputTouched, aiStatus, scoreFilter, setScoreFilter, engineFilter, traceSearch, strategyFilter, } = useDinsarStore(useShallow((state) => ({ dinsarResults: state.dinsarResults, setDinsarResults: state.setDinsarResults, dinsarPagination: state.dinsarPagination, dinsarPageInput: state.dinsarPageInput, setDinsarPageInput: state.setDinsarPageInput, dinsarPageInputTouched: state.dinsarPageInputTouched, setDinsarPageInputTouched: state.setDinsarPageInputTouched, aiStatus: state.aiStatus, scoreFilter: state.scoreFilter, setScoreFilter: state.setScoreFilter, engineFilter: state.engineFilter, traceSearch: state.traceSearch, strategyFilter: state.strategyFilter, }))); const { batchTab, setDinsarBatches, setPsBatches, selectedBatchId, setBatchItems, setBatchLoading, setBatchError, } = useBatchStore(useShallow((state) => ({ batchTab: state.batchTab, setDinsarBatches: state.setDinsarBatches, setPsBatches: state.setPsBatches, selectedBatchId: state.selectedBatchId, setBatchItems: state.setBatchItems, setBatchLoading: state.setBatchLoading, setBatchError: state.setBatchError, }))); const { foundPairs, setFoundPairs, setShowPairingModal, pairingAoiMode, setPairingAoiMode, pairingRegionOptions, setPairingRegionOptions, setPairingRegionSelection, setPairingRegionLoading, setPairingRegionError, setShowPsModal, psAoiMode, setPsAoiMode, psRegionOptions, setPsRegionOptions, setPsRegionSelection, setPsRegionLoading, setPsRegionError, } = usePairingStore(useShallow((state) => ({ foundPairs: state.foundPairs, setFoundPairs: state.setFoundPairs, setShowPairingModal: state.setShowPairingModal, pairingAoiMode: state.pairingAoiMode, setPairingAoiMode: state.setPairingAoiMode, pairingRegionOptions: state.pairingRegionOptions, setPairingRegionOptions: state.setPairingRegionOptions, setPairingRegionSelection: state.setPairingRegionSelection, setPairingRegionLoading: state.setPairingRegionLoading, setPairingRegionError: state.setPairingRegionError, setShowPsModal: state.setShowPsModal, psAoiMode: state.psAoiMode, setPsAoiMode: state.setPsAoiMode, psRegionOptions: state.psRegionOptions, setPsRegionOptions: state.setPsRegionOptions, setPsRegionSelection: state.setPsRegionSelection, setPsRegionLoading: state.setPsRegionLoading, setPsRegionError: state.setPsRegionError, }))); const { hazardPoints, showHazardPoints, setShowHazardPoints, focusedHazardPoint, setFocusedHazardPoint, } = useHazardStore(useShallow((state) => ({ hazardPoints: state.hazardPoints, showHazardPoints: state.showHazardPoints, setShowHazardPoints: state.setShowHazardPoints, focusedHazardPoint: state.focusedHazardPoint, setFocusedHazardPoint: state.setFocusedHazardPoint, }))); const { aoiLayer, setAoiLayer, showMapRegionLocator, setShowMapRegionLocator, mapRegionOptions, setMapRegionOptions, mapRegionSelection, setMapRegionSelection, mapRegionLoading, setMapRegionLoading, mapRegionLocating, setMapRegionLocating, mapRegionError, setMapRegionError, mapRegionLocatedName, setMapRegionLocatedName, } = useMapStore(useShallow((state) => ({ aoiLayer: state.aoiLayer, setAoiLayer: state.setAoiLayer, showMapRegionLocator: state.showMapRegionLocator, setShowMapRegionLocator: state.setShowMapRegionLocator, mapRegionOptions: state.mapRegionOptions, setMapRegionOptions: state.setMapRegionOptions, mapRegionSelection: state.mapRegionSelection, setMapRegionSelection: state.setMapRegionSelection, mapRegionLoading: state.mapRegionLoading, setMapRegionLoading: state.setMapRegionLoading, mapRegionLocating: state.mapRegionLocating, setMapRegionLocating: state.setMapRegionLocating, mapRegionError: state.mapRegionError, setMapRegionError: state.setMapRegionError, mapRegionLocatedName: state.mapRegionLocatedName, setMapRegionLocatedName: state.setMapRegionLocatedName, }))); const mapRef = useRef(null); const tileLayerRef = useRef(null); const activeLayersRef = useRef({}); const radarPreviewLayersRef = useRef({}); const pairLayersRef = useRef({}); const psStackPreviewLayerRef = useRef(null); const psStackPreviewStateRef = useRef({ previousVisibilityById: new Map() }); const hazardLayersGroupRef = useRef(null); const aoeLayerRef = useRef(null); const waterSceneLayersRef = useRef({}); const floodEventLayersRef = useRef({}); // { eventId: { pre, post, classified } } const floodPairPreviewLayerRef = useRef(null); const floodVectorLayersRef = useRef({}); const mapRegionLayerRef = useRef(null); const prevLicenseOkRef = useRef(false); const initializeAppDataRef = useRef(null); const addLogRef = useRef(addLog); const handleTaskCompletionRef = useRef(null); const updateLayerTooltipRef = useRef(null); const radarSearchRequestSeqRef = useRef(0); const licenseFileRef = useRef(null); const foundPairsRef = useRef(foundPairs); const hazardLayersRef = useRef({}); const dinsarResultLayersRef = useRef({}); const sbasAnalysisLayersRef = useRef({}); const resizeStateRef = useRef({ side: null, startX: 0, startLeft: 0, startRight: 0 }); const allDataRef = useRef(allData); const dinsarResultsRef = useRef(dinsarResults); const mapBatchRef = useRef({ frameId: null, token: 0 }); const isAdmin = currentUser?.role === 'admin'; const isReadOnlyUser = !!currentUser && !isAdmin; const isStandaloneLeftPage = FULL_WIDTH_LEFT_TABS.has(leftPanelTab); useEffect(() => { if (isStandaloneLeftPage || !mapRef.current) { return undefined; } const invalidateMap = () => { mapRef.current?.invalidateSize(false); }; const frameId = requestAnimationFrame(invalidateMap); const timeoutId = window.setTimeout(invalidateMap, 120); return () => { cancelAnimationFrame(frameId); window.clearTimeout(timeoutId); }; }, [isStandaloneLeftPage, leftPanelWidth, rightPanelWidth]); const getVisibleLayerRefs = useCallback(() => ({ activeLayersRef: activeLayersRef.current, hazardLayersGroupRef: hazardLayersGroupRef.current, dinsarResultLayersRef: dinsarResultLayersRef.current, sbasAnalysisLayersRef: sbasAnalysisLayersRef.current, waterSceneLayersRef: waterSceneLayersRef.current, radarPreviewLayersRef: radarPreviewLayersRef.current, pairLayersRef: pairLayersRef.current, aoeLayerRef: aoeLayerRef.current, floodEventLayersRef: floodEventLayersRef.current, floodVectorLayersRef: floodVectorLayersRef.current, }), []); const mapExport = useMapExport({ mapRef, getVisibleLayerRefs, addLog, language }); const cancelMapBatch = useCallback(() => { if (mapBatchRef.current.frameId !== null) { cancelAnimationFrame(mapBatchRef.current.frameId); mapBatchRef.current.frameId = null; } mapBatchRef.current.token += 1; }, []); const runMapBatch = useCallback((items, processItem, onComplete) => { cancelMapBatch(); if (!Array.isArray(items) || items.length === 0) { onComplete?.(); return; } const token = mapBatchRef.current.token; let cursor = 0; const flushFrame = () => { if (token !== mapBatchRef.current.token || !mapRef.current) { return; } const frameStart = typeof performance !== 'undefined' ? performance.now() : Date.now(); while (cursor < items.length) { processItem(items[cursor], cursor); cursor += 1; const now = typeof performance !== 'undefined' ? performance.now() : Date.now(); if ((now - frameStart) >= 8) { break; } } if (cursor < items.length) { mapBatchRef.current.frameId = requestAnimationFrame(flushFrame); return; } mapBatchRef.current.frameId = null; onComplete?.(); }; mapBatchRef.current.frameId = requestAnimationFrame(flushFrame); }, [cancelMapBatch]); const ensureCanOperate = useCallback(() => { if (!isAdmin) { addLog('warn', '当前账号为只读用户,无法执行写操作。'); return false; } return true; }, [addLog, isAdmin]); const clearRadarMapLayers = () => { cancelMapBatch(); if (aoeLayerRef.current) { aoeLayerRef.current.remove(); aoeLayerRef.current = null; } if (psStackPreviewLayerRef.current) { psStackPreviewLayerRef.current.remove(); psStackPreviewLayerRef.current = null; } psStackPreviewStateRef.current = { previousVisibilityById: new Map() }; setAoiLayer(null); Object.values(activeLayersRef.current).forEach(layer => layer.remove()); activeLayersRef.current = {}; Object.values(radarPreviewLayersRef.current).forEach(layer => layer.remove()); radarPreviewLayersRef.current = {}; }; const clearSbasAnalysisLayers = useCallback(() => { Object.values(sbasAnalysisLayersRef.current).forEach((entry) => { const layer = entry?.layer || entry; if (layer?.remove) { layer.remove(); } }); sbasAnalysisLayersRef.current = {}; }, []); const clearRadarSearchResults = (options = {}) => { const nextLimit = Math.max( 1, Math.min(Number(options.limit ?? radarPagination.limit ?? DEFAULT_LIST_PAGE_SIZE) || DEFAULT_LIST_PAGE_SIZE, 2000) ); clearRadarMapLayers(); allDataRef.current = []; setAllData([]); setRadarPagination({ total: 0, limit: nextLimit, offset: 0, hasMore: false, }); }; const { fetchRadarImagingDates, fetchRadarSearchOptions, fetchAllData, applyRadarSearch, resetRadarSearch, searchAllRadarData, refreshCurrentRadarSearch, changeSatelliteGroup, } = useRadarSearch({ addLog, setIsLoading, setAllData, setRadarImagingDates, setRadarSearchOptions, setRadarSearchOptionsLoading, radarPagination, setRadarPagination, radarSearchDraft, setRadarSearchDraft, radarSearchApplied, setRadarSearchApplied, radarSearchAoiMode, setRadarSearchAoiMode, setRadarSearchAppliedAoiMode, radarSearchAppliedAoiMode, radarSearchFiles, setRadarSearchFiles, setRadarSearchRegionOptions, radarSearchRegionSelection, setRadarSearchRegionSelection, radarSearchAppliedRegionTreeId, setRadarSearchAppliedRegionTreeId, setRadarSearchRegionError, radarSearchAoiToken, setRadarSearchAoiToken, hasRadarSearched, setHasRadarSearched, selectedSatelliteGroup, setSelectedSatelliteGroup, radarSearchRequestSeqRef, clearRadarSearchResults, clearRadarMapLayers, }); const { refreshBatchList, fetchBatchItems, } = useBatchOperations({ addLog, ensureCanOperate, batchTab, selectedBatchId, setDinsarBatches, setPsBatches, setBatchItems, setBatchLoading, setBatchError, }); useEffect(() => { addLogRef.current = addLog; }, [addLog]); useEffect(() => { allDataRef.current = allData; }, [allData]); useEffect(() => { dinsarResultsRef.current = dinsarResults; }, [dinsarResults]); useEffect(() => { foundPairsRef.current = foundPairs; }, [foundPairs]); useEffect(() => () => { cancelMapBatch(); }, [cancelMapBatch]); const applyPreviewStatus = useCallback((itemId, payload) => { if (!payload) return; setAllData(prev => prev.map(row => ( row.id === itemId ? { ...row, previewStatus: normalizePreviewStatus(payload.status), previewFallbackInUse: !!payload.fallback_in_use, previewHasGeoCache: !!payload.has_geo_cache, previewHasRawCache: !!payload.has_raw_cache, previewSourceFound: !!payload.source_found, previewMessage: payload.message || '', previewError: payload.error || '', previewCacheKey: payload.cache_updated_at || row.previewCacheKey || `${Date.now()}-${itemId}`, } : row ))); }, [setAllData]); const { fetchRegionGeometry, handlePairingAoiModeChange, handlePairingProvinceChange, handlePairingCityChange, toggleMapRegionLocator, handleMapRegionProvinceChange, handleMapRegionCityChange, handleRadarSearchAoiModeChange, handleRadarSearchProvinceChange, handleRadarSearchCityChange, updateRadarSearchDraft, locateSelectedRegionOnMap, clearMapRegionHighlight, openPairingModal, } = useRegionAoiHandlers({ setPairingRegionLoading, setPairingRegionError, setPairingRegionOptions, setPsRegionLoading, setPsRegionError, setPsRegionOptions, setPairingAoiMode, pairingRegionOptions, setPsAoiMode, psRegionOptions, setPairingRegionSelection, setPsRegionSelection, setMapRegionLoading, setMapRegionError, setMapRegionOptions, showMapRegionLocator, setShowMapRegionLocator, mapRegionOptions, setMapRegionSelection, setMapRegionLocatedName, setMapRegionLocating, setRadarSearchRegionLoading, setRadarSearchRegionError, setRadarSearchRegionOptions, setRadarSearchAoiMode, setRadarSearchFiles, setRadarSearchRegionSelection, radarSearchRegionOptions, setRadarSearchDraft, mapRef, mapRegionLayerRef, mapRegionSelection, addLog, pairingAoiMode, setShowPairingModal, psAoiMode, setShowPsModal, }); const { startResize } = usePanelResize({ isResizing, setIsResizing, leftPanelWidth, rightPanelWidth, setLeftPanelWidth, setRightPanelWidth, resizeStateRef, }); const { handleLoginSuccess, handleLogout, fetchLicenseStatus, fetchHealthStatus, handleLicenseUpload, } = useAppAuthLifecycle({ ensureCanOperate, clearRadarSearchResults, radarSearchRequestSeqRef, prevLicenseOkRef, aoeLayerRef, activeLayersRef, radarPreviewLayersRef, setHasRadarSearched, setCurrentUser, setAuthChecked, setPendingTaskIds, setLicenseLoading, setLicenseStatus, setHealthLoading, setHealthError, setHealthStatus, setLicenseFileName, setLicenseUploadStatus, setAoiLayer, setAllData, setRadarPagination, }); // --- Refactored Initialization --- useEffect(() => { if (!currentUser?.id) { return; } let isDisposed = false; addLogRef.current('info', '应用初始化...'); mapRef.current = L.map('map', { zoomControl: false }); // 底图图层将在 baseLayerKey effect 中加载 // 设置地图初始视图以匹配切片范围 if (TILE_LAYER_BOUNDS) { mapRef.current.fitBounds(TILE_LAYER_BOUNDS); } const loadNationalBoundary = async () => { const fetchBoundaryGeoJson = async url => { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }; try { let geojsonData; let loadedFromFallback = false; try { geojsonData = await fetchBoundaryGeoJson(NATIONAL_BOUNDARY_GEOJSON_URL); } catch (primaryError) { console.warn('全国行政区边界远程加载失败,尝试本地静态文件兜底。', primaryError); geojsonData = await fetchBoundaryGeoJson(NATIONAL_BOUNDARY_STATIC_URL); loadedFromFallback = true; } if (isDisposed || !mapRef.current) return; L.geoJSON(geojsonData, { style: { color: "#64748b", weight: 1, opacity: 0.35, fillOpacity: 0 }, interactive: false, }).addTo(mapRef.current); addLogRef.current( 'info', loadedFromFallback ? '全国行政区边界已通过本地静态文件加载。' : '成功加载全国行政区边界。' ); } catch (error) { addLogRef.current('warn', '未能加载全国行政区边界,继续使用切片底图。'); console.error('加载全国行政区边界失败:', error); if (!TILE_LAYER_BOUNDS && mapRef.current) { mapRef.current.setView([35.8617, 104.1954], 5); } } }; void loadNationalBoundary(); L.control.zoom({ position: 'topright' }).addTo(mapRef.current); hazardLayersGroupRef.current = L.layerGroup().addTo(mapRef.current); addLogRef.current('info', '地图初始化完成。'); return () => { isDisposed = true; if (mapRef.current) { if (mapRegionLayerRef.current) { mapRegionLayerRef.current.remove(); mapRegionLayerRef.current = null; } mapRef.current.remove(); mapRef.current = null; } tileLayerRef.current = null; }; }, [currentUser?.id]); useEffect(() => { if (!mapRef.current) return; const config = getBaseLayerConfig(baseLayerKey); const isInitial = !tileLayerRef.current; if (tileLayerRef.current) { mapRef.current.removeLayer(tileLayerRef.current); } try { tileLayerRef.current = L.tileLayer(config.url, { ...TILE_LAYER_OPTIONS, attribution: config.attribution, }).addTo(mapRef.current); addLogRef.current('info', `${isInitial ? '加载' : '切换'}底图:${config.label}`); } catch (e) { addLogRef.current('error', '加载切片底图失败,请检查配置。'); console.error('加载切片底图失败:', e); } }, [baseLayerKey, currentUser?.id]); useEffect(() => { if (licenseLoading || !authChecked) return; const isOk = !!licenseStatus?.ok && !!currentUser; const prevOk = prevLicenseOkRef.current; if (isOk && !prevOk) { initializeAppDataRef.current?.(); } prevLicenseOkRef.current = isOk; }, [licenseLoading, licenseStatus?.ok, authChecked, currentUser]); const { fetchDinsarResults, fetchHazardPoints, handleTaskCompletion, handleLabelResult, handleTrainAi, handlePredictAll, handleAnalyzeResult, handleTaskStart, initializeAppData, } = useDinsarOperations({ onCleanupDinsarLayers: () => { Object.values(dinsarResultLayersRef.current).forEach(layerData => { if (layerData?.layer && mapRef.current?.hasLayer(layerData.layer)) { layerData.layer.remove(); } }); dinsarResultLayersRef.current = {}; }, fetchRadarImagingDates, fetchRadarSearchOptions, fetchAllData, radarSearchRequestSeqRef, }); initializeAppDataRef.current = initializeAppData; const { radarCurrentPage, radarTotalPages, dinsarCurrentPage, dinsarTotalPages, radarPageInputValidationError, dinsarPageInputValidationError, showRadarPageInputError, showDinsarPageInputError, handleRadarPageSizeChange, handleDinsarPageSizeChange, goToRadarPage, goToDinsarPage, changeRadarPage, changeDinsarPage, } = usePaginationControls({ language, hasRadarSearched, addLog, radarPagination, dinsarPagination, radarPageInput, dinsarPageInput, radarPageInputTouched, dinsarPageInputTouched, setRadarPageInput, setRadarPageInputTouched, setDinsarPageInput, setDinsarPageInputTouched, setIsLoading, fetchAllData, fetchDinsarResults, radarSearchRequestSeqRef, }); handleTaskCompletionRef.current = handleTaskCompletion; const { cancelTaskPwd, setCancelTaskPwd, showCancelTask, setShowCancelTask, handleCancelActiveTasks, } = useGlobalTaskControl({ currentUser, licenseOk: !!licenseStatus?.ok, activeTasks, setActiveTasks, pendingTaskIds, setPendingTaskIds, setIsCheckingTasks, handleTaskCompletionRef, }); const fetchRadarPreviewStatus = useCallback(async (itemId, options = {}) => { const { silent = false } = options; try { const response = await apiClient.get(`/radar-data/${itemId}/preview-status`); applyPreviewStatus(itemId, response.data); return response.data; } catch { if (!silent) { addLog('warn', `获取影像预览状态失败 (ID: ${itemId})`); } return null; } }, [addLog, applyPreviewStatus]); const rebuildRadarPreviewCache = useCallback(async (itemId, event) => { if (event) event.stopPropagation(); if (!ensureCanOperate()) return; if (rebuildingPreviewIds[itemId]) return; addLog('info', `正在生成影像 ${itemId} 的预览缓存...`); setRebuildingPreviewIds(prev => ({ ...prev, [itemId]: true })); try { const response = await apiClient.post(`/radar-data/${itemId}/rebuild-preview-cache`); const payload = response.data; applyPreviewStatus(itemId, payload); if (payload?.has_geo_cache) { addLog('success', `影像 ${itemId} 纠正缓存重建完成。`); } else if (payload?.has_raw_cache) { addLog('warn', `影像 ${itemId} 已回退原图缓存,请检查纠正参数。`); } else { addLog('error', `影像 ${itemId} 预览缓存重建失败。`); } } catch { addLog('error', `影像 ${itemId} 预览缓存重建失败。`); } finally { setRebuildingPreviewIds(prev => { const next = { ...prev }; delete next[itemId]; return next; }); } }, [addLog, applyPreviewStatus, ensureCanOperate, handleTaskStart, rebuildingPreviewIds, setRebuildingPreviewIds]); useEffect(() => { if (!mapRef.current || !hazardLayersGroupRef.current) return; hazardLayersGroupRef.current.clearLayers(); hazardLayersRef.current = {}; if (showHazardPoints) { hazardPoints.forEach(point => { const marker = L.circleMarker([point.latitude, point.longitude], { radius: 6, fillColor: "#e53e3e", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.8 }); const hazardName = escapeHtml(point.hazard_name); const hazardId = escapeHtml(point.tybh); const hazardType = escapeHtml(point.hazard_type); const hazardCity = escapeHtml(point.city); const hazardCounty = escapeHtml(point.county); const hazardTownship = escapeHtml(point.township); const hazardLongitude = formatCoordinate(point.longitude); const hazardLatitude = formatCoordinate(point.latitude); marker.bindPopup(`

${hazardName}

编号:${hazardId}
类型:${hazardType}
位置:${hazardCity} ${hazardCounty} ${hazardTownship}
经纬度:${hazardLongitude}, ${hazardLatitude}
`); hazardLayersGroupRef.current.addLayer(marker); hazardLayersRef.current[point.tybh] = marker; }); } }, [hazardPoints, showHazardPoints]); const flyToHazardPoint = (point) => { setFocusedHazardPoint(point); // 切换到 D-InSAR 结果标签页,方便用户查看关联结果 setLeftPanelTab('dinsar_results'); addLog('info', `定位到灾害点: ${point.hazard_name} (${point.tybh}),已自动筛选覆盖该点的 D-InSAR 结果。`); if (mapRef.current) { mapRef.current.flyTo([point.latitude, point.longitude], 14, { duration: 1.5 }); setTimeout(() => { if (hazardLayersRef.current[point.tybh]) { hazardLayersRef.current[point.tybh].openPopup(); } }, 1600); } }; useEffect(() => { if (aoiLayer) { if (aoeLayerRef.current) { aoeLayerRef.current.remove(); } const aoiGeoJsonLayer = L.geoJSON(aoiLayer, { style: { color: '#f6e05e', weight: 2, opacity: 1, dashArray: '5, 5', fill: false, } }).addTo(mapRef.current); aoeLayerRef.current = aoiGeoJsonLayer; mapRef.current.fitBounds(aoiGeoJsonLayer.getBounds(), { padding: [50, 50] }); } }, [aoiLayer]); const restorePsStackPreviewVisibility = () => { const previousVisibilityById = psStackPreviewStateRef.current?.previousVisibilityById; if (!(previousVisibilityById instanceof Map) || previousVisibilityById.size === 0) { psStackPreviewStateRef.current = { previousVisibilityById: new Map() }; return false; } let changed = false; const currentData = allDataRef.current; const restoredData = currentData.map(item => { if (!previousVisibilityById.has(item.id)) { return item; } const shouldBeVisible = previousVisibilityById.get(item.id); if (item.isVisible !== shouldBeVisible) { updateLayerVisibility(item, shouldBeVisible); changed = true; return { ...item, isVisible: shouldBeVisible }; } return item; }); if (changed) { allDataRef.current = restoredData; setAllData(restoredData); } psStackPreviewStateRef.current = { previousVisibilityById: new Map() }; return changed; }; const clearPsStackPreview = ({ silent = false } = {}) => { const hadLayer = !!psStackPreviewLayerRef.current; if (psStackPreviewLayerRef.current) { psStackPreviewLayerRef.current.remove(); psStackPreviewLayerRef.current = null; } const restoredVisibility = restorePsStackPreviewVisibility(); if (silent) { return; } if (!hadLayer && !restoredVisibility) { addLog('info', '当前没有打开的时序候选栈预览范围。'); return; } addLog('info', '已关闭时序候选栈预览范围。'); }; const previewPsStack = (stack) => { cancelMapBatch(); clearPsStackPreview({ silent: true }); const stackIds = new Set(stack.map(img => img.id)); const currentData = allDataRef.current; const previousVisibilityById = new Map(currentData.map(item => [item.id, item.isVisible])); const newAllData = currentData.map(item => { const shouldBeVisible = stackIds.has(item.id); if (item.isVisible !== shouldBeVisible) { updateLayerVisibility(item, shouldBeVisible); return { ...item, isVisible: shouldBeVisible }; } return item; }); allDataRef.current = newAllData; setAllData(newAllData); psStackPreviewStateRef.current = { previousVisibilityById }; if (!mapRef.current) { clearPsStackPreview({ silent: true }); addLog('warn', '地图尚未就绪,无法预览时序候选栈范围。'); return; } const previewGroup = L.layerGroup(); const allLatLngs = []; const palette = ['#f59e0b', '#06b6d4', '#a855f7', '#22c55e', '#ef4444', '#3b82f6']; let validSceneCount = 0; stack.forEach((scene, index) => { const polygon = scene.coverage_polygon; if (!Array.isArray(polygon) || polygon.length < 3) { return; } const latLngs = polygon .filter((point) => Array.isArray(point) && point.length >= 2) .map((point) => [Number(point[1]), Number(point[0])]) .filter(([lat, lon]) => Number.isFinite(lat) && Number.isFinite(lon)); if (latLngs.length < 3) { return; } validSceneCount += 1; allLatLngs.push(...latLngs); const color = palette[index % palette.length]; const scenePolygon = L.polygon(latLngs, { color, weight: 3, opacity: 0.95, fillColor: color, fillOpacity: 0.08, dashArray: scene.stack_selection_mode === 'pairwise_sbas_network' ? '8, 5' : null, }); scenePolygon.bindPopup( `时序候选场景
` + `ID: ${escapeHtml(scene.id)}
` + `日期: ${escapeHtml(scene.imaging_date || '-')}
` + `卫星: ${escapeHtml(scene.satellite || '-')}
` + `模式: ${escapeHtml(scene.imaging_mode || '-')} / ${escapeHtml(scene.polarization || '-')}` ); previewGroup.addLayer(scenePolygon); }); if (validSceneCount === 0 || allLatLngs.length < 3) { clearPsStackPreview({ silent: true }); addLog('warn', `时序候选栈包含 ${stack.length} 个场景,但没有可绘制的覆盖范围。`); return; } previewGroup.addTo(mapRef.current); psStackPreviewLayerRef.current = previewGroup; mapRef.current.fitBounds(L.latLngBounds(allLatLngs), { padding: [50, 50], maxZoom: 10 }); addLog('info', `正在预览包含 ${stack.length} 个场景的时序InSAR候选栈,已绘制 ${validSceneCount} 个覆盖范围。`); }; const updateLayerTooltip = useCallback((layer, result, show) => { if (show) { const dates = parseDatesFromName(result.name, (value) => formatYmd(value, language)); if (dates) { const content = `${dates.master} → ${dates.slave}`; if (layer.getTooltip()) { layer.setTooltipContent(content); if (!layer.isTooltipOpen()) { layer.openTooltip(); } } else { layer.bindTooltip(content, { permanent: true, direction: 'center', className: 'date-tooltip', opacity: 0.9 }).openTooltip(); } } } else { if (layer.getTooltip()) { layer.unbindTooltip(); } } }, [language]); updateLayerTooltipRef.current = updateLayerTooltip; // 当 showDates 改变时,更新所有可见图层的 Tooltip useEffect(() => { Object.entries(dinsarResultLayersRef.current).forEach(([id, layerData]) => { if (layerData && layerData.layer && mapRef.current.hasLayer(layerData.layer)) { const result = dinsarResults.find(r => r.id === parseInt(id)); if (result) { updateLayerTooltipRef.current?.(layerData.layer, result, showDates); } } }); }, [showDates, dinsarResults]); const buildDinsarResultPopupHtml = useCallback((result) => { const dates = parseDatesFromName(result.name, (value) => formatYmd(value, language)); const aiScore = result.ai_score === null || result.ai_score === undefined ? '-' : `${(Number(result.ai_score) * 100).toFixed(0)}%`; const engine = getDinsarEngineMeta(result.engine_code); const strategy = escapeHtml(result.selection_strategy || 'legacy'); const taskAlias = escapeHtml(result.task_alias || result.task_name || result.name || '-'); const pairKey = escapeHtml(result.pair_key || '-'); const pairUid = escapeHtml(result.pair_uid || '-'); const runKey = escapeHtml(result.run_key || '-'); const networkRunId = escapeHtml(result.network_run_id || '-'); const networkEdgeId = escapeHtml(result.network_edge_id ?? '-'); const policyVersion = escapeHtml(result.policy_version || '-'); const dateText = dates ? `${escapeHtml(dates.master)} → ${escapeHtml(dates.slave)}` : '-'; return `
${escapeHtml(result.name || '-') }
任务: ${taskAlias}
日期: ${dateText}
AI: ${aiScore}
引擎: ${escapeHtml(engine.label)} (${escapeHtml(engine.code)})
策略: ${strategy}
edge: ${networkEdgeId}
run: ${networkRunId}
run_key: ${runKey}
pair_key: ${pairKey}
pair_uid: ${pairUid}
policy: ${policyVersion}
`; }, [language]); const updateDinsarLayer = useCallback((result, shouldBeVisible) => { const resultId = result.id; const layerData = dinsarResultLayersRef.current[resultId]; const popupHtml = buildDinsarResultPopupHtml(result); if (shouldBeVisible) { let layer = layerData?.layer; if (layer) { // 如果图层已存在,只需将其添加回地图 if (!mapRef.current.hasLayer(layer)) { // 检查是否已在地图上 layer.addTo(mapRef.current); } layer.bindPopup(popupHtml, { maxWidth: 360 }); } else { // 创建新图层 // 后端已统一接口,不再区分 thumb/full,直接请求即可 const imageUrl = `${apiClient.defaults.baseURL}/dinsar-results/${resultId}/thumb`; addLog('info', `加载结果 ${result.name} 可视化图...`); const bounds = [[result.min_lat, result.min_lon], [result.max_lat, result.max_lon]]; layer = L.imageOverlay(imageUrl, bounds, { opacity: 0.7, interactive: true, }).addTo(mapRef.current); layer.on('load', () => { addLog('success', `结果 ${result.name} 的图像已加载。`); }); layer.on('error', (e) => { addLog('error', `无法加载结果 ${result.name} 的图像。`); console.error(`加载图像失败 (ID: ${resultId}, URL: ${imageUrl})`, e); layer.remove(); // 从ref中清除,以便下次可以重试 delete dinsarResultLayersRef.current[resultId]; }); layer.bindPopup(popupHtml, { maxWidth: 360 }); // 在ref中存储图层对象 dinsarResultLayersRef.current[resultId] = { layer: layer }; } // 确保 Tooltip 状态正确 updateLayerTooltip(layer, result, showDates); } else { // 隐藏图层 if (layerData && layerData.layer && mapRef.current.hasLayer(layerData.layer)) { layerData.layer.remove(); } } }, [addLog, buildDinsarResultPopupHtml, showDates, updateLayerTooltip]); const flyToSbasProduct = useCallback((product) => { if (!mapRef.current || !product) return false; const bounds = getSbasProductBounds(product); if (!bounds) { addLog('warn', '当前 SBAS 产品没有可定位的地理范围。'); return false; } mapRef.current.flyToBounds(L.latLngBounds(bounds), { padding: [45, 45], maxZoom: 12 }); return true; }, [addLog]); const toggleSbasRateLayer = useCallback((detail, shouldBeVisible, opacity = 0.78) => { if (!mapRef.current || !detail) return false; const layerKey = `rate:${detail.id}`; const existing = sbasAnalysisLayersRef.current[layerKey]?.layer; if (!shouldBeVisible) { if (existing) existing.remove(); delete sbasAnalysisLayersRef.current[layerKey]; return true; } const asset = findSbasAsset(detail, ['primary_geocoded_preview', 'primary_rate_color_preview']); const bounds = getSbasProductBounds(detail); if (!asset || !bounds) { addLog('warn', 'SBAS 产品缺少 LOS 速率图或地理范围,无法叠加到地图。'); return false; } if (existing) { if (!mapRef.current.hasLayer(existing)) existing.addTo(mapRef.current); existing.setOpacity(opacity); flyToSbasProduct(detail); return true; } const imageUrl = getSbasInsarProductAssetUrl(detail.id, asset.id, sbasAssetCacheKey(asset)); const layer = L.imageOverlay(imageUrl, bounds, { opacity, interactive: true, crossOrigin: true, }).addTo(mapRef.current); layer.bindPopup( `
${escapeHtml(detail.display_name || detail.run_key || 'Gamma SBAS')}
` + `
图层: LOS 速率图
` + `
色表: ${escapeHtml(detail.color_policy?.colormap || 'Gamma hls.cm')}
` + `
范围: ${escapeHtml((detail.color_policy?.display_range_mm_per_year || [-80, 80]).join(' 到 '))} mm/yr
`, { maxWidth: 340 }, ); layer.on('load', () => addLog('success', `SBAS LOS 速率图已加载:${detail.display_name || detail.run_key || detail.id}`)); layer.on('error', () => { addLog('error', 'SBAS LOS 速率图加载失败。'); layer.remove(); delete sbasAnalysisLayersRef.current[layerKey]; }); sbasAnalysisLayersRef.current[layerKey] = { layer, kind: 'rate', productId: detail.id }; flyToSbasProduct(detail); return true; }, [addLog, flyToSbasProduct]); const updateSbasRateOpacity = useCallback((opacity) => { Object.values(sbasAnalysisLayersRef.current).forEach((entry) => { if (entry?.kind === 'rate' && entry.layer?.setOpacity) { entry.layer.setOpacity(opacity); } }); }, []); const toggleSbasProductOverview = useCallback((products, shouldBeVisible) => { if (!mapRef.current) return false; const layerKey = 'overview'; const existing = sbasAnalysisLayersRef.current[layerKey]?.layer; if (!shouldBeVisible) { if (existing) existing.remove(); delete sbasAnalysisLayersRef.current[layerKey]; return true; } if (existing) { if (!mapRef.current.hasLayer(existing)) existing.addTo(mapRef.current); const existingBounds = sbasAnalysisLayersRef.current[layerKey]?.bounds; if (existingBounds) { mapRef.current.flyToBounds(existingBounds, { padding: [55, 55], maxZoom: 10 }); } return true; } const validProducts = (Array.isArray(products) ? products : []) .map((product) => ({ product, bounds: getSbasProductBounds(product) })) .filter(item => item.bounds); if (!validProducts.length) { addLog('warn', '当前没有可绘制范围的 SBAS 产品。'); return false; } const group = L.layerGroup(); let allBounds = null; validProducts.forEach(({ product, bounds }, index) => { const color = SBAS_OVERVIEW_COLORS[index % SBAS_OVERVIEW_COLORS.length] || '#1d4ed8'; const rectangle = L.rectangle(bounds, { color, weight: 2, opacity: 0.95, fillColor: color, fillOpacity: 0.08, dashArray: index % 2 === 0 ? undefined : '6 4', interactive: true, }); rectangle.bindPopup(buildSbasOverviewPopupHtml(product), { maxWidth: 360 }); rectangle.bindTooltip( `${product.display_name || product.stack_key || product.run_key || product.id}
${String(product.date_start || '-').slice(0, 10)} → ${String(product.date_end || '-').slice(0, 10)}`, { sticky: true, direction: 'top', opacity: 0.92 }, ); rectangle.addTo(group); const nextBounds = L.latLngBounds(bounds); allBounds = allBounds ? allBounds.extend(nextBounds) : nextBounds; }); group.addTo(mapRef.current); sbasAnalysisLayersRef.current[layerKey] = { layer: group, kind: 'overview', bounds: allBounds }; if (allBounds) { mapRef.current.flyToBounds(allBounds, { padding: [55, 55], maxZoom: 10 }); } addLog('info', `已显示 ${validProducts.length} 个 SBAS 产品范围和时间。`); return true; }, [addLog]); const toggleSbasMonitorPoints = useCallback((detail, shouldBeVisible) => { if (!mapRef.current || !detail) return false; const layerKey = `points:${detail.id}`; const existing = sbasAnalysisLayersRef.current[layerKey]?.layer; if (!shouldBeVisible) { if (existing) existing.remove(); delete sbasAnalysisLayersRef.current[layerKey]; return true; } if (existing) { if (!mapRef.current.hasLayer(existing)) existing.addTo(mapRef.current); flyToSbasProduct(detail); return true; } const points = (detail.monitor_points?.monitor_points || []) .filter(point => Number.isFinite(Number(point.lat)) && Number.isFinite(Number(point.lon))); if (!points.length) { addLog('warn', '当前 SBAS 监测点没有 WGS84 坐标,请重新注册资产后再显示。'); return false; } const group = L.layerGroup(); const latLngs = []; points.forEach((point) => { const lat = Number(point.lat); const lon = Number(point.lon); const rate = Number(point.deformation_rate_mm_per_year); const color = sbasRateColor(rate); const marker = L.circleMarker([lat, lon], { radius: 7, color: '#ffffff', weight: 2, fillColor: color, fillOpacity: 0.92, interactive: true, }); marker.bindPopup(buildSbasPointPopupHtml(point), { maxWidth: 320 }); marker.addTo(group); latLngs.push([lat, lon]); }); group.addTo(mapRef.current); sbasAnalysisLayersRef.current[layerKey] = { layer: group, kind: 'points', productId: detail.id }; if (latLngs.length) { mapRef.current.flyToBounds(L.latLngBounds(latLngs), { padding: [55, 55], maxZoom: 13 }); } addLog('info', `已显示 ${points.length} 个 SBAS 监测点。`); return true; }, [addLog, flyToSbasProduct]); const showSbasQueryPoint = useCallback((result, detail) => { if (!mapRef.current || !result?.matched) return false; const matched = result.matched; const lat = Number(matched.lat); const lon = Number(matched.lon); if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false; const layerKey = 'query'; const existing = sbasAnalysisLayersRef.current[layerKey]?.layer; if (existing) existing.remove(); const marker = L.circleMarker([lat, lon], { radius: 8, color: '#111827', weight: 2, fillColor: matched.used_nearest ? '#f59e0b' : '#22c55e', fillOpacity: 0.95, interactive: true, }).addTo(mapRef.current); const point = { point_id: matched.used_nearest ? 'query_nearest' : 'query_point', selection_label: matched.used_nearest ? '查询点最近有效像元' : '查询点', deformation_rate_mm_per_year: matched.los_rate_mm_per_year, displacements: result.displacements || [], matched, lon, lat, }; marker.bindPopup(buildSbasPointPopupHtml(point), { maxWidth: 320 }).openPopup(); sbasAnalysisLayersRef.current[layerKey] = { layer: marker, kind: 'query', productId: detail?.id }; mapRef.current.flyTo([lat, lon], Math.max(mapRef.current.getZoom(), 12), { animate: true }); return true; }, []); const sbasAnalysisPanel = { onToggleRateLayer: toggleSbasRateLayer, onRateOpacityChange: updateSbasRateOpacity, onToggleMonitorPoints: toggleSbasMonitorPoints, onToggleProductOverview: toggleSbasProductOverview, onFlyToProduct: flyToSbasProduct, onShowQueryPoint: showSbasQueryPoint, onClearLayers: clearSbasAnalysisLayers, }; const toggleDinsarResultVisibility = useCallback((resultId) => { cancelMapBatch(); const currentResults = dinsarResultsRef.current; const newResults = [...currentResults]; const resultIndex = newResults.findIndex(r => r.id === resultId); if (resultIndex === -1) return; const result = newResults[resultIndex]; const nextResult = { ...result, isVisible: !result.isVisible }; newResults[resultIndex] = nextResult; dinsarResultsRef.current = newResults; setDinsarResults(newResults); updateDinsarLayer(nextResult, nextResult.isVisible); }, [cancelMapBatch, setDinsarResults, updateDinsarLayer]); const handleSetAllDinsarVisibility = useCallback((shouldBeVisible) => { const currentResults = dinsarResultsRef.current; const currentFilters = { scoreFilter, engineFilter, strategyFilter, traceSearch, focusedHazardPoint, }; const matchedResults = filterDinsarResults(currentResults, currentFilters); const matchedIds = new Set(matchedResults.map((result) => result.id)); const traceKeyword = String(traceSearch || '').trim(); const filterNotes = []; if (scoreFilter > 0) { filterNotes.push(`AI>=${Math.round(scoreFilter * 100)}%`); } if (engineFilter !== DINSAR_ENGINE_ALL) { filterNotes.push(`引擎:${getDinsarEngineMeta(engineFilter).shortLabel}`); } if (focusedHazardPoint?.hazard_name) { filterNotes.push(`点位:${focusedHazardPoint.hazard_name}`); } if (strategyFilter !== DINSAR_STRATEGY_ALL) { filterNotes.push(`策略:${strategyFilter}`); } if (traceKeyword) { filterNotes.push(`trace:${traceKeyword}`); } if (matchedResults.length === 0) { addLog('warn', `当前筛选下没有可${shouldBeVisible ? '显示' : '隐藏'}的 D-InSAR 结果。`); return; } addLog( 'info', `${shouldBeVisible ? '显示' : '隐藏'} ${matchedResults.length} 个 D-InSAR 结果${filterNotes.length > 0 ? `(${filterNotes.join(',')})` : ''}...` ); const changedResults = []; const nextResults = currentResults.map((result) => { const isMatched = matchedIds.has(result.id); const nextVisibleState = shouldBeVisible ? isMatched : (isMatched ? false : result.isVisible); if (result.isVisible === nextVisibleState) { return result; } const nextResult = { ...result, isVisible: nextVisibleState }; changedResults.push(nextResult); return nextResult; }); dinsarResultsRef.current = nextResults; setDinsarResults(nextResults); runMapBatch(changedResults, (result) => { updateDinsarLayer(result, result.isVisible); }); }, [addLog, engineFilter, focusedHazardPoint, runMapBatch, scoreFilter, setDinsarResults, strategyFilter, traceSearch, updateDinsarLayer]); const handleScoreFilterChange = useCallback((e) => { cancelMapBatch(); const newScore = parseFloat(e.target.value); setScoreFilter(newScore); setDinsarResults(prevResults => { const newResults = prevResults.map(result => { if (result.isVisible && result.ai_score !== null && result.ai_score < newScore) { updateDinsarLayer(result, false); return { ...result, isVisible: false }; } return result; }); return newResults; }); }, [cancelMapBatch, setDinsarResults, setScoreFilter, updateDinsarLayer]); const handleShowDataInfo = useCallback((item, event) => { if (event) event.stopPropagation(); setSelectedDataInfo(item); setShowDataInfo(true); }, [setSelectedDataInfo, setShowDataInfo]); const { findPairs, createDinsarBatch, } = usePairingLogic({ fetchRegionGeometry, refreshBatchList, fetchBatchItems, onClearAoiLayer: () => { if (aoeLayerRef.current) { aoeLayerRef.current.remove(); aoeLayerRef.current = null; } }, }); const flyTo = useCallback((item) => { if (mapRef.current && item.coverage_polygon) { mapRef.current.closePopup(); const latLngs = item.coverage_polygon.map(p => [p[1], p[0]]); const bounds = L.latLngBounds(latLngs); mapRef.current.flyToBounds(bounds, { padding: [50, 50], maxZoom: 10 }); if (item.isVisible && activeLayersRef.current[item.id]) { mapRef.current.once('moveend', () => { activeLayersRef.current[item.id].openPopup(); }); } } }, []); const updateLayerVisibility = useCallback((item, shouldBeVisible) => { if (shouldBeVisible) { if (activeLayersRef.current[item.id]) return; const latLngs = item.coverage_polygon.map(p => [p[1], p[0]]); const polygon = L.polygon(latLngs, { color: item.has_orbit_data ? '#48bb78' : '#f56565', weight: 2, }); const originalFilename = escapeHtml(item.file_path.split(/[\\/]/).pop()); const displayName = escapeHtml(item.displayName); const imagingDate = escapeHtml(item.imaging_date); polygon.bindPopup( `原始名称:
${originalFilename}

` + `列表名称: ${displayName}
` + `日期: ${imagingDate}` ); activeLayersRef.current[item.id] = polygon; polygon.addTo(mapRef.current); } else { if (activeLayersRef.current[item.id]) { activeLayersRef.current[item.id].remove(); delete activeLayersRef.current[item.id]; } } }, []); const updateRadarPreviewVisibility = useCallback(async (item, shouldBeVisible) => { if (!item || !mapRef.current) return; const itemId = item.id; const layer = radarPreviewLayersRef.current[itemId]; if (shouldBeVisible) { const refreshedStatus = await fetchRadarPreviewStatus(itemId, { silent: true }); if (refreshedStatus) { item = { ...item, previewStatus: normalizePreviewStatus(refreshedStatus.status), previewFallbackInUse: !!refreshedStatus.fallback_in_use, previewHasGeoCache: !!refreshedStatus.has_geo_cache, previewHasRawCache: !!refreshedStatus.has_raw_cache, previewSourceFound: !!refreshedStatus.source_found, previewMessage: refreshedStatus.message || '', previewError: refreshedStatus.error || '', previewCacheKey: refreshedStatus.cache_updated_at || item.previewCacheKey || `${Date.now()}-${itemId}`, }; } if (layer) { if (!mapRef.current.hasLayer(layer)) { layer.addTo(mapRef.current); } return; } const cacheTag = encodeURIComponent(item.previewCacheKey || `${Date.now()}-${itemId}`); const imageUrl = `${apiClient.defaults.baseURL}/radar-data/${itemId}/thumb?v=${cacheTag}`; const bounds = [[item.min_lat, item.min_lon], [item.max_lat, item.max_lon]]; const previewLayer = L.imageOverlay(imageUrl, bounds, { opacity: 0.78, crossOrigin: true, }); previewLayer.on('error', () => { addLog('warn', `未找到 ${item.displayName} 的源影像缓存(或缓存生成失败)。`); if (radarPreviewLayersRef.current[itemId]) { radarPreviewLayersRef.current[itemId].remove(); delete radarPreviewLayersRef.current[itemId]; } setAllData(prev => prev.map(row => ( row.id === itemId ? { ...row, isPreviewVisible: false } : row ))); fetchRadarPreviewStatus(itemId, { silent: true }); }); radarPreviewLayersRef.current[itemId] = previewLayer; previewLayer.addTo(mapRef.current); } else if (layer) { layer.remove(); delete radarPreviewLayersRef.current[itemId]; } }, [addLog, fetchRadarPreviewStatus, setAllData]); const toggleLayerVisibility = useCallback((itemId) => { cancelMapBatch(); const currentData = allDataRef.current; const itemIndex = currentData.findIndex(d => d.id === itemId); if (itemIndex === -1) return; const newAllData = [...currentData]; const item = { ...newAllData[itemIndex] }; item.isVisible = !item.isVisible; newAllData[itemIndex] = item; allDataRef.current = newAllData; setAllData(newAllData); updateLayerVisibility(item, item.isVisible); }, [cancelMapBatch, setAllData, updateLayerVisibility]); const toggleRadarPreviewVisibility = useCallback((itemId) => { cancelMapBatch(); const currentData = allDataRef.current; const itemIndex = currentData.findIndex(d => d.id === itemId); if (itemIndex === -1) return; const newAllData = [...currentData]; const item = { ...newAllData[itemIndex] }; item.isPreviewVisible = !item.isPreviewVisible; newAllData[itemIndex] = item; allDataRef.current = newAllData; setAllData(newAllData); updateRadarPreviewVisibility(item, item.isPreviewVisible); }, [cancelMapBatch, setAllData, updateRadarPreviewVisibility]); const handleSelectAllVisibility = useCallback((e) => { const shouldBeVisible = e.target.checked; const currentData = allDataRef.current; const changedItems = []; const newAllData = currentData.map(item => { if (item.isVisible === shouldBeVisible) { return item; } const nextItem = { ...item, isVisible: shouldBeVisible }; changedItems.push(nextItem); return nextItem; }); allDataRef.current = newAllData; setAllData(newAllData); runMapBatch(changedItems, (item) => { updateLayerVisibility(item, item.isVisible); }); }, [runMapBatch, setAllData, updateLayerVisibility]); const handleSetAllRadarPreviewVisibility = useCallback((shouldBeVisible) => { const currentData = allDataRef.current; const changedItems = []; const newAllData = currentData.map(item => { if (item.isPreviewVisible === shouldBeVisible) { return item; } const nextItem = { ...item, isPreviewVisible: shouldBeVisible }; changedItems.push(nextItem); return nextItem; }); allDataRef.current = newAllData; setAllData(newAllData); runMapBatch(changedItems, (item) => { updateRadarPreviewVisibility(item, item.isPreviewVisible); }); }, [runMapBatch, setAllData, updateRadarPreviewVisibility]); const handleWaterSceneOnMap = (scene) => { if (!mapRef.current || !scene.coverage_polygon) return; const layerKey = scene.radar_data_id ? `scene_${scene.id}` : `radar_${scene.id}`; if (waterSceneLayersRef.current[layerKey]) { waterSceneLayersRef.current[layerKey].remove(); delete waterSceneLayersRef.current[layerKey]; return false; } const latLngs = scene.coverage_polygon.map(p => [p[1], p[0]]); const polygon = L.polygon(latLngs, { color: '#00e5cc', weight: 3, fillColor: '#00e5cc', fillOpacity: 0.12, dashArray: '6, 4', }); polygon.bindPopup( `水体场景 ID=${scene.id}
` + `${scene.satellite || ''} · ${scene.imaging_date || ''}
` + `${(scene.geo_path || '').split(/[\\/]/).pop()}` ); waterSceneLayersRef.current[layerKey] = polygon; polygon.addTo(mapRef.current); mapRef.current.flyToBounds(L.latLngBounds(latLngs), { padding: [50, 50], maxZoom: 10 }); polygon.openPopup(); return true; }; const handleFloodSourcePreviewOnMap = (item) => { if (!mapRef.current || !item) return; const enriched = { ...item, displayName: item.displayName || item.file_path?.split(/[\\/]/).pop() || `${item.satellite || 'SAR'} #${item.id}`, previewCacheKey: item.previewCacheKey || item.preview_cache_updated_at || `${Date.now()}-${item.id}`, }; if (radarPreviewLayersRef.current[enriched.id]) { updateRadarPreviewVisibility(enriched, false); return false; } updateRadarPreviewVisibility(enriched, true); if ( enriched.min_lat != null && enriched.max_lat != null && enriched.min_lon != null && enriched.max_lon != null ) { mapRef.current.flyToBounds( L.latLngBounds([[enriched.min_lat, enriched.min_lon], [enriched.max_lat, enriched.max_lon]]), { padding: [50, 50], maxZoom: 10 }, ); } return true; }; // 洪涝事件三图层叠加(灾前/灾后/分类结果),每层可独立开关 const handleFloodEventOnMap = (ev, layers) => { if (!mapRef.current) return; const evId = ev.id; // 清除该事件已有图层 const existing = floodEventLayersRef.current[evId]; if (existing) { Object.values(existing).forEach(l => l && l.remove()); } const created = {}; let allBounds = null; const addImageLayer = (key, data, opacity) => { if (!data || !data.image_b64 || !data.bounds) return; const [minLat, minLon, maxLat, maxLon] = data.bounds; const bounds = [[minLat, minLon], [maxLat, maxLon]]; const url = `data:image/png;base64,${data.image_b64}`; const layer = L.imageOverlay(url, bounds, { opacity, interactive: false }); layer.addTo(mapRef.current); created[key] = layer; allBounds = allBounds ? allBounds.extend(bounds) : L.latLngBounds(bounds); }; addImageLayer('pre', layers.pre, 0.75); addImageLayer('post', layers.post, 0.75); addImageLayer('classified', layers.classified, 0.85); floodEventLayersRef.current[evId] = created; if (allBounds) { mapRef.current.flyToBounds(allBounds, { padding: [40, 40], maxZoom: 12 }); } }; // 切换洪涝事件单个图层可见性 const toggleFloodEventLayer = (evId, key, visible) => { const layers = floodEventLayersRef.current[evId]; if (!layers || !layers[key]) return; if (visible) layers[key].addTo(mapRef.current); else layers[key].remove(); }; const handleFloodPairPreviewOnMap = (pair) => { if (!mapRef.current || !pair?.pre?.coverage_polygon || !pair?.post?.coverage_polygon) return; if (floodPairPreviewLayerRef.current) { floodPairPreviewLayerRef.current.remove(); floodPairPreviewLayerRef.current = null; } const makePolygon = (scene, color, label) => { const latLngs = scene.coverage_polygon.map(p => [p[1], p[0]]); const polygon = L.polygon(latLngs, { color, weight: 2, fillColor: color, fillOpacity: 0.14, }); polygon.bindPopup( `${escapeHtml(label)}
` + `场景 #${escapeHtml(scene.id)} · ${escapeHtml(scene.satellite || '')}
` + `${escapeHtml(formatYmd(scene.imaging_date, language))} · ${escapeHtml(scene.polarization || '')}` ); return polygon; }; const prePolygon = makePolygon(pair.pre, '#2563eb', '灾前覆盖'); const postPolygon = makePolygon(pair.post, '#16a34a', '灾后覆盖'); const group = L.featureGroup([prePolygon, postPolygon]); floodPairPreviewLayerRef.current = group; group.addTo(mapRef.current); mapRef.current.flyToBounds(group.getBounds(), { padding: [50, 50], maxZoom: 10 }); }; const handleFloodVectorOnMap = (impact) => { if (!mapRef.current || !impact?.flood_vector_geojson) return; const layerId = impact.overlay_id || impact.detection_id || 'current'; const existing = floodVectorLayersRef.current[layerId]; if (existing) existing.remove(); const layer = L.geoJSON(impact.flood_vector_geojson, { style: { color: '#dc2626', weight: 2, fillColor: '#ef4444', fillOpacity: 0.26, }, onEachFeature: (feature, featureLayer) => { const props = feature?.properties || {}; featureLayer.bindPopup( `洪涝矢量
` + `Overlay #${escapeHtml(layerId)}
` + `${escapeHtml(props.name || props.class || '')}` ); }, }); floodVectorLayersRef.current[layerId] = layer; layer.addTo(mapRef.current); const bounds = layer.getBounds(); if (bounds.isValid()) { mapRef.current.flyToBounds(bounds, { padding: [50, 50], maxZoom: 12 }); } }; const visualizePair = useCallback((pair) => { const masterLatLngs = pair.master.coverage_polygon.map(p => [p[1], p[0]]); const slaveLatLngs = pair.slave.coverage_polygon.map(p => [p[1], p[0]]); const groupBounds = L.latLngBounds(masterLatLngs).extend(slaveLatLngs); mapRef.current.flyToBounds(groupBounds, { padding: [50, 50], maxZoom: 10 }); }, []); const togglePairVisibility = useCallback((pairIndex) => { const currentPairs = foundPairsRef.current; const pair = currentPairs[pairIndex]; if (!pair) return; const nextPair = { ...pair, isVis: !pair.isVis }; const newFoundPairs = [...currentPairs]; newFoundPairs[pairIndex] = nextPair; foundPairsRef.current = newFoundPairs; setFoundPairs(newFoundPairs); const pairId = nextPair.task_name; if (nextPair.isVis) { if (pairLayersRef.current[pairId]) { pairLayersRef.current[pairId].addTo(mapRef.current); } else { const masterLatLngs = nextPair.master.coverage_polygon.map(p => [p[1], p[0]]); const slaveLatLngs = nextPair.slave.coverage_polygon.map(p => [p[1], p[0]]); const masterPolygon = L.polygon(masterLatLngs, { color: '#3498db', weight: 2, fillOpacity: 0.2 }); const slavePolygon = L.polygon(slaveLatLngs, { color: '#2ecc71', weight: 2, fillOpacity: 0.2 }); masterPolygon.bindPopup(`Master: ${escapeHtml(nextPair.master.id)}`); slavePolygon.bindPopup(`Slave: ${escapeHtml(nextPair.slave.id)}`); const group = L.featureGroup([masterPolygon, slavePolygon]); pairLayersRef.current[pairId] = group; group.addTo(mapRef.current); } } else { if (pairLayersRef.current[pairId]) { pairLayersRef.current[pairId].remove(); } } }, [setFoundPairs]); const selectedPairsCount = foundPairs.filter(p => p.isSelected).length; const hasEnoughRadarScenesForPlanning = radarImagingDates.length >= 2; const licenseOk = !!licenseStatus?.ok; const avgTaskProgress = activeTasks.length ? Math.round(activeTasks.reduce((sum, t) => sum + (t.progress || 0), 0) / activeTasks.length) : 0; const handleRefreshHealth = useCallback(() => { fetchHealthStatus({ refresh: true }); }, [fetchHealthStatus]); const openStatisticsDashboard = useCallback(() => { setShowStats(true); }, [setShowStats]); const refreshDinsarResults = useCallback(() => { fetchDinsarResults({ offset: 0 }); }, [fetchDinsarResults]); const handleCloseCancelTask = useCallback(() => { setShowCancelTask(false); setCancelTaskPwd(''); }, [setShowCancelTask, setCancelTaskPwd]); const radarPanel = { radarCurrentPage, radarTotalPages, showRadarPageInputError, radarPageInputValidationError, onSearchAll: searchAllRadarData, onShowStats: openStatisticsDashboard, onSearch: applyRadarSearch, onReset: resetRadarSearch, onAoiModeChange: handleRadarSearchAoiModeChange, onProvinceChange: handleRadarSearchProvinceChange, onCityChange: handleRadarSearchCityChange, onSetRadarSearchFiles: setRadarSearchFiles, updateDraft: updateRadarSearchDraft, onPageChange: changeRadarPage, onPageSizeChange: handleRadarPageSizeChange, onGoToPage: goToRadarPage, onSelectAllVisibility: handleSelectAllVisibility, onSetAllPreviewVisibility: handleSetAllRadarPreviewVisibility, onToggleLayer: toggleLayerVisibility, onTogglePreview: toggleRadarPreviewVisibility, onRebuildPreview: rebuildRadarPreviewCache, onShowDataInfo: handleShowDataInfo, onFlyTo: flyTo, onChangeSatelliteGroup: changeSatelliteGroup, }; const pairingPanel = { onOpenPairingModal: openPairingModal, onRefreshRadarSearch: refreshCurrentRadarSearch, onRefreshDinsar: refreshDinsarResults, }; const taskPanel = { onTaskStart: handleTaskStart, }; const hazardPanel = { onPointClick: flyToHazardPoint, onToggleVisibility: setShowHazardPoints, onScanComplete: fetchHazardPoints, }; const floodPanel = { onShowSourceSceneOnMap: handleWaterSceneOnMap, onShowSourcePreviewOnMap: handleFloodSourcePreviewOnMap, onShowReadyProductOnMap: handleWaterSceneOnMap, onShowOnMap: handleWaterSceneOnMap, onShowFloodOnMap: handleFloodEventOnMap, onShowFloodRunOnMap: handleFloodEventOnMap, onToggleFloodLayer: toggleFloodEventLayer, onShowFloodPairOnMap: handleFloodPairPreviewOnMap, onShowFloodVectorOnMap: handleFloodVectorOnMap, }; const dinsarPanel = { dinsarCurrentPage, dinsarTotalPages, showDinsarPageInputError, dinsarPageInputValidationError, onSetAllVisibility: handleSetAllDinsarVisibility, onScoreFilterChange: handleScoreFilterChange, onPageChange: changeDinsarPage, onPageSizeChange: handleDinsarPageSizeChange, onGoToPage: goToDinsarPage, onToggleVisibility: toggleDinsarResultVisibility, onLabel: handleLabelResult, onAnalyze: handleAnalyzeResult, }; const aiPanel = { onTrain: handleTrainAi, onPredictAll: handlePredictAll, }; const pairsPanel = { onVisualizePair: visualizePair, onTogglePairVisibility: togglePairVisibility, onCreateDinsarBatch: createDinsarBatch, }; if (!authChecked) { return (

正在检查登录状态...

请稍候,系统正在验证会话。

); } if (!currentUser) { return ; } return (
startResize('left', event)} style={{ display: isStandaloneLeftPage ? 'none' : undefined }} />
startResize('right', event)} style={{ display: isStandaloneLeftPage ? 'none' : undefined }} />
setShowCancelTask(true)} onCancelTaskPwdChange={setCancelTaskPwd} onCancelTaskConfirm={handleCancelActiveTasks} onCloseCancelTask={handleCloseCancelTask} mapExport={mapExport} />
); } export default App;