From 8d17118b4089af0c2088d00159294807036723fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=88=E5=85=86=E5=A2=9E?= <你的邮箱@example.com> Date: Fri, 10 Jul 2026 17:56:16 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=B8=89=E7=BB=B4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/gis/GisView.vue | 8 +- frontend/src/components/gis/map.cesium.ts | 1045 ++++++++++++++++- frontend/src/components/gis/map.class.ts | 4 + frontend/src/components/gis/map.d.ts | 6 + frontend/src/components/gis/map.ol.ts | 114 +- .../components/gis/ol/point-layer-manager.ts | 2 +- .../src/components/gis/ol/popup-manager.ts | 127 +- frontend/src/components/mapFilter/index.vue | 51 +- frontend/src/modules/jidiSelectorMod.vue | 4 +- .../map/application/map-orchestrator.ts | 290 ++++- frontend/src/store/modules/ui.ts | 9 +- frontend/src/utils/popupHtmlGenerator.ts | 39 +- frontend/src/utils/request.ts | 1 - 13 files changed, 1566 insertions(+), 134 deletions(-) diff --git a/frontend/src/components/gis/GisView.vue b/frontend/src/components/gis/GisView.vue index 60ea9a73..57f2525f 100644 --- a/frontend/src/components/gis/GisView.vue +++ b/frontend/src/components/gis/GisView.vue @@ -32,8 +32,10 @@ import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator'; import { MapClass } from './map.class'; import { getQgcRvcd } from '@/api/map'; import { servers } from './mapurlManage'; +import { useUiStore } from '@/store/modules/ui'; const mapOrchestrator = useMapOrchestrator(); +const uiStore = useUiStore(); const route = useRoute(); const mapClass = MapClass.getInstance(); @@ -82,7 +84,11 @@ const init = async () => { const handleMapController = async (e: any, mapType: any) => { switch (e) { case 'dim': - await mapClass.switchView(mapType); + await mapOrchestrator.switchMapType(mapType, { + popupContainer: popupRef.value, + getIsHydroMenu: () => isShuiDianKaiFaMenu.value + }); + uiStore.markMapSwitchCompleted(); break; case 4: // 梯级流域显示 tlyLayerVisible.value = !tlyLayerVisible.value; diff --git a/frontend/src/components/gis/map.cesium.ts b/frontend/src/components/gis/map.cesium.ts index 684b4b85..aaac37c4 100644 --- a/frontend/src/components/gis/map.cesium.ts +++ b/frontend/src/components/gis/map.cesium.ts @@ -1,6 +1,12 @@ -// src/components/gis/map.cesium.ts +// src/components/gis/map.cesium.ts import * as Cesium from 'cesium'; +import { getIconPath } from '@/utils'; +import { generatePopupHtml } from '@/utils/popupHtmlGenerator'; +import type { MDOptions } from './map.class'; import type { MapInterface, layer } from './map.d'; +import { useModelStore } from '@/store/modules/model'; + +const modelStore = useModelStore(); // ✅ 1. 全局配置 Cesium 静态资源路径 (vite-plugin-cesium 通常会自动处理,但显式指定更保险) // 注意:如果你使用了 vite-plugin-cesium,这一步通常不需要,但如果报错,可以尝试取消注释下面这行 @@ -13,6 +19,26 @@ export class MapCesium implements MapInterface { private loadingOverlayElement: HTMLDivElement | null = null; private loadingSpinnerAnimation: Animation | null = null; private flightFallbackTimer: number | null = null; + private imageryBaseLayer: Cesium.ImageryLayer | null = null; + private baseLayerRegistry = new Map(); + private baseLayerAliasMap = new Map(); + private pointLayerRegistry = new Map(); + private pointLegendVisibility = new Map>(); + private pointEntityIndex = new Map>(); + private popupElement: HTMLDivElement | null = null; + private hoveredEntityId: string | null = null; + private removeCameraChangedListener: (() => void) | null = null; + private readonly IGNORED_BASE_LAYER_KEYS = new Set([ + 'hydropBase', + 'powerBaseStation', + 's_hydropBase_wfs' + ]); + private readonly PRIMARY_BASE_LAYER_KEYS = new Set([ + 'customBaseLayer', + 's_province_boundaries', + 'BASEMAP-white', + 'BASEMAP-img' + ]); private readonly CHINA_CENTER = { lng: 114.5, @@ -20,11 +46,16 @@ export class MapCesium implements MapInterface { }; private readonly CHINA_BOOT_HEIGHT = 20000000; private readonly CHINA_DEFAULT_HEIGHT = 12000000; + private readonly HOVER_POPUP_MAX_HEIGHT = 9000000; private readonly CHINA_VIEW_ORIENTATION = { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0 }; + private readonly CESIUM_ZOOM_FORMULA_A = 40487.57; + private readonly CESIUM_ZOOM_FORMULA_B = 0.00007096758; + private readonly CESIUM_ZOOM_FORMULA_C = 91610.74; + private readonly CESIUM_ZOOM_FORMULA_D = -40467.74; async init(container: HTMLElement, _rectangle?: any): Promise { try { @@ -59,10 +90,14 @@ export class MapCesium implements MapInterface { maximumLevel: 19 } ); - layers.addImageryProvider(provider); + this.imageryBaseLayer = layers.addImageryProvider(provider); + this.imageryBaseLayer.show = true; this.viewer.scene.globe.depthTestAgainstTerrain = false; this.viewer.useBrowserRecommendedResolution = true; + this.bindHoverPopupHandler(); + this.bindPointClickHandler(); + this.bindCameraPopupReset(); // 先把相机直接摆到中国上空,避免初始化阶段短暂闪到美国默认视角。 this.setBootChinaView(); @@ -111,15 +146,12 @@ export class MapCesium implements MapInterface { } const finish = () => { - if (this.flightFallbackTimer !== null) { - window.clearTimeout(this.flightFallbackTimer); - this.flightFallbackTimer = null; - } + this.clearFlightFallbackTimer(); this.hideLoadingOverlay(); resolve(); }; - this.viewer.camera.cancelFlight(); + this.clearFlightFallbackTimer(); this.flightFallbackTimer = window.setTimeout(() => { finish(); }, 2600); @@ -138,6 +170,760 @@ export class MapCesium implements MapInterface { }); } + private clearFlightFallbackTimer() { + if (this.flightFallbackTimer !== null) { + window.clearTimeout(this.flightFallbackTimer); + this.flightFallbackTimer = null; + } + } + + private getRegistryKeys(layerConfig: any): string[] { + const keys = [layerConfig?.key, layerConfig?.id].filter( + (key): key is string => typeof key === 'string' && key.length > 0 + ); + return Array.from(new Set(keys)); + } + + private resolvePrimaryBaseLayerKey(layerKey: string): string | undefined { + if (!layerKey) return undefined; + if (this.baseLayerRegistry.has(layerKey)) { + return layerKey; + } + return this.baseLayerAliasMap.get(layerKey); + } + + private unregisterBaseLayer(layerConfig: any) { + if (!this.viewer) return; + + const registryKeys = this.getRegistryKeys(layerConfig); + const uniquePrimaryKeys = new Set(); + + registryKeys.forEach(key => { + const primaryKey = this.resolvePrimaryBaseLayerKey(key) || key; + uniquePrimaryKeys.add(primaryKey); + this.baseLayerAliasMap.delete(key); + }); + + uniquePrimaryKeys.forEach(primaryKey => { + const existingLayer = this.baseLayerRegistry.get(primaryKey); + if (existingLayer) { + this.viewer?.imageryLayers.remove(existingLayer, true); + this.baseLayerRegistry.delete(primaryKey); + } + }); + } + + private registerBaseLayer( + layerConfig: any, + imageryLayer: Cesium.ImageryLayer + ) { + const registryKeys = this.getRegistryKeys(layerConfig); + const primaryKey = layerConfig?.key || layerConfig?.id; + + if (!primaryKey) { + return; + } + + this.baseLayerRegistry.set(primaryKey, imageryLayer); + registryKeys.forEach(key => { + this.baseLayerAliasMap.set(key, primaryKey); + }); + } + + private getLayerRequestUrl(layerConfig: any): string { + return layerConfig?.url_3d || layerConfig?.url || ''; + } + + private createImageryLayer(layerConfig: any, visible: boolean) { + if (!this.viewer) { + return null; + } + + const url = this.getLayerRequestUrl(layerConfig); + if (!url) { + return null; + } + + let imageryLayer: Cesium.ImageryLayer | null = null; + + if (layerConfig?.type === 'raster-dem' || /\{x\}|\{y\}|\{z\}/.test(url)) { + imageryLayer = this.viewer.imageryLayers.addImageryProvider( + new Cesium.UrlTemplateImageryProvider({ + url + }) + ); + } else if (url.includes('MapServer')) { + imageryLayer = this.viewer.imageryLayers.add( + Cesium.ImageryLayer.fromProviderAsync( + Cesium.ArcGisMapServerImageryProvider.fromUrl(url, { + enablePickFeatures: false, + maximumLevel: 19 + }) + ) + ); + } else { + const cleanUrl = url.split('?')[0]; + const layers = layerConfig?.layers || ''; + imageryLayer = this.viewer.imageryLayers.addImageryProvider( + new Cesium.WebMapServiceImageryProvider({ + url: cleanUrl, + layers, + parameters: { + service: 'WMS', + transparent: 'true', + format: 'image/png' + } + }) + ); + } + + if (!imageryLayer) { + return null; + } + + imageryLayer.show = visible; + if (typeof layerConfig?.rasteropacity === 'number') { + imageryLayer.alpha = layerConfig.rasteropacity; + } + return imageryLayer; + } + + private getOrCreatePointLayer(layerKey: string) { + if (!this.viewer || !layerKey) { + return null; + } + + let dataSource = this.pointLayerRegistry.get(layerKey); + if (dataSource) { + return dataSource; + } + + dataSource = new Cesium.CustomDataSource(layerKey); + this.viewer.dataSources.add(dataSource); + this.pointLayerRegistry.set(layerKey, dataSource); + if (!this.pointLegendVisibility.has(layerKey)) { + this.pointLegendVisibility.set(layerKey, new Map()); + } + this.pointEntityIndex.set(layerKey, new Map()); + return dataSource; + } + + private resetPointLayerIndexes(layerKey: string) { + this.pointEntityIndex.set(layerKey, new Map()); + } + + private normalizeLegendState(value?: string) { + const normalizedValue = String(value || '').trim(); + if (!normalizedValue) { + return ''; + } + + const withoutTestSuffix = normalizedValue.includes('_测试') + ? normalizedValue.split('_测试')[0] + : normalizedValue; + + return withoutTestSuffix.startsWith('large_eng_built_alarm_range_') + ? withoutTestSuffix.replace('large_eng_built_', '') + : withoutTestSuffix; + } + + private getLayerLegendVisibility(layerKey: string, legendState: string) { + const layerStateMap = this.pointLegendVisibility.get(layerKey); + if (!layerStateMap) { + return true; + } + + const normalizedLegendState = + this.normalizeLegendState(legendState) || '__default__'; + if (layerStateMap.has(normalizedLegendState)) { + return layerStateMap.get(normalizedLegendState) ?? true; + } + + if (layerStateMap.has('__default__')) { + return layerStateMap.get('__default__') ?? true; + } + return true; + } + + private indexPointEntity( + layerKey: string, + legendState: string, + entity: Cesium.Entity + ) { + const layerIndex = this.pointEntityIndex.get(layerKey) || new Map(); + const normalizedLegendState = + this.normalizeLegendState(legendState) || '__default__'; + const entities = layerIndex.get(normalizedLegendState) || []; + entities.push(entity); + layerIndex.set(normalizedLegendState, entities); + this.pointEntityIndex.set(layerKey, layerIndex); + } + + private normalizePopupSttpMap(item: any, layerKey: string) { + if (item?.popupSttpMap) { + return item.popupSttpMap; + } + if (item?._sttpMap) { + return item._sttpMap; + } + if (item?.sttpMap) { + return item.sttpMap; + } + + const normalizedLayerKey = String(layerKey || '').toLowerCase(); + const sttpCode = String(item?.sttpCode || item?.sttp || '').toUpperCase(); + + if (normalizedLayerKey.includes('ylfb')) { + return 'ylfb'; + } + if (normalizedLayerKey.includes('fh_zq_point') || sttpCode === 'ZQ') { + return 'ZQ'; + } + if (normalizedLayerKey.includes('fh_wtrv_point') || sttpCode === 'WTRV') { + return 'eng3'; + } + if (normalizedLayerKey.includes('ai_video') || sttpCode === 'AIVD') { + return 'AIVD'; + } + if (normalizedLayerKey.includes('video') || sttpCode === 'VD') { + return 'VD'; + } + if (normalizedLayerKey.includes('wq') || sttpCode === 'WQ') { + return 'WQ'; + } + + return ''; + } + + private buildFallbackPopupHtml(props: any) { + const title = + props?.ennm || props?.stnm || props?.titleName || props?.ftp || ''; + if (!title) { + return ''; + } + + return ` +
+
+
${title}
+
+
+ `; + } + + private buildPointPopupProps(item: any, layerKey: string) { + const normalizedSttpMap = this.normalizePopupSttpMap(item, layerKey); + return { + ...item, + sttpMap: normalizedSttpMap, + _sttpMap: normalizedSttpMap, + popupSttpMap: normalizedSttpMap + }; + } + + private createEntityPropertiesBag( + props: Record, + layerKey: string, + legendVisible: boolean + ) { + const propertyBag = new Cesium.PropertyBag(); + + Object.entries(props).forEach(([key, value]) => { + propertyBag.addProperty(key); + propertyBag[key] = value; + }); + + propertyBag.addProperty('_layerKey'); + propertyBag._layerKey = layerKey; + propertyBag.addProperty('_legendVisible'); + propertyBag._legendVisible = legendVisible; + + return propertyBag; + } + + private createPointEntity( + item: any, + layerKey: string, + entityIndex: number + ): Cesium.Entity | null { + const lon = Number(item?.lgtd); + const lat = Number(item?.lttd); + + if ( + !isFinite(lon) || + !isFinite(lat) || + Math.abs(lon) > 180 || + Math.abs(lat) > 90 + ) { + return null; + } + + const iconUrl = getIconPath(item?.iconCode || 'default') || ''; + const popupProps = this.buildPointPopupProps(item, layerKey); + const normalizedSttpMap = popupProps.sttpMap; + const labelText = this.formatPointLabelText( + normalizedSttpMap === 'ylfb' + ? item?.ftp || '' + : item?.titleName || item?.stnm || item?.ennm || '' + ); + const legendState = this.normalizeLegendState( + item?.anchoPointState ? String(item.anchoPointState) : '' + ); + const id = + item?.stcd || item?._id || `${layerKey}-${entityIndex}-${lon}-${lat}`; + + const entity = new Cesium.Entity({ + id: `${layerKey}:${id}`, + position: Cesium.Cartesian3.fromDegrees(lon, lat), + properties: this.createEntityPropertiesBag( + popupProps, + layerKey, + this.getLayerLegendVisibility(layerKey, legendState) + ), + billboard: iconUrl + ? { + image: iconUrl, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + scale: 1 + } + : undefined, + label: labelText + ? { + text: labelText, + font: '12px sans-serif', + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.fromCssColorString('#16324f'), + outlineWidth: 3, + pixelOffset: new Cesium.Cartesian2(0, -26), + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + : undefined + }); + + (entity as any).__popupSource = popupProps; + entity.show = this.getLayerLegendVisibility(layerKey, legendState); + this.indexPointEntity(layerKey, legendState, entity); + return entity; + } + + private requestRender() { + this.viewer?.scene.requestRender(); + } + + private setEntityLegendVisible(entity: Cesium.Entity, visible: boolean) { + entity.show = visible; + + if ( + this.viewer && + typeof entity.properties?._legendVisible?.setValue === 'function' + ) { + entity.properties._legendVisible.setValue(visible); + } + } + + private applyLayerLegendVisibility( + layerKey: string, + dataSource?: Cesium.CustomDataSource | null + ) { + const targetDataSource = + dataSource || this.pointLayerRegistry.get(layerKey); + if (!targetDataSource) { + return; + } + + targetDataSource.entities.values.forEach(entity => { + const legendState = this.getEntityLegendState(entity); + this.setEntityLegendVisible( + entity, + this.getLayerLegendVisibility(layerKey, legendState) + ); + }); + } + + private formatPointLabelText(text: string): string { + const normalizedText = String(text || '') + .replace(/[()()]/g, '') + .trim(); + if (!normalizedText) { + return ''; + } + + const maxLineLength = 12; + if (normalizedText.length <= maxLineLength) { + return normalizedText; + } + + const firstLine = normalizedText.slice(0, maxLineLength); + if (normalizedText.length <= maxLineLength * 2) { + return `${firstLine}\n${normalizedText.slice(maxLineLength)}`; + } + + const secondLine = `${normalizedText.slice( + maxLineLength, + maxLineLength + 9 + )}...`; + return `${firstLine}\n${secondLine}`; + } + + private getEntityRuntimeProps(entity: Cesium.Entity) { + if (!this.viewer) { + return {} as Record; + } + + return ( + entity.properties?.getValue?.(this.viewer.clock.currentTime) || + ({} as Record) + ); + } + + private getEntityLabelText(entity: Cesium.Entity) { + if (!this.viewer) { + return ''; + } + + const labelText = entity.label?.text?.getValue?.( + this.viewer.clock.currentTime + ); + return typeof labelText === 'string' ? labelText : ''; + } + + private isEntityEng(entity: Cesium.Entity): boolean { + const props = this.getEntityRuntimeProps(entity); + const layerKey = String(this.getEntityLayerKey(entity) || '').toLowerCase(); + const sttpMap = String( + props._sttpMap || props.sttpMap || props.popupSttpMap || '' + ).toUpperCase(); + const sttpCode = String(props.sttpCode || props.sttp || '').toUpperCase(); + + return ( + layerKey.includes('eng_point') || + sttpMap === 'ENG' || + sttpMap === 'ENG2' || + sttpCode === 'ENG' + ); + } + + private getEntityEngRenderPriority(entity: Cesium.Entity): number { + return this.isEntityEng(entity) ? 200 : 100; + } + + private getEntityExpandOffset(_entity: Cesium.Entity, _currentZoom: number) { + return { + x: 0, + y: 0 + }; + } + + private requestRefreshPointVisibility(_immediate = false, _debounceMs = 0) { + if (!this.viewer) { + return; + } + + this.pointLayerRegistry.forEach((dataSource, layerKey) => { + dataSource.entities.values.forEach(entity => { + const legendState = this.getEntityLegendState(entity); + const visible = + dataSource.show && + this.getLayerLegendVisibility(layerKey, legendState); + this.setEntityLegendVisible(entity, visible); + if (entity.billboard) { + entity.billboard.show = visible; + } + if (entity.label) { + entity.label.show = visible && !!this.getEntityLabelText(entity); + } + }); + }); + + this.requestRender(); + } + private getCurrentCesiumZoom() { + if (!this.viewer) { + return undefined; + } + const height = this.viewer.camera.positionCartographic.height; + if (!height || !isFinite(height) || height <= 0) { + return undefined; + } + const zoom = + this.CESIUM_ZOOM_FORMULA_D + + (this.CESIUM_ZOOM_FORMULA_A - this.CESIUM_ZOOM_FORMULA_D) / + (1 + + Math.pow( + Number(height) / this.CESIUM_ZOOM_FORMULA_C, + this.CESIUM_ZOOM_FORMULA_B + )) + + 1; + + return zoom > -1 ? zoom : 0; + } + + private getCurrentCameraHeight() { + if (!this.viewer) { + return Number.POSITIVE_INFINITY; + } + + const height = this.viewer.camera.positionCartographic.height; + if (!height || !isFinite(height) || height <= 0) { + return Number.POSITIVE_INFINITY; + } + return height; + } + + private getEntityLayerKey(entity: Cesium.Entity) { + if (!this.viewer) { + return ''; + } + const layerKey = entity.properties?._layerKey?.getValue?.( + this.viewer.clock.currentTime + ); + return typeof layerKey === 'string' ? layerKey : ''; + } + + private getEntityLegendState(entity: Cesium.Entity) { + if (!this.viewer) { + return ''; + } + const legendState = entity.properties?.anchoPointState?.getValue?.( + this.viewer.clock.currentTime + ); + return this.normalizeLegendState( + legendState == null ? '' : String(legendState) + ); + } + + private isEntityInteractive(entity: Cesium.Entity) { + const layerKey = this.getEntityLayerKey(entity); + if (!layerKey) { + return false; + } + + const dataSource = this.pointLayerRegistry.get(layerKey); + if (!dataSource || !dataSource.show || entity.show === false) { + return false; + } + + const legendState = this.getEntityLegendState(entity); + return this.getLayerLegendVisibility(layerKey, legendState); + } + + private isHoveredEntityStillVisible() { + if (!this.hoveredEntityId) { + return false; + } + + for (const dataSource of this.pointLayerRegistry.values()) { + const hoveredEntity = dataSource.entities.getById(this.hoveredEntityId); + if (hoveredEntity instanceof Cesium.Entity) { + return this.isEntityInteractive(hoveredEntity); + } + } + + return false; + } + + private hidePopupElement() { + if (!this.popupElement) { + return; + } + + this.popupElement.style.display = 'none'; + } + + private hidePopup() { + this.hoveredEntityId = null; + if (!this.popupElement) { + return; + } + + this.hidePopupElement(); + this.popupElement.innerHTML = ''; + } + + private getPopupHtml(props: Record) { + return ( + props.popupHtml || + generatePopupHtml(props) || + this.buildFallbackPopupHtml(props) + ); + } + + private updatePopupPosition( + position: Cesium.Cartesian3, + entity?: Cesium.Entity + ) { + if (!this.viewer || !this.popupElement) { + return; + } + + const canvasPosition = Cesium.SceneTransforms.worldToWindowCoordinates( + this.viewer.scene, + position + ); + + if (!canvasPosition) { + this.hidePopup(); + return; + } + + const currentZoom = this.getCurrentCesiumZoom(); + const expandOffset = + entity && currentZoom !== undefined + ? this.getEntityExpandOffset(entity, currentZoom) + : { x: 0, y: 0 }; + + this.popupElement.style.left = `${canvasPosition.x + expandOffset.x}px`; + this.popupElement.style.top = `${canvasPosition.y + expandOffset.y}px`; + this.popupElement.style.display = 'block'; + } + + private showPopupForEntity(entity: Cesium.Entity) { + if (!this.viewer || !this.popupElement || !entity.position) { + return; + } + + const currentTime = this.viewer.clock.currentTime; + const rawPopupSource = ((entity as any).__popupSource || {}) as Record< + string, + any + >; + const propertyValue = + entity.properties?.getValue?.(currentTime) || ({} as Record); + const props = { + ...propertyValue, + ...rawPopupSource + }; + + const popupHtml = this.getPopupHtml(props); + const position = entity.position.getValue(currentTime); + if (!popupHtml || !position) { + this.hidePopup(); + return; + } + + this.popupElement.innerHTML = popupHtml; + this.hoveredEntityId = entity.id; + this.updatePopupPosition(position, entity); + } + + private getEntityPopupProps(entity: Cesium.Entity) { + if (!this.viewer) { + return {}; + } + + const currentTime = this.viewer.clock.currentTime; + const rawPopupSource = ((entity as any).__popupSource || {}) as Record< + string, + any + >; + const propertyValue = + entity.properties?.getValue?.(currentTime) || ({} as Record); + + return { + ...propertyValue, + ...rawPopupSource + }; + } + + private bindHoverPopupHandler() { + if (!this.viewer) { + return; + } + + this.viewer.screenSpaceEventHandler.removeInputAction( + Cesium.ScreenSpaceEventType.MOUSE_MOVE + ); + + this.viewer.screenSpaceEventHandler.setInputAction(movement => { + if (!this.viewer) { + return; + } + + const cameraHeight = this.getCurrentCameraHeight(); + if (cameraHeight > this.HOVER_POPUP_MAX_HEIGHT) { + this.hidePopup(); + return; + } + + const picked = this.viewer.scene.pick(movement.endPosition); + const entity = + picked && picked.id instanceof Cesium.Entity ? picked.id : null; + + if (!entity || !this.isEntityInteractive(entity)) { + this.hidePopup(); + return; + } + + if (this.hoveredEntityId === entity.id) { + const position = entity.position?.getValue( + this.viewer.clock.currentTime + ); + if (position) { + this.updatePopupPosition(position, entity); + } + return; + } + + this.showPopupForEntity(entity); + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + } + + private bindPointClickHandler() { + if (!this.viewer) { + return; + } + + this.viewer.screenSpaceEventHandler.removeInputAction( + Cesium.ScreenSpaceEventType.LEFT_CLICK + ); + + this.viewer.screenSpaceEventHandler.setInputAction(movement => { + if (!this.viewer) { + return; + } + + const picked = this.viewer.scene.pick(movement.position); + const entity = + picked && picked.id instanceof Cesium.Entity ? picked.id : null; + + if (!entity || !this.isEntityInteractive(entity)) { + return; + } + + const props = this.getEntityPopupProps(entity); + if (props?.sttpMap === 'ylfb') { + modelStore.ylfbModalVisible = true; + modelStore.params = props; + return; + } + + modelStore.modalVisible = true; + modelStore.params = props; + modelStore.title = props?.titleName || `${props?.stnm || ''} 详情信息`; + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + } + + private bindCameraPopupReset() { + if (!this.viewer) { + return; + } + + if (this.removeCameraChangedListener) { + this.removeCameraChangedListener(); + this.removeCameraChangedListener = null; + } + + this.removeCameraChangedListener = + this.viewer.camera.changed.addEventListener(() => { + if (this.hoveredEntityId) { + this.hidePopup(); + } + }); + } + private showLoadingOverlay(container: HTMLElement) { this.hideLoadingOverlay(); @@ -152,8 +938,8 @@ export class MapCesium implements MapInterface { overlay.style.gap = '12px'; overlay.style.background = 'linear-gradient(180deg, rgba(7, 20, 36, 0.72) 0%, rgba(7, 20, 36, 0.46) 100%)'; - overlay.style.backdropFilter = 'blur(2px)'; overlay.style.zIndex = '30'; + overlay.style.backdropFilter = 'blur(2px)'; overlay.style.pointerEvents = 'auto'; const spinner = document.createElement('div'); @@ -199,17 +985,32 @@ export class MapCesium implements MapInterface { } destroy(): void { - if (this.flightFallbackTimer !== null) { - window.clearTimeout(this.flightFallbackTimer); - this.flightFallbackTimer = null; - } + this.clearFlightFallbackTimer(); this.hideLoadingOverlay(); + this.hidePopup(); + this.baseLayerRegistry.clear(); + this.baseLayerAliasMap.clear(); + this.pointLayerRegistry.clear(); + this.pointLegendVisibility.clear(); + this.pointEntityIndex.clear(); + this.imageryBaseLayer = null; if (this.viewer) { + this.viewer.screenSpaceEventHandler.removeInputAction( + Cesium.ScreenSpaceEventType.MOUSE_MOVE + ); + this.viewer.screenSpaceEventHandler.removeInputAction( + Cesium.ScreenSpaceEventType.LEFT_CLICK + ); + if (this.removeCameraChangedListener) { + this.removeCameraChangedListener(); + this.removeCameraChangedListener = null; + } this.viewer.camera.cancelFlight(); this.viewer.destroy(); this.viewer = null; } this.containerElement = null; + this.popupElement = null; } flyTopanto(position: number[], zoom: number): void { @@ -228,32 +1029,200 @@ export class MapCesium implements MapInterface { }); } + getCurrentZoom(): number | undefined { + return this.getCurrentCesiumZoom(); + } + // ... 其他空实现方法保持不变 ... jdPanelControlShowAndHidden(_baseid: string, _isAll: boolean): void {} mdLayerShowOrHidden(): void {} - addBaseDataLayer(_layer: any): void {} - controlBaseLayerTreeShowAndHidden( - _layerType: string, - _key: string, - _checked: boolean - ): void {} - mdLayerTreeShowOrHidden(_layerType: string, _checked?: boolean): void {} - hasLayer(_layerKey: string): boolean { - return false; + addBaseDataLayer(layerConfig: any, isShow = true): void { + if (!this.viewer || !layerConfig?.key) { + return; + } + + if ( + this.IGNORED_BASE_LAYER_KEYS.has(layerConfig.key) || + this.IGNORED_BASE_LAYER_KEYS.has(layerConfig.id) + ) { + return; + } + + if (layerConfig.key === 'BASEMAP-img') { + if (this.imageryBaseLayer) { + this.imageryBaseLayer.show = !!isShow; + } + return; + } + + const imageryLayer = this.createImageryLayer(layerConfig, !!isShow); + if (!imageryLayer) { + return; + } + + this.unregisterBaseLayer(layerConfig); + this.registerBaseLayer(layerConfig, imageryLayer); } - hasBaseLayer(_layerKey: string): boolean { - return false; + controlBaseLayerTreeShowAndHidden( + layerType: string, + key: string, + checked: boolean + ): void { + const targetKey = key || layerType; + if (targetKey === 'BASEMAP-img') { + if (this.imageryBaseLayer) { + this.imageryBaseLayer.show = checked; + } + return; + } + + const primaryKey = this.resolvePrimaryBaseLayerKey(targetKey); + if (!primaryKey) { + return; + } + + const imageryLayer = this.baseLayerRegistry.get(primaryKey); + if (!imageryLayer) { + return; + } + + imageryLayer.show = checked; + this.requestRender(); + } + mdLayerTreeShowOrHidden(layerType: string, checked = true): void { + const dataSource = this.pointLayerRegistry.get(layerType); + if (!dataSource) { + return; + } + + dataSource.show = checked; + if (!checked) { + this.hidePopup(); + this.requestRender(); + return; + } + this.applyLayerLegendVisibility(layerType, dataSource); + this.requestRefreshPointVisibility(true); + } + hasLayer(layerKey: string): boolean { + return this.pointLayerRegistry.has(layerKey); + } + hasBaseLayer(layerKey: string): boolean { + if (layerKey === 'BASEMAP-img') { + return !!this.imageryBaseLayer; + } + return !!this.resolvePrimaryBaseLayerKey(layerKey); } setLegendPointVisible( - _layerKey: string, - _anchoPointState: string, - _checked: boolean - ): void {} + layerKey: string, + anchoPointState: string, + checked: boolean + ): void { + const layerStateMap = this.pointLegendVisibility.get(layerKey) || new Map(); + const layerIndex = this.pointEntityIndex.get(layerKey) || new Map(); + const dataSource = this.pointLayerRegistry.get(layerKey); + + if (!anchoPointState) { + layerStateMap.clear(); + layerStateMap.set('__default__', checked); + layerIndex.forEach((_entities, legendKey) => { + layerStateMap.set(legendKey, checked); + }); + this.pointLegendVisibility.set(layerKey, layerStateMap); + if (dataSource) { + this.applyLayerLegendVisibility(layerKey, dataSource); + } + if (!checked) { + this.hidePopup(); + } + this.requestRefreshPointVisibility(true); + return; + } + + const normalizedLegendState = this.normalizeLegendState( + String(anchoPointState) + ); + layerStateMap.set(normalizedLegendState, checked); + this.pointLegendVisibility.set(layerKey, layerStateMap); + + if (dataSource) { + dataSource.entities.values.forEach(entity => { + if (this.getEntityLegendState(entity) === normalizedLegendState) { + this.setEntityLegendVisible(entity, checked); + } + }); + } + if (!checked) { + this.hidePopup(); + } + this.requestRefreshPointVisibility(true); + } addInitDataLayer( - _pointData: any[], - _layerType: any, - _mdoptions?: any - ): void {} + pointData: any[], + layerType: any, + _mdoptions?: MDOptions + ): void { + const layerKey = String(layerType || ''); + if (!layerKey) { + return; + } + + const dataSource = this.getOrCreatePointLayer(layerKey); + if (!dataSource) { + return; + } + + const entityCollection = dataSource.entities as Cesium.EntityCollection & { + suspendEvents?: () => void; + resumeEvents?: () => void; + }; + const canSuspendEntityEvents = + typeof entityCollection?.suspendEvents === 'function' && + typeof entityCollection?.resumeEvents === 'function'; + + if (canSuspendEntityEvents) { + entityCollection.suspendEvents(); + } + + try { + dataSource.entities.removeAll(); + this.resetPointLayerIndexes(layerKey); + + const list = Array.isArray(pointData) ? pointData : []; + list.forEach((item, index) => { + const entity = this.createPointEntity(item, layerKey, index); + if (!entity) { + return; + } + dataSource.entities.add(entity); + }); + } finally { + if (canSuspendEntityEvents) { + entityCollection.resumeEvents?.(); + } + } + + dataSource.show = true; + this.applyLayerLegendVisibility(layerKey, dataSource); + this.requestRefreshPointVisibility(true); + } + removePointLayer(layerKey: string): void { + if (!this.viewer) { + return; + } + + const dataSource = this.pointLayerRegistry.get(layerKey); + if (!dataSource) { + return; + } + + this.viewer.dataSources.remove(dataSource, true); + this.pointLayerRegistry.delete(layerKey); + this.pointLegendVisibility.delete(layerKey); + this.pointEntityIndex.delete(layerKey); + this.hidePopup(); + this.requestRender(); + } switchView(_type: any): void {} fitBounds(_bounds: any): void {} baseLayerSwitcher(_key: string): void {} @@ -326,5 +1295,17 @@ export class MapCesium implements MapInterface { } // ... 其他代码 ... - initPopupOverlay(_popupContainer: HTMLDivElement): void {} + initPopupOverlay(popupContainer: HTMLDivElement): void { + this.popupElement = popupContainer; + this.popupElement.style.display = 'none'; + this.popupElement.style.position = 'absolute'; + this.popupElement.style.transform = 'translate(-50%, calc(-100% - 10px))'; + this.popupElement.style.pointerEvents = 'none'; + if ( + this.containerElement && + getComputedStyle(this.containerElement).position === 'static' + ) { + this.containerElement.style.position = 'relative'; + } + } } diff --git a/frontend/src/components/gis/map.class.ts b/frontend/src/components/gis/map.class.ts index c0eada73..5758848c 100644 --- a/frontend/src/components/gis/map.class.ts +++ b/frontend/src/components/gis/map.class.ts @@ -272,4 +272,8 @@ export class MapClass implements MapClassInterface { flyTopanto(position: number[], zoom: number): void { this.service.flyTopanto(position, zoom); } + + getCurrentZoom(): number | undefined { + return this.service.getCurrentZoom(); + } } diff --git a/frontend/src/components/gis/map.d.ts b/frontend/src/components/gis/map.d.ts index e18e481f..24cb929b 100644 --- a/frontend/src/components/gis/map.d.ts +++ b/frontend/src/components/gis/map.d.ts @@ -124,6 +124,12 @@ export interface MapInterface { fitBounds(bbox, bearing): void; flyTopanto(positon, zoom): void; + + /** + * 获取当前地图缩放级别语义 + * 2D 直接返回原生 zoom,3D 由相机高度换算后返回 + */ + getCurrentZoom(): number | undefined; // /** // * 加载倾斜摄影数据 // * @param HH3DUrlArray diff --git a/frontend/src/components/gis/map.ol.ts b/frontend/src/components/gis/map.ol.ts index c5aa5d7b..db4a9528 100644 --- a/frontend/src/components/gis/map.ol.ts +++ b/frontend/src/components/gis/map.ol.ts @@ -49,7 +49,7 @@ const MIN_ZOOM = 4.23; const MAX_ZOOM = 22; const INITIAL_ZOOM = 4.5; const BATCH_POPUP_MODE_ZOOM = 15; -const FULL_DISPLAY_NO_COLLISION_ZOOM = 15; +const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY; // 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标) const BOUNDS_SW = [26.5, -9.99999999999929]; @@ -80,6 +80,8 @@ export class MapOl implements MapInterface { private regionMaskManager: RegionMaskManager; private isBatchPopupMode = false; private batchPopupRefreshFrameId: number | null = null; + private batchPopupPendingRebuild = false; + private batchPopupPostRenderListenerKey: any = null; private pointLayerStyleRefreshFrameId: number | null = null; private labelVisibilityRefreshFrameId: number | null = null; private labelVisibilityDebounceTimerId: number | null = null; @@ -200,13 +202,14 @@ export class MapOl implements MapInterface { this.map.on('pointerdrag', () => { if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(); + this.ensureBatchPopupPostRenderSync(); } }); this.map.on('moveend', () => { + this.clearBatchPopupPostRenderSync(); this.requestRefreshPointLabelVisibility(false, 60); if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(); + this.requestUpdateBatchPopups(false, true); } }); @@ -291,8 +294,9 @@ export class MapOl implements MapInterface { */ private enableBatchPopupMode() { this.isBatchPopupMode = true; + this.clearBatchPopupPostRenderSync(); this.popupManager.clearBatchPopups(); - this.requestUpdateBatchPopups(true); + this.requestUpdateBatchPopups(true, true); } /** @@ -300,6 +304,8 @@ export class MapOl implements MapInterface { */ private disableBatchPopupMode() { this.isBatchPopupMode = false; + this.batchPopupPendingRebuild = false; + this.clearBatchPopupPostRenderSync(); if (this.batchPopupRefreshFrameId !== null) { cancelAnimationFrame(this.batchPopupRefreshFrameId); this.batchPopupRefreshFrameId = null; @@ -314,11 +320,40 @@ export class MapOl implements MapInterface { private updateBatchPopups() { const features = this.pointLayerManager.getFeaturesInViewport(); // 传入样式检查器,过滤掉 declutter/距离过滤等隐藏的锚点 - this.popupManager.showPopupsForFeatures(features, feature => { - const style = this.createPointStyle(feature); - return style !== null; + this.popupManager.showPopupsForFeatures( + features, + feature => { + const style = this.createPointStyle(feature); + return style !== null; + }, + { + rebuild: this.batchPopupPendingRebuild + } + ); + } + + private ensureBatchPopupPostRenderSync() { + if (!this.map || this.batchPopupPostRenderListenerKey) { + return; + } + + this.batchPopupPostRenderListenerKey = this.map.on('postrender', () => { + if (!this.isBatchPopupMode) { + return; + } + this.popupManager.updateBatchPopupPositions(feature => { + const style = this.createPointStyle(feature); + return style !== null; + }); }); } + + private clearBatchPopupPostRenderSync() { + if (this.batchPopupPostRenderListenerKey) { + unByKey(this.batchPopupPostRenderListenerKey); + this.batchPopupPostRenderListenerKey = null; + } + } /** * 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小) */ @@ -350,6 +385,7 @@ export class MapOl implements MapInterface { const dynamicScale = this.getDynamicPointScale(currentZoom); const fontSize = this.getPointFontSize(dynamicScale); + const engPriority = this.getFeatureEngRenderPriority(feature); const formattedLabelText = this.formatPointLabelText(labelText); const labelLineCount = formattedLabelText ? formattedLabelText.split('\n').length @@ -363,7 +399,10 @@ export class MapOl implements MapInterface { feature.get('_labelCollisionVisible') === true; const finalIconVisible = iconTargetVisible && iconCollisionVisible; const labelTargetVisible = - finalIconVisible && !!formattedLabelText && labelCollisionVisible; + !this.isBatchPopupMode && + finalIconVisible && + !!formattedLabelText && + labelCollisionVisible; if (!finalIconVisible) { return null; @@ -373,6 +412,7 @@ export class MapOl implements MapInterface { iconUrl, dynamicScale.toFixed(2), fontSize, + engPriority, labelOffsetY, labelTargetVisible ? formattedLabelText : '', labelTargetVisible ? 1 : 0 @@ -384,6 +424,7 @@ export class MapOl implements MapInterface { const styles: Style[] = [ new Style({ + zIndex: engPriority, image: new Icon({ src: iconUrl, scale: dynamicScale, @@ -397,7 +438,7 @@ export class MapOl implements MapInterface { if (formattedLabelText && labelTargetVisible) { styles.push( new Style({ - zIndex: 101, + zIndex: engPriority + 1, text: new Text({ text: formattedLabelText, offsetY: labelOffsetY, @@ -598,6 +639,7 @@ export class MapOl implements MapInterface { top: number; bottom: number; priority: number; + engPriority: number; densityPriority: number; id: string; pixelX: number; @@ -616,6 +658,7 @@ export class MapOl implements MapInterface { top: number; bottom: number; priority: number; + engPriority: number; densityPriority: number; id: string; pixelX: number; @@ -808,7 +851,7 @@ export class MapOl implements MapInterface { const dynamicScale = this.getDynamicPointScale(currentZoom); // 备注:图标碰撞盒适当小于视觉图标尺寸,允许相邻站点轻微贴近显示, // 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。 - const iconCollisionSize = Math.max(12, 16 * dynamicScale); + const iconCollisionSize = Math.max(14, 24 * dynamicScale); const iconPadding = 0; const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding; const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding; @@ -851,6 +894,7 @@ export class MapOl implements MapInterface { top: centerY - estimatedHeight / 2, bottom: centerY + estimatedHeight / 2, priority: Number(entry.feature.get('_nearbyPriority') || 9999), + engPriority: this.getFeatureEngRenderPriority(entry.feature), densityPriority: entry.densityPriority, id: candidateId, pixelX: entry.pixelX, @@ -863,6 +907,9 @@ export class MapOl implements MapInterface { }); candidates.sort((left, right) => { + if (left.engPriority !== right.engPriority) { + return right.engPriority - left.engPriority; + } if (left.priority !== right.priority) { return left.priority - right.priority; } @@ -951,6 +998,9 @@ export class MapOl implements MapInterface { }); const labelCandidates = [...candidates].sort((left, right) => { + if (left.engPriority !== right.engPriority) { + return right.engPriority - left.engPriority; + } if (left.priority !== right.priority) { return left.priority - right.priority; } @@ -1149,10 +1199,11 @@ export class MapOl implements MapInterface { } } - private requestUpdateBatchPopups(immediate = false) { + private requestUpdateBatchPopups(immediate = false, rebuild = true) { if (!this.isBatchPopupMode) { return; } + this.batchPopupPendingRebuild = this.batchPopupPendingRebuild || rebuild; if (immediate) { if (this.batchPopupRefreshFrameId !== null) { @@ -1160,6 +1211,7 @@ export class MapOl implements MapInterface { this.batchPopupRefreshFrameId = null; } this.updateBatchPopups(); + this.batchPopupPendingRebuild = false; return; } @@ -1173,6 +1225,7 @@ export class MapOl implements MapInterface { return; } this.updateBatchPopups(); + this.batchPopupPendingRebuild = false; }); } @@ -1326,6 +1379,27 @@ export class MapOl implements MapInterface { return densityDisplayRules.at(-1)?.minZoom ?? 0; } + private isFeatureEng(feature: Feature): boolean { + const layerKey = String(feature.get('_layerKey') || '').toLowerCase(); + const sttpMap = String( + feature.get('_sttpMap') || feature.get('sttpMap') || '' + ).toUpperCase(); + const sttpCode = String( + feature.get('sttpCode') || feature.get('sttp') || '' + ).toUpperCase(); + + return ( + layerKey.includes('eng_point') || + sttpMap === 'ENG' || + sttpMap === 'ENG2' || + sttpCode === 'ENG' + ); + } + + private getFeatureEngRenderPriority(feature: Feature): number { + return this.isFeatureEng(feature) ? 200 : 100; + } + /** * 如果点位在当前视图中没有近邻点,则允许跳过静态密度门槛直接参与显示。 * 这样像“戈兰滩”这类 distance 档位偏密、但当前周围实际很空的点也能出现。 @@ -1407,10 +1481,9 @@ export class MapOl implements MapInterface { const nearbyGroupId = feature.get('_nearbyGroupId'); const groupSize = Number(feature.get('_nearbyGroupSize')); const priority = Number(feature.get('_nearbyPriority')); - const replaceAtZoom = Number(feature.get('_nearbyReplaceAtZoom')); - const expandAtZoom = Number(feature.get('_nearbyExpandAtZoom')); const showZoomMin = feature.get('_nearbyShowZoomMin'); const showZoomMax = feature.get('_nearbyShowZoomMax'); + const expandAtZoom = feature.get('_nearbyExpandAtZoom'); if (!nearbyGroupId || !Number.isFinite(groupSize) || groupSize < 2) { return true; @@ -1436,14 +1509,14 @@ export class MapOl implements MapInterface { return false; } - if (Number.isFinite(expandAtZoom) && currentZoom >= expandAtZoom) { + if ( + expandAtZoom !== null && + expandAtZoom !== undefined && + currentZoom >= Number(expandAtZoom) + ) { return true; } - if (Number.isFinite(replaceAtZoom) && currentZoom >= replaceAtZoom) { - return priority <= Math.min(2, groupSize); - } - return priority === 1; } /** @@ -2436,6 +2509,10 @@ export class MapOl implements MapInterface { ); } + getCurrentZoom(): number | undefined { + return this.view?.getZoom(); + } + /** * 生成带完整圆边框的上半圆 Canvas * @param radius 半径(像素) @@ -2496,6 +2573,7 @@ export class MapOl implements MapInterface { window.clearTimeout(this.labelVisibilityDebounceTimerId); this.labelVisibilityDebounceTimerId = null; } + this.clearBatchPopupPostRenderSync(); this.removeQueryLayer(); this.regionMaskManager.destroy(); diff --git a/frontend/src/components/gis/ol/point-layer-manager.ts b/frontend/src/components/gis/ol/point-layer-manager.ts index 95489733..b448288f 100644 --- a/frontend/src/components/gis/ol/point-layer-manager.ts +++ b/frontend/src/components/gis/ol/point-layer-manager.ts @@ -318,7 +318,7 @@ export class PointLayerManager { layerKey: string, matchValue: string, checked: boolean, - fieldKey: string = 'anchoPointState' + fieldKey = 'anchoPointState' ): void { const vectorLayer = this.layerRegistry.get(layerKey); if (!vectorLayer) return; diff --git a/frontend/src/components/gis/ol/popup-manager.ts b/frontend/src/components/gis/ol/popup-manager.ts index dd21e450..929d8dfb 100644 --- a/frontend/src/components/gis/ol/popup-manager.ts +++ b/frontend/src/components/gis/ol/popup-manager.ts @@ -4,7 +4,10 @@ import OlMap from 'ol/Map'; import Overlay from 'ol/Overlay'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { generatePopupHtml } from '@/utils/popupHtmlGenerator'; +import { + generatePopupHtml, + shouldPreferEng2Popup +} from '@/utils/popupHtmlGenerator'; type PopupManagerOptions = { map: OlMap | null; @@ -27,6 +30,11 @@ export class PopupManager { private map: OlMap | null; private popupOverlay: Overlay | null = null; private popupElement: HTMLElement | null = null; + private batchPopupContainer: HTMLDivElement | null = null; + private batchPopupItems: Array<{ + feature: Feature; + element: HTMLDivElement; + }> = []; private lastHoveredId: string | number | null = null; private animationFrameId: number | null = null; private getPointLayers: () => VectorLayer[]; @@ -49,6 +57,11 @@ export class PopupManager { // 备注:初始化 Popup Overlay 并挂载到地图实例。 initPopupOverlay(container: HTMLElement) { this.popupElement = container; + this.popupElement.style.display = 'none'; + this.popupElement.style.removeProperty('position'); + this.popupElement.style.removeProperty('transform'); + this.popupElement.style.removeProperty('left'); + this.popupElement.style.removeProperty('top'); this.popupElement.style.setProperty('pointer-events', 'none', 'important'); this.popupMouseEnterHandler = () => { this.forceHidePopup(); @@ -190,7 +203,7 @@ export class PopupManager { if (feature && coordinate) { const props = feature.getProperties(); - const popupHtml = props.popupHtml || generatePopupHtml(props); + const popupHtml = this.getPopupHtml(props); if (popupHtml) { this.popupElement.innerHTML = popupHtml; @@ -204,22 +217,37 @@ export class PopupManager { this.popupElement.style.display = 'none'; } + private getPopupHtml(props: Record) { + return props.popupHtml || generatePopupHtml(props); + } + // 备注:批量显示多个要素的 Popup,使用绝对定位的 div 而非 Overlay。 showPopupsForFeatures( features: Feature[], - styleChecker?: (feature: Feature) => boolean + styleChecker?: (feature: Feature) => boolean, + options?: { + rebuild?: boolean; + } ) { if (!this.map || !this.popupElement) return; - - // 清除之前批量显示的 popups - this.clearBatchPopups(); + const shouldRebuild = options?.rebuild !== false; // 过滤:只显示图例可见的要素 const visibleFeatures = features.filter( f => f.get('_legendVisible') !== false ); - if (visibleFeatures.length === 0) return; + if (visibleFeatures.length === 0) { + this.clearBatchPopups(); + return; + } + + if (!shouldRebuild && this.batchPopupItems.length > 0) { + this.updateBatchPopupPositions(styleChecker); + return; + } + + this.clearBatchPopups(); const mapElement = this.map.getTargetElement(); const batchContainer = document.createElement('div'); @@ -234,6 +262,7 @@ export class PopupManager { z-index: 1000; `; batchContainer.id = 'batch-popup-container'; + this.batchPopupContainer = batchContainer; // 先添加容器到 DOM,以便后续元素能正确测量尺寸 mapElement.appendChild(batchContainer); @@ -245,12 +274,6 @@ export class PopupManager { top: number; bottom: number; }> = []; - let styleHiddenCount = 0; - let blockedCount = 0; - let renderedCount = 0; - const renderedIds: Array = []; - const blockedIds: Array = []; - visibleFeatures.forEach(feature => { const geom = feature.getGeometry(); if (!geom || geom.getType() !== 'Point') return; @@ -261,12 +284,15 @@ export class PopupManager { // 检查样式是否隐藏(declutter、距离过滤等) if (styleChecker && !styleChecker(feature)) { - styleHiddenCount += 1; return; } const props = feature.getProperties(); - const popupHtml = props.popupHtml || generatePopupHtml(props); + const popupHtml = shouldPreferEng2Popup(props) + ? generatePopupHtml(props, { forceEng2: true }) || + props.popupHtml || + generatePopupHtml(props) + : this.getPopupHtml(props); if (!popupHtml) return; // 创建与原始 popupElement 完全相同的样式 @@ -314,8 +340,10 @@ export class PopupManager { popupEl.style.visibility = 'visible'; popupEl.style.left = `${popupX}px`; popupEl.style.top = `${popupY}px`; - renderedCount += 1; - renderedIds.push((feature.getId?.() as string | number | null) ?? null); + this.batchPopupItems.push({ + feature, + element: popupEl + }); placedPopups.push({ left: popupRect.left, @@ -325,15 +353,76 @@ export class PopupManager { }); } else { // 有碰撞,移除该 popup - blockedCount += 1; - blockedIds.push((feature.getId?.() as string | number | null) ?? null); batchContainer.removeChild(popupEl); } }); } + updateBatchPopupPositions(styleChecker?: (feature: Feature) => boolean) { + if ( + !this.map || + !this.batchPopupContainer || + this.batchPopupItems.length === 0 + ) { + return; + } + + const mapSize = this.map.getSize(); + const viewportWidth = mapSize?.[0] ?? 0; + const viewportHeight = mapSize?.[1] ?? 0; + const viewportPadding = 48; + + this.batchPopupItems.forEach(({ feature, element }) => { + if (feature.get('_legendVisible') === false) { + element.style.display = 'none'; + return; + } + + if (styleChecker && !styleChecker(feature)) { + element.style.display = 'none'; + return; + } + + const geom = feature.getGeometry(); + if (!geom || geom.getType() !== 'Point') { + element.style.display = 'none'; + return; + } + + const coords = (geom as Point).getCoordinates(); + const pixel = this.map?.getPixelFromCoordinate(coords); + if (!pixel) { + element.style.display = 'none'; + return; + } + + const popupX = pixel[0]; + const popupY = pixel[1] - 10; + if ( + popupX < -viewportPadding || + popupY < -viewportPadding || + popupX > viewportWidth + viewportPadding || + popupY > viewportHeight + viewportPadding + ) { + element.style.display = 'none'; + return; + } + + element.style.left = `${popupX}px`; + element.style.top = `${popupY}px`; + element.style.display = 'block'; + }); + } + // 备注:清除批量显示的 popups。 clearBatchPopups() { + this.batchPopupItems = []; + if (this.batchPopupContainer) { + this.batchPopupContainer.remove(); + this.batchPopupContainer = null; + return; + } + const existing = document.getElementById('batch-popup-container'); if (existing) { existing.remove(); diff --git a/frontend/src/components/mapFilter/index.vue b/frontend/src/components/mapFilter/index.vue index 626038af..c6cf8ef8 100644 --- a/frontend/src/components/mapFilter/index.vue +++ b/frontend/src/components/mapFilter/index.vue @@ -1,5 +1,8 @@ diff --git a/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue b/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue new file mode 100644 index 00000000..1fd92357 --- /dev/null +++ b/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue b/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue index 64294dbc..01fc94ce 100644 --- a/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue +++ b/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue @@ -7,15 +7,30 @@ :handle-add="handleAdd" @reset="handleReset" @search-finish="onSearchFinish" - /> + /> --> +
新增
+ diff --git a/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue b/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue new file mode 100644 index 00000000..bdde6844 --- /dev/null +++ b/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue b/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue index 372b78b1..265111ce 100644 --- a/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue +++ b/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue @@ -1,208 +1,161 @@ From 3623a47546714572de439390f9adb07e326aa432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=B4=E5=87=AF?= <2448379534@qq.com> Date: Tue, 14 Jul 2026 16:08:10 +0800 Subject: [PATCH 3/7] =?UTF-8?q?BUG=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/home/index.ts | 10 + .../system/map/TiltPhotoManagement/index.ts | 8 +- frontend/src/components/BasicSearch/index.vue | 2 +- frontend/src/components/BasicTable/index.vue | 1 + .../carouselIntroduce/ArtsDetail.vue | 38 +- frontend/src/components/gis/map.ol.ts | 8 + .../components/gis/ol/point-layer-manager.ts | 2 +- .../components/gis/ol/region-mask-manager.ts | 2 +- .../EnvironmentalQualityTwoLayers.vue | 24 +- .../TwoLayers/HJMZDTwoLays.vue | 29 +- ...eixingzuchengjijieruqingkuangTwoLayers.vue | 3 + .../huanbaozidongjianceEJ/index.vue | 15 +- .../src/modules/liuyu/ai/spdzwaijc/AISPTK.vue | 2 +- .../ModeGaoJing.vue | 28 +- .../shuizhijiancegaojing/ModeGaoJing.vue | 21 +- .../shuizhijianceshuju.vue | 450 +++++----- .../modules/lushengshengtaijiance/index.vue | 2 +- frontend/src/modules/qixidijchuXx/index.vue | 4 +- .../modules/qixidiliuliangbianhua/index.vue | 5 +- .../modules/qixidishuiwenbianhua/index.vue | 7 +- .../TwoLayer/FenBuQingKuangTwoLayer.vue | 4 +- .../ShengTaiLiuLiangDaBQKTwoLayer.vue | 2 +- .../TwoLayer/HuanJingSJJRQK.vue | 23 +- .../TwoLayer/ShuiDianKaiFQKTwoLayer.vue | 28 +- .../shuiwenjiancegongzuoEJ.vue | 2 +- .../yanchengshuiwenChangeMod/index.vue | 30 +- frontend/src/store/modules/jidiSelectEvent.ts | 4 +- frontend/src/store/modules/shuJuTianBao.ts | 2 +- frontend/src/utils/popupHtmlGenerator.ts | 4 +- .../conventionalHydropower/Approval/index.vue | 17 + .../FishResource/index.vue | 17 + .../views/fish-survey/fishSurveyZhuanZhi.vue | 2 +- .../shiPinJianKong/shiPinJianKongZhuanTi.vue | 165 +++- .../src/views/shuJuFenXi/components/utils.ts | 16 +- .../ConfigManagement/ConfigManagementForm.vue | 828 ++++++++++++++++++ .../ConfigManagementSearch.vue | 83 ++ .../views/system/ConfigManagement/index.vue | 379 ++++++++ .../StationManagementForm.vue | 160 ++++ .../StationManagementSearch.vue | 67 ++ .../views/system/StationManagement/index.vue | 203 +++++ .../TiltPhotoManagementForm.vue | 249 ++++++ .../TiltPhotoManagementSearch.vue | 81 ++ .../system/TiltPhotoManagement/index.vue | 269 ++++++ .../LayerManagement/LayerManagementForm.vue | 1 + .../map/components/LayerManagement/index.vue | 5 +- .../map/components/LegendStructure/index.vue | 14 +- frontend/src/views/system/map/index.vue | 24 +- .../components/ConfigManagement/index.vue | 2 +- 48 files changed, 2962 insertions(+), 380 deletions(-) create mode 100644 frontend/src/views/system/ConfigManagement/ConfigManagementForm.vue create mode 100644 frontend/src/views/system/ConfigManagement/ConfigManagementSearch.vue create mode 100644 frontend/src/views/system/ConfigManagement/index.vue create mode 100644 frontend/src/views/system/StationManagement/StationManagementForm.vue create mode 100644 frontend/src/views/system/StationManagement/StationManagementSearch.vue create mode 100644 frontend/src/views/system/StationManagement/index.vue create mode 100644 frontend/src/views/system/TiltPhotoManagement/TiltPhotoManagementForm.vue create mode 100644 frontend/src/views/system/TiltPhotoManagement/TiltPhotoManagementSearch.vue create mode 100644 frontend/src/views/system/TiltPhotoManagement/index.vue diff --git a/frontend/src/api/home/index.ts b/frontend/src/api/home/index.ts index 1f2ac831..0c450a18 100644 --- a/frontend/src/api/home/index.ts +++ b/frontend/src/api/home/index.ts @@ -79,6 +79,16 @@ export function vmsstbprptGetKendoList(data: any) { data: data }); } +// 环保自动监测工作开展情弹窗 +export function overviewvmsstbprptGetKendoList(data: any) { + return request({ + url: '/overview/vmsstbprpt/GetKendoList', + method: 'post', + data: data + }); +} + + // 获取基本信息 export function baseMsstbprptGetKendoList(data: any) { diff --git a/frontend/src/api/system/map/TiltPhotoManagement/index.ts b/frontend/src/api/system/map/TiltPhotoManagement/index.ts index b6ee61fc..5b041a97 100644 --- a/frontend/src/api/system/map/TiltPhotoManagement/index.ts +++ b/frontend/src/api/system/map/TiltPhotoManagement/index.ts @@ -1,9 +1,9 @@ import request from '@/utils/request'; - + // 获取所有倾斜摄影 export function getAllTiltPhotoTree(data: any) { return request({ - url: '/api/wmp-sys-server/sys/oblique/GetKendoListCust', + url: '/sys/oblique/GetKendoListCust', method: 'post', data: data }); @@ -11,7 +11,7 @@ export function getAllTiltPhotoTree(data: any) { // 保存倾斜摄影 export function saveTiltPhoto(data: any) { return request({ - url: '/api/wmp-sys-server/sys/oblique/addOrUpdate', + url: '/sys/oblique/addOrUpdate', method: 'post', data: data }); @@ -19,7 +19,7 @@ export function saveTiltPhoto(data: any) { // 删除倾斜摄影 export function deleteTiltPhoto(data: any) { return request({ - url: '/api/wmp-sys-server/sys/oblique/delete', + url: '/sys/oblique/delete', method: 'get', params: data }); diff --git a/frontend/src/components/BasicSearch/index.vue b/frontend/src/components/BasicSearch/index.vue index 1a500565..1df4f7c2 100644 --- a/frontend/src/components/BasicSearch/index.vue +++ b/frontend/src/components/BasicSearch/index.vue @@ -468,7 +468,7 @@ const triggerManualValuesChange = (changedKey: string, newValue: any) => { emit('valuesChange', changedValues, { ...formData }); }; -const dataDimensionDataChange = (value: any, fieldName: string = 'baseId') => { +const dataDimensionDataChange = (value: any, fieldName = 'baseId') => { formData[fieldName] = value; formData.rstcd = ""; shuJuTianBaoStore.getEngOption(value,'baseId'); diff --git a/frontend/src/components/BasicTable/index.vue b/frontend/src/components/BasicTable/index.vue index 5f5d7990..716cc092 100644 --- a/frontend/src/components/BasicTable/index.vue +++ b/frontend/src/components/BasicTable/index.vue @@ -192,6 +192,7 @@ const getList = async (filter?: Record) => { total.value = 0; if (filter !== undefined) { lastFilter.value = filter; + page.value = 1; } try { // 合并排序参数:优先使用用户手动排序,否则使用 searchParams 中的默认排序 diff --git a/frontend/src/components/carouselIntroduce/ArtsDetail.vue b/frontend/src/components/carouselIntroduce/ArtsDetail.vue index b0d95b82..e2e1de19 100644 --- a/frontend/src/components/carouselIntroduce/ArtsDetail.vue +++ b/frontend/src/components/carouselIntroduce/ArtsDetail.vue @@ -5,7 +5,7 @@