diff --git a/frontend/.env.development b/frontend/.env.development index 51fb15b8..b560df26 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -11,9 +11,9 @@ VITE_APP_BASE_URL = 'http://localhost:8093' # 测试环境 # VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # 汤伟 -# VITE_APP_BASE_URL = 'http://10.84.111.235:8093' +VITE_APP_BASE_URL = 'http://10.84.111.235:8093' # 李林 -VITE_APP_BASE_URL = 'http://10.84.111.25:8093' +# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' ## 开发环境 附件服务地址 VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125' diff --git a/frontend/src/components/gis/gisUtils.ts b/frontend/src/components/gis/gisUtils.ts index 2466a5fd..6a01223b 100644 --- a/frontend/src/components/gis/gisUtils.ts +++ b/frontend/src/components/gis/gisUtils.ts @@ -191,7 +191,7 @@ export const layerConfig2Flat = (data: any): any[] => { arr.forEach((item: any) => { const { type } = item; if (type) { - if (type == 'GISMap') { + if (type == 'GISMap' || type == 'geojson') { if (item?.children && item.children.length > 0) { f(item.children); } else { diff --git a/frontend/src/components/gis/map.ol.ts b/frontend/src/components/gis/map.ol.ts index 21c8a5b0..55ad7d39 100644 --- a/frontend/src/components/gis/map.ol.ts +++ b/frontend/src/components/gis/map.ol.ts @@ -11,7 +11,6 @@ import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Icon from 'ol/style/Icon'; import Text from 'ol/style/Text'; -import Circle from 'ol/style/Circle'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; import { get as getProjection, fromLonLat } from 'ol/proj'; @@ -77,6 +76,7 @@ export class MapOl implements MapInterface { private currentClipGeoJson: any | null = null; private clipRequestController: AbortController | null = null; private hoveredFeatureId: string | number | null = null; + private wfsHighlightLayer: VectorLayer | null = null; private popupManager: PopupManager; private regionMaskManager: RegionMaskManager; private isBatchPopupMode = false; @@ -166,6 +166,19 @@ export class MapOl implements MapInterface { this.popupManager.setMap(this.map); this.regionMaskManager.setContext(this.map, this.view); + // WFS 河段悬停高亮图层:与 demo 项目保持一致,命中要素复制到该图层以加深色描边高亮 + this.wfsHighlightLayer = new VectorLayer({ + source: new VectorSource(), + style: new Style({ + stroke: new Stroke({ + color: '#FFFF00', + width: 2 + }) + }), + zIndex: 20 + }); + this.map.addLayer(this.wfsHighlightLayer); + this.view.on('change:resolution', () => { this.handleZoomChange(); }); @@ -193,12 +206,69 @@ export class MapOl implements MapInterface { }); this.map.on('pointermove', evt => { + // fh_qxd 河段悬停高亮 + let hitLayer: any = null; + const hitFeature = this.map.forEachFeatureAtPixel( + evt.pixel, + (feature, layer) => { + hitLayer = layer; + return feature; + }, + { + hitTolerance: 0, + layerFilter: layer => layer.get('key') === 'fh_qxd' + } + ); + + // 高亮图层存在时,命中要素复制到高亮图层,未命中则清空 + if (this.wfsHighlightLayer) { + const highlightSource = this.wfsHighlightLayer.getSource(); + if (hitFeature) { + // 根据命中图层的底色动态调整高亮颜色(统一加深) + const baseColor = hitLayer?.get('bjColor') || ''; + // 读取命中图层原始描边宽度,保持高亮只变颜色不加粗 + let highlightWidth = 5; + const layerStyle = hitLayer?.getStyle?.(); + if ( + layerStyle && + typeof layerStyle === 'object' && + !Array.isArray(layerStyle) + ) { + const strokeWidth = (layerStyle as Style) + .getStroke?.() + ?.getWidth?.(); + if (typeof strokeWidth === 'number' && strokeWidth > 0) { + highlightWidth = strokeWidth; + } + } + this.wfsHighlightLayer.setStyle( + new Style({ + stroke: new Stroke({ + color: '#37ff00ff', + width: 4 + }) + }) + ); + highlightSource?.clear(); + highlightSource?.addFeature(hitFeature); + } else { + highlightSource?.clear(); + } + } + + // 命中河段时直接切换鼠标样式(与 demo 一致,popup 是节流回调不能依赖它) + const targetElement = this.map?.getTargetElement() as HTMLElement; + if (targetElement) { + targetElement.style.cursor = hitFeature ? 'pointer' : ''; + } + this.popupManager.handlePointerMove(evt.pixel, payload => { this.hoveredFeatureId = payload.hoveredId; - const targetElement = this.map?.getTargetElement() as HTMLElement; if (targetElement) { - targetElement.style.cursor = payload.hoveredId ? 'pointer' : ''; + // 命中河段或 hover popup 命中锚点时显示 pointer 光标 + targetElement.style.cursor = + payload.hoveredId || hitFeature ? 'pointer' : ''; } // 批量 popup 模式下不触发 hover popup @@ -1572,19 +1642,66 @@ export class MapOl implements MapInterface { } else { wmtsLayer.setZIndex(-100); } + wmtsLayer.set('key', layer.key); wmtsLayer.type = 'layer'; wmtsLayer.setVisible(isShow); this.layerRegistry.set(layer.key, wmtsLayer); this.map.addLayer(wmtsLayer); layer._layer = wmtsLayer; + } else if (layer.type === 'wfs') { + if (!this.view) return; + // 先加载数据,在原始 GeoJSON 层面对坐标做 4326 有效性筛选,删除混入的异常投影数据 + const isCoordIn4326Range = (lon: number, lat: number) => + lon >= -180 && lon <= 180 && lat >= -90 && lat <= 90; + const isValidGeoJsonFeature = (feature: any): boolean => { + const geom = feature?.geometry; + if (!geom) return false; + const flat = geom.coordinates.flat(Infinity) as number[]; + for (let i = 0; i < flat.length; i += 2) { + if (!isCoordIn4326Range(flat[i], flat[i + 1])) return false; + } + return true; + }; + const vectorSource = new VectorSource(); + // 先加载数据,过滤异常坐标,再创建图层添加到地图 + fetch(layer.url) + .then(res => res.json()) + .then(data => { + data.features = (data.features || []).filter(isValidGeoJsonFeature); + const features = new GeoJSON().readFeatures(data, { + // dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影 + }); + vectorSource.addFeatures(features); + }) + .catch(err => console.error(`WFS 图层加载失败 [${layer.key}]:`, err)); + const wfsLayer = new VectorLayer({ + source: vectorSource, + style: new Style({ + stroke: new Stroke({ + color: '#038303', + width: 3 + }) + }), + zIndex: -98, + visible: layer.visible !== false + }); + wfsLayer.set('bjColor', layer.color); + wfsLayer.set('key', layer.key); + wfsLayer.setVisible(isShow); + this.layerRegistry.set(layer.key, wfsLayer); + this.map.addLayer(wfsLayer); + layer._layer = wfsLayer; } else if (layer.type == 'raster-dem') { const tileLayer = new TileLayer({ source: new XYZ({ url: layer.url, - wrapX: true, + wrapX: false, crossOrigin: 'anonymous' }) }); + + tileLayer.set('key', layer.key); tileLayer.setZIndex(-100); tileLayer.setVisible(isShow); tileLayer.type = 'layer'; diff --git a/frontend/src/modules/map/application/map-orchestrator.ts b/frontend/src/modules/map/application/map-orchestrator.ts index 93785339..d658cbf7 100644 --- a/frontend/src/modules/map/application/map-orchestrator.ts +++ b/frontend/src/modules/map/application/map-orchestrator.ts @@ -91,9 +91,8 @@ export const useMapOrchestrator = () => { // 备注:统一把当前页面中的 GIS 基础图层初始化到地图实例,避免在图层树组件里做配置解析和加层编排。 const ensureBaseLayersInitialized = (configs: any[] = []) => { const flatLayerConfigs = layerConfig2Flat(configs); - flatLayerConfigs.forEach((item: any) => { - if (item.type !== 'GISMap' || !item.key) { + if ((item.type !== 'GISMap' && item.type !== 'geojson') || !item.key) { return; } @@ -110,6 +109,12 @@ export const useMapOrchestrator = () => { return; } + // 备注:保证 config.key 与树节点 key 一致,否则 layerRegistry 会注册到错误/空的 key 下, + // 导致图层树勾选时 hasBaseLayer 查不到而重复创建图层。 + if (item.config.key !== item.key) { + item.config = { ...item.config, key: item.key }; + } + if (item.key === 'customBaseLayer') { const serializableBaseLayerConfig = { key: item.config?.key, @@ -134,7 +139,7 @@ export const useMapOrchestrator = () => { mapClass.controlBaseLayerTreeShowAndHidden( item.key, - item.config?.id || item.key, + item.config?.key || item.key, item.checked === 1 ); }); diff --git a/frontend/src/store/modules/map.ts b/frontend/src/store/modules/map.ts index 7a99a910..e9e558dd 100644 --- a/frontend/src/store/modules/map.ts +++ b/frontend/src/store/modules/map.ts @@ -781,7 +781,10 @@ export const useMapStore = defineStore('map', () => { restoreLayerLegendVisibility(key); mapClass.mdLayerTreeShowOrHidden(key, true); } - } else if (layerItem && layerItem.type === 'GISMap') { + } else if ( + layerItem && + (layerItem.type === 'GISMap' || layerItem.type === 'geojson') + ) { // 处理 GISMap 类型的图层:勾选时若地图实例中不存在,则先重新加载再控制显隐。 if (key && key !== '-' && key !== 'powerBaseStation') { const shouldBeVisible = checkKeys.includes(key); @@ -900,7 +903,8 @@ export const useMapStore = defineStore('map', () => { } } - const allPointData = mapDataStore.rebuildPointDataFromCache(allLayerKeys); + const allPointData = + mapDataStore.rebuildPointDataFromCache(allLayerKeys); attachNearbyPointRuntimeMeta(allPointData); const runtimeCheckedKeys = getRuntimeCheckedLayerKeys(); const treeCheckedKeys = mapConfigStore.extractCheckedLayerKeys( @@ -1084,7 +1088,7 @@ export const useMapStore = defineStore('map', () => { legendItem.nameEn ); - if (layerItem?.type === 'GISMap') { + if (layerItem?.type === 'GISMap' || layerItem?.type === 'geojson') { mapClass.controlBaseLayerTreeShowAndHidden( layerKey, layerItem.config?.id || layerKey, diff --git a/frontend/src/views/qiXiDi/shengTaiDiBaoHu.vue b/frontend/src/views/qiXiDi/shengTaiDiBaoHu.vue index e38e076b..4f48bc92 100644 --- a/frontend/src/views/qiXiDi/shengTaiDiBaoHu.vue +++ b/frontend/src/views/qiXiDi/shengTaiDiBaoHu.vue @@ -1,10 +1,20 @@