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 @@
@@ -14,6 +24,10 @@ import QiXiDiLiuLiangBianHua from "@/modules/qixidiliuliangbianhua/index.vue"
+
+ 叠加栖息地图层
+
+ 单个展示
diff --git a/frontend/src/views/system/map/components/LayerManagement/LayerManagementForm.vue b/frontend/src/views/system/map/components/LayerManagement/LayerManagementForm.vue
index ffe7d157..5dc808f6 100644
--- a/frontend/src/views/system/map/components/LayerManagement/LayerManagementForm.vue
+++ b/frontend/src/views/system/map/components/LayerManagement/LayerManagementForm.vue
@@ -79,7 +79,7 @@
/>