From 3200f1f36f4ebee12189ddd649cbd6111aaf8169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=88=E5=85=86=E5=A2=9E?= <你的邮箱@example.com> Date: Wed, 1 Jul 2026 08:48:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E6=A8=A1=E5=9D=97=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/file/过鱼设施数据填报操作手册.docx | Bin 312026 -> 315995 bytes .../docs/地图模块-OpenLayers抽吸与碰撞分析.md | 5 +- frontend/docs/地图模块-抽吸规则与参数说明.md | 316 +++++++ frontend/docs/地图模块-详细说明.md | 867 +++++++++-------- frontend/src/App.vue | 4 + .../src/assets/legend/map-zxsdzGaojing0.svg | 12 + .../src/assets/legend/map-zxsdzGaojing1.svg | 12 + .../src/assets/legend/map-zxsdzGaojing2.svg | 12 + .../MapModal/components/AIYXSB/index.vue | 1 + frontend/src/components/gis/GisView.vue | 1 + frontend/src/components/gis/map.cesium.ts | 6 + frontend/src/components/gis/map.class.ts | 6 +- frontend/src/components/gis/map.d.ts | 8 +- frontend/src/components/gis/map.ol.ts | 769 +++++++++++++-- .../components/gis/ol/point-layer-manager.ts | 111 +++ .../src/components/gis/ol/popup-manager.ts | 57 -- frontend/src/components/mapFilter/index.vue | 28 +- frontend/src/components/mapLegend/index.vue | 8 +- .../map/application/map-orchestrator.ts | 70 +- .../modules/map/domain/nearby-point-rules.ts | 396 ++++++++ .../modules/map/stores/map-config.store.ts | 73 +- .../src/modules/map/stores/map-data.store.ts | 11 +- frontend/src/store/modules/map.ts | 257 +++-- frontend/src/utils/GisUrlList.ts | 10 +- .../leaflet/leaflet-tilelayer-wmts-src.js | 99 -- .../leaflet.inflatable-markers-group.js | 877 ------------------ frontend/src/utils/request.ts | 2 +- 27 files changed, 2373 insertions(+), 1645 deletions(-) create mode 100644 frontend/docs/地图模块-抽吸规则与参数说明.md create mode 100644 frontend/src/assets/legend/map-zxsdzGaojing0.svg create mode 100644 frontend/src/assets/legend/map-zxsdzGaojing1.svg create mode 100644 frontend/src/assets/legend/map-zxsdzGaojing2.svg create mode 100644 frontend/src/modules/map/domain/nearby-point-rules.ts delete mode 100644 frontend/src/utils/leaflet/leaflet-tilelayer-wmts-src.js delete mode 100644 frontend/src/utils/leaflet/leaflet.inflatable-markers-group.js diff --git a/frontend-sjtb/public/file/过鱼设施数据填报操作手册.docx b/frontend-sjtb/public/file/过鱼设施数据填报操作手册.docx index f3d2e60dd2a5a716fdc5ed02e8f28eea8fdebfe9..536eb4420477ee90f1a79ac3b06a84817463a5dc 100644 GIT binary patch delta 18865 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 DS>^m^ delta 14819 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≤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 VgvoD&c= expandAtZoom` + - 同组点全部显示 + - 同时进入展开布局阶段 + +补充规则: + +- 如果某个点配置了 `showZoomMin` 或 `showZoomMax`,则先受这两个值约束 +- 没有进入近邻组的点,不受抽吸规则影响 + +### 3.3 文字显示规则 + +当前文字显示必须依赖点位显示。 + +也就是说: + +- 点不显示,文字一定不显示 +- 点显示后,文字还要继续通过碰撞判断才会显示 + +当前文字碰撞规则: + +- 文字要避让已保留的图标占位盒 +- 文字要避让已保留的其他文字 +- 图标与文字不再分别交给 OpenLayers 的 `declutter` 自动处理 +- 当前走的是项目内的手工碰撞控制 + +## 4. 当前默认参数 + +当前默认参数集中维护在: + +- `src/modules/map/domain/nearby-point-rules.ts` + +当前采用统一配置对象: + +```ts +type NearbyPointConfig = { + autoRule: NearbyPointAutoRule; + layoutRule: NearbyPointLayoutRule; + densityDisplayRules: NearbyPointDensityDisplayRule[]; +}; +``` + +其中: + +- `autoRule`:控制近邻点自动识别、缩放切换和基础展开半径 +- `layoutRule`:控制展开阶段的单圈容量、扇形角度和外圈间距 +- `densityDisplayRules`:控制 `distance` 密度分档在不同缩放级别的基础显示资格 + +### 4.1 自动识别与缩放阶段参数 + +当前值如下: + +| 参数 | 当前值 | 作用 | +|---|---:|---| +| `distanceThresholdMeters` | `9000` | 近邻点自动识别距离阈值 | +| `replaceAtZoom` | `10.5` | 进入“主点 + 次点”阶段的缩放级别 | +| `expandAtZoom` | `12.2` | 进入“全部显示并展开”阶段的缩放级别 | +| `expandOffsetPx` | `48` | 第一圈展开的基础偏移半径 | +| `angleStepDeg` | `55` | 默认角度步长参考值 | +| `minGroupSize` | `2` | 最小成组数量 | + +### 4.2 展开布局参数 + +当前值如下: + +| 参数 | 当前值 | 作用 | +|---|---:|---| +| `ringSize` | `5` | 单圈最多容纳的次点数量 | +| `fanAngleForTwoDeg` | `110` | 单圈剩余 `2` 个点时的扇形总角度 | +| `fanAngleForFourDeg` | `150` | 单圈剩余 `3` 到 `4` 个点时的扇形总角度 | +| `fanAngleForManyDeg` | `180` | 单圈剩余 `5` 个及以上点时的扇形总角度 | +| `ringGapPx` | `22` | 外圈最小增量半径 | +| `ringGapFactor` | `0.85` | 外圈相对基础半径的增量系数 | + +### 4.3 密度分档显示参数 + +这里的 `distance` 不是物理距离,而是点位密度分档值。 +数值越大表示越稀疏,越早参与显示候选;数值越小表示越密集,需要更高缩放级别才参与显示。 + +当前值如下: + +| `minDensityValue` | `minZoom` | 作用 | +|---:|---:|---| +| `1500000` | `0` | 很稀疏的点,从小缩放级别就可参与显示 | +| `800000` | `6.1` | 稍密集的点,从中低缩放开始参与显示 | +| `400000` | `8.1` | 中密度点,需要进一步放大 | +| `50000` | `10.6` | 高密度点,需要较高缩放级别 | +| `0` | `12.2` | 极密集点,只有更高缩放才进入候选 | + +## 5. 当前展开布局 + +展开阶段由 `point-layer-manager.ts` 控制。 + +### 5.1 布局特点 + +当前布局不是简单把点随机散开,而是按以下方式处理: + +- 主点保留原位置 +- 同组其他点围绕主点展开 +- 展开方向优先偏下方 +- 单圈数量由 `ringSize` 控制 +- 点数较多时进入第二圈 +- 第二圈半径按 `ringGapPx` 和 `ringGapFactor` 继续增大,避免继续叠在一起 + +### 5.2 为什么优先向下展开 + +当前优先向下展开,主要是为了: + +- 尽量给上方文字留空间 +- 降低文字压住上方点的概率 +- 让地图上的抽吸形态更稳定 + +## 6. 当前实现的几个关键约束 + +### 6.1 不使用淡入淡出动画 + +目前已移除点和文字的淡入淡出动画。 + +原因: + +- 抽吸状态切换和动画叠加时,容易让点和文字出现闪烁 +- 会干扰对“当前到底该不该显示”的判断 + +当前阶段优先保证: + +- 显示规则稳定 +- 点和文字状态一致 +- 放大缩小时不出现明显错位 + +### 6.2 不再依赖 OL 的自动 declutter 决定图标命运 + +当前图标和文字都设置为 `declutterMode: 'none'`,由项目内逻辑统一控制。 + +这样做的原因: + +- 避免图标和文字落在两套碰撞体系里 +- 避免出现“文字先出来、点没出来” +- 避免文字压住其他点却仍然显示 + +## 7. 常见现象与调参建议 + +### 7.1 成组太少 + +表现: + +- 相近点仍然容易互相盖住 +- 进入展开前看起来还是压在一起 + +优先调整: + +- 增大 `distanceThresholdMeters` + +### 7.2 成组太多 + +表现: + +- 距离并不算近的点也被当成一组 +- 放大后过早进入抽吸关系 + +优先调整: + +- 减小 `distanceThresholdMeters` + +### 7.3 展开太晚 + +表现: + +- 放大到较深层级前仍然挤在一起 + +优先调整: + +- 减小 `expandAtZoom` + +### 7.4 展开太早 + +表现: + +- 中等缩放时地图上就出现较明显散开效果 + +优先调整: + +- 增大 `expandAtZoom` + +### 7.5 展开后间距不够 + +表现: + +- 虽然已经展开,但图标还是比较挤 + +优先调整: + +- 增大 `expandOffsetPx` +- 或增大 `ringGapPx` +- 或增大 `ringGapFactor` + +### 7.6 单圈散开角度不合适 + +表现: + +- 次点虽然已经展开,但仍然集中在一侧 +- 或者扇形过大,展开形态显得太散 + +优先调整: + +- 调整 `fanAngleForTwoDeg` +- 调整 `fanAngleForFourDeg` +- 调整 `fanAngleForManyDeg` + +### 7.7 主点在放大过程中突然消失 + +当前已经通过 `replaceAtZoom <= zoom < expandAtZoom` 阶段显示 `priority <= 2` 进行修正。 +如果后续又出现类似现象,优先检查: + +- `map.ol.ts` 中 `shouldRenderNearbyFeature()` 的阶段逻辑 +- 是否误改成只显示 `priority = 2` + +### 7.8 明明周围看起来空,文字还是不显示 + +优先检查: + +- 图标碰撞盒是否过大 +- 文字碰撞盒是否过于保守 +- 候选集是否发生重复统计 + +相关代码主要在: + +- `src/components/gis/map.ol.ts` + +## 8. 后续维护建议 + +建议后续继续保持以下约定: + +1. 抽吸参数继续统一放在 `nearby-point-rules.ts` +2. 显示决策继续集中在 `map.ol.ts` +3. 几何偏移继续集中在 `point-layer-manager.ts` +4. 后续调参优先改 `DEFAULT_NEARBY_POINT_CONFIG`,不要直接把数值散落回业务代码 +5. 如果继续加动画,必须放在抽吸状态机完全稳定之后 +6. 如果继续优化 popup,也要同步遵循“点先于文字”的显示原则 + +## 9. 当前结论 + +当前这版抽吸实现已经形成了比较稳定的主线: + +- 自动识别近邻点 +- 统一缩放阶段显示规则 +- 展开阶段几何偏移 +- 文字依赖点显示 +- 文字避让图标与文字 + +后续如果继续调优,建议先从参数层开始,不要先动主逻辑结构。 diff --git a/frontend/docs/地图模块-详细说明.md b/frontend/docs/地图模块-详细说明.md index 0b98ad2e..b957f0bb 100644 --- a/frontend/docs/地图模块-详细说明.md +++ b/frontend/docs/地图模块-详细说明.md @@ -2,590 +2,580 @@ ## 1. 文档目的 -本文档用于梳理当前前端地图模块的整体结构、运行链路、核心文件职责、图层与点位渲染机制、图例与筛选联动方式,以及后台配置与前台运行时之间的关系。 +本文档用于说明当前地图模块的真实运行结构、初始化时序、图层与图例联动、点位渲染规则、筛选与缩放行为,以及近期已经落地的关键约束。 -本文档只描述当前地图模块整体实现,不包含抽吸和碰撞的专项分析。 -抽吸与碰撞问题单独整理在: +本文档描述的是“当前可运行版本”的现状,不展开抽吸参数细节与专项问题分析。相关补充文档如下: +- `docs/地图模块-抽吸规则与参数说明.md` - `docs/地图模块-OpenLayers抽吸与碰撞分析.md` ## 2. 当前技术栈 -当前地图模块涉及的核心技术如下: - - 前端框架:Vue 3 + TypeScript + Ant Design Vue - 状态管理:Pinia -- 2D 地图引擎:OpenLayers +- 2D 地图引擎:OpenLayers 10.8.0 - 3D 地图引擎:Cesium -- 地图服务:GeoServer -- 数据来源: - - 后端接口返回图层树配置 - - 后端接口返回图例配置 - - 后端接口返回各类锚点数据 - -项目依赖中当前地图引擎版本如下: - -- `ol@^10.8.0` -- `cesium@^1.141.0` -- `leaflet@^1.9.4` +- 地图服务:GeoServer / WMTS / XYZ +- 运行模式:后台配置驱动 + 前台运行时编排 说明: -- 当前 2D 主链路已经切换到 OpenLayers; -- Leaflet 依赖仍保留在项目中,但不是当前主运行路径。 +- 当前 2D 主链路已经完全围绕 OpenLayers 组织。 +- 项目中仍保留 Leaflet 相关依赖和旧代码,但不是当前主执行路径。 -## 3. 地图整体架构 +## 3. 模块分层 -当前地图模块可以分成 6 层: +当前地图模块可以按 6 层理解: 1. 页面入口层 2. 应用编排层 3. 地图能力门面层 -4. 地图引擎实现层 -5. 状态存储层 -6. 交互组件层 +4. 地图引擎执行层 +5. Store 运行态层 +6. 前台交互组件层 -对应文件如下: +对应核心文件如下: - 页面入口层 - `src/components/gis/GisView.vue` - - 应用编排层 - `src/modules/map/application/map-orchestrator.ts` - - 地图能力门面层 - `src/components/gis/map.class.ts` - -- 地图引擎实现层 +- 地图引擎执行层 - `src/components/gis/map.ol.ts` - `src/components/gis/map.cesium.ts` - -- 状态存储层 + - `src/components/gis/ol/point-layer-manager.ts` + - `src/components/gis/ol/popup-manager.ts` +- Store 运行态层 - `src/store/modules/map.ts` - `src/modules/map/stores/map-config.store.ts` - `src/modules/map/stores/map-data.store.ts` - `src/modules/map/stores/map-view.store.ts` - -- 交互组件层 +- 前台交互组件层 - `src/components/mapLegend/index.vue` - `src/components/mapFilter/index.vue` - `src/components/mapController/index.vue` - `src/components/mapController/LayerController.vue` - `src/components/BaseLayerSwitcher/index.vue` -## 4. 页面入口层 +## 4. 整体原则 -### 4.1 全局挂载位置 +当前地图模块遵循以下几个运行原则: -地图视图最终由主布局中的 `GisView` 常驻挂载,入口位置在: +### 4.1 地图容器全局常驻 -- [AppMain.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/layout/components/AppMain.vue) +地图由 `GisView.vue` 常驻挂载,不会随着菜单切换被销毁重建。页面切换本质上是: -这意味着地图并不是某一个业务页单独创建,而是作为全局地图容器持续存在,然后随着菜单切换切换图层和页面配置。 +- 重新计算 `pageKey` +- 重新加载该页图层配置和图例配置 +- 用新的运行态覆盖旧页面显示内容 -### 4.2 `GisView.vue` 职责 +### 4.2 配置驱动 -文件: +前端不把全部图层和图例硬编码死,而是依赖后端返回的: -- [GisView.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/GisView.vue) +- 图层树配置 +- 图例配置 +- 页面级图例配置 +- 各业务点位接口数据 -当前职责主要有: +### 4.3 运行态绝对优先 -- 提供地图 DOM 容器 `#mapContainer` -- 挂载地图 popup 容器 -- 挂载地图图例、筛选器、控制器、底图切换器 -- 根据当前路由计算 `pageKey` -- 在组件挂载时触发地图初始化 -- 在页面切换时触发地图页面配置切换 -- 分发少量地图控制命令,例如 2D/3D 切换、梯级流域显示 +初始化默认值只在第一次派生时生效;一旦进入运行态,后续图层勾选、图例勾选、筛选器切换,都必须以当前用户操作后的状态为准,不能在加载完成后回退到默认状态。 -它已经不再承担大部分地图业务拼装逻辑,更多是地图页面入口和组件装配层。 +## 5. 页面入口层 -## 5. 应用编排层 +### 5.1 `GisView.vue` -文件: +`GisView.vue` 的职责主要是: -- [map-orchestrator.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/application/map-orchestrator.ts) +- 提供地图容器 `#mapContainer` +- 提供 popup 挂载容器 +- 挂载图例、筛选器、控制器、底图切换器 +- 根据路由计算 `pageKey` +- 在挂载时调用 `mapOrchestrator.mountView()` +- 在页面切换时交给编排器重载页面 -`map-orchestrator` 是当前地图运行时的调度中心,主要负责把页面、Store、地图实例串起来。 +当前页面层已经不再直接承载复杂业务逻辑,复杂链路都收敛到了编排器和 Store。 -核心职责包括: +## 6. 应用编排层 -- 初始化地图壳和 popup 容器 -- 根据 `pageKey` 加载当前页面地图配置 -- 拉取图层树和图例配置 -- 初始化基础底图 -- 触发点位数据加载 +### 6.1 `map-orchestrator.ts` 的职责 + +`map-orchestrator.ts` 是当前地图业务的总调度中心,主要负责: + +- 初始化地图壳与 popup +- 加载页面图层配置和图例配置 +- 初始化 GIS 基础图层 +- 触发点位图层加载 +- 管理缩放监听和基地切换监听 - 处理图层树勾选 -- 处理图例显隐 -- 处理基地切换 -- 处理时间筛选和重新加载 -- 处理搜索定位 -- 处理某些菜单下的缩放联动图层 +- 处理图例勾选 +- 处理时间筛选、容量筛选、搜索定位 +- 处理水电开发菜单下的缩放联动图层 -### 5.1 初始化主链路 +### 6.2 页面初始化链路 -大致流程如下: +当前初始化顺序如下: 1. `GisView.vue` 调用 `mapOrchestrator.mountView()` -2. 编排器初始化地图实例 -3. 挂载 popup 容器 -4. 加载当前页面的图层树、图例和页面图例 -5. 初始化基础底图 -6. 加载点位图层数据 -7. 建立缩放监听、基地监听和图例联动 +2. `MapClass.init()` 初始化地图实例 +3. 页面级 popup 容器挂到地图实例 +4. 绑定缩放监听和基地监听 +5. 并行请求图层树配置与图例配置 +6. 图例接口先返回时,立即生成图例运行态 +7. 图层树接口返回后,生成默认勾选图层并初始化 GIS 基础图层 +8. 再按图层配置批量加载点位数据 -### 5.2 页面切换 +这里有两个当前已落地的关键时序调整: -当路由变化后: +- 图例转圈只跟图例接口绑定,不再等待所有锚点接口完成。 +- 筛选器数据会随着已加载图层逐步出现,不再必须等全量点位接口结束。 -- `GisView.vue` 重新计算 `pageKey` -- 调用 `mapOrchestrator.handlePageChange()` -- 编排器重新加载该页面对应的地图配置和显示状态 +### 6.3 页面切换 -这使得地图容器本身不销毁,只是页面配置和显示内容发生变化。 +页面切换时地图不销毁,只重载当前页面的: -## 6. 地图能力门面层 +- 图层树配置 +- 图例配置 +- 点位数据 +- 当前页面的默认运行态 -文件: +如果仍处于同一个 `pageKey` 且用户之前修改过勾选态,会尽量复用当前运行态,而不是重新回放默认勾选。 -- [map.class.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.class.ts) +### 6.4 缩放联动 -`MapClass` 是地图能力统一门面,对外提供统一方法,屏蔽 2D 和 3D 的差异。 +当前编排层主要有两类缩放联动: -典型能力包括: +- `eng_point` 缩放联动 + - 大型电站始终允许显示 + - 中型电站仅在 `zoom > 7.5` 时进入显示候选 +- 水电开发菜单动态图层联动 + - `HYDRO_DYNAMIC_LAYER_KEYS` 在缩放跨越阈值时参与加载与显隐编排 + +## 7. 地图能力门面层 + +### 7.1 `map.class.ts` + +`MapClass` 是地图能力统一门面,屏蔽 2D / 3D 引擎差异,对外暴露统一方法。 + +当前常用能力包括: - `init()` +- `initPopupOverlay()` - `addBaseDataLayer()` - `addInitDataLayer()` - `mdLayerTreeShowOrHidden()` +- `controlBaseLayerTreeShowAndHidden()` - `setLegendPointVisible()` -- `baseLayerSwitcher()` +- `hasLayer()` +- `hasBaseLayer()` +- `removePointLayer()` - `flyTopanto()` -- `zoomToggle()` -- `lengthCalculate()` -- `areCalculate()` -- `mapOutPut()` -- `destroy()` - `switchView()` -当前 2D 主实现是: +当前业务主执行层仍然是 `MapOl`。 -- `this.service = new MapOl()` +## 8. 地图引擎执行层 -说明当前前台主要还是 OpenLayers 在承担地图业务。 +### 8.1 OpenLayers 主执行层 -## 7. 地图引擎实现层 +`map.ol.ts` 负责 OpenLayers 地图的实际渲染与交互,是当前 2D 地图主执行文件。 -### 7.1 OpenLayers 主实现 +当前承担的核心能力包括: -文件: - -- [map.ol.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.ol.ts) - -这是当前地图模块最核心的执行层文件,承担了大量实际渲染与地图交互逻辑,包括: - -- OpenLayers 地图初始化 -- 视图和底图管理 -- 点图层管理 -- 点位样式生成 +- 地图实例初始化 +- 底图与 GIS 图层管理 +- 点位图层渲染 +- 图标与文字样式生成 - hover popup 与批量 popup -- 区域裁切 -- 基地过滤 -- 梯级流域图层 -- 测量、截图、定位 +- 基地范围裁切 +- 高缩放交互 +- 测量、截图、飞行定位等基础地图能力 -### 7.2 Cesium 实现 +### 8.2 Cesium 执行层 -文件: +`map.cesium.ts` 目前更多是保留 3D 入口和部分基础能力,复杂业务点位与碰撞规则并未与 OpenLayers 完全对齐。 -- [map.cesium.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.cesium.ts) +结论上,当前真实业务规则仍然以 OpenLayers 链路为准。 -当前 3D 能力存在,但相对 2D 来说并没有完全对齐。 -目前更像是保留了切换入口和部分基础能力,例如: +## 9. Store 分层 -- viewer 初始化 -- 底图加载 -- 定位与飞行 -- 部分截图能力 +### 9.1 `map-config.store.ts` -多数业务点位和复杂运行逻辑仍然主要围绕 OpenLayers 设计。 +负责配置态数据: -## 8. 点图层管理 +- 图层树配置 +- 图层配置索引 +- 全量原始图例配置 +- 图例索引 +- 页面级图例配置 +- 配置接口加载状态 -文件: +当前这里拆分出了两个加载态: -- [point-layer-manager.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/ol/point-layer-manager.ts) +- `configLoading` +- `legendLoading` -这个文件负责 OpenLayers 点图层的生命周期管理。 +其中 `legendLoading` 单独控制图例转圈。 -核心职责包括: +### 9.2 `map-data.store.ts` -- 按图层 key 创建 `VectorLayer` -- 给每个点图层维护独立的 `VectorSource` -- 把后端点位数据转为 `Feature` -- 建立图例字段索引 -- 控制整层显隐 -- 控制图例项对应点位显隐 -- 删除点图层 -- 获取当前可视范围内点位 +负责数据态: -### 8.1 当前图层组织方式 +- `pointData` +- `pointDataCache` +- `layerLoadState` +- 地图整体 `loading` -当前点位图层的组织方式是: +这里的关键设计是: -- 每个业务点图层对应一个 `VectorLayer` -- 每条后端点位数据被转换为一个 `Feature` -- Feature 上会额外挂载运行时字段,例如: - - `_iconUrl` - - `_labelText` - - `_legendVisible` - - `_regionVisible` - - `_layerKey` +- 每个点图层有独立缓存 +- `pointData` 是由缓存合并出来的运行态数据 +- 单个图层命中缓存或接口返回后,就会立即回填 `pointData` -### 8.2 图层显隐 +因此当前搜索筛选下拉已经支持“渐进可用”,不再只能等全部接口结束。 -图层显隐主要分三类: +### 9.3 `map-view.store.ts` -- 图层树控制整层显隐 -- 图例项控制某一类点位显隐 -- 基地裁切控制某些点位在当前基地范围外隐藏 +负责视图运行态: -这些状态最终会体现在 Feature 属性和图层可见性上。 +- 当前勾选图层 `checkedLayerKeys` +- 图例勾选状态 `legendCheckedState` +- 搜索时间范围 +- 当前基地 +- 当前缩放级别 -## 9. 点位样式与渲染 +### 9.4 `src/store/modules/map.ts` -点位样式主逻辑在: +这是当前地图运行逻辑最重的 Store,负责串联配置、缓存和地图实例。 -- [map.ol.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.ol.ts) +主要职责包括: -当前点位样式包含以下内容: +- 生成图例运行态 +- 批量加载点位数据 +- 控制图层显隐 +- 控制图例显隐 +- 管理请求缓存与并发加载 +- 刷新点图层显示数据 +- 处理 `eng_point` 的显示过滤 +- 在全量点位准备完成后挂载近邻点元数据 + +## 10. 图层分类 + +当前地图里的图层大体可以分成三类: + +### 10.1 `GISMap` + +这类图层对应 GIS 基础图层或专题底图,由 `addBaseDataLayer()` 加载到 `layerRegistry`。 + +当前已补充的规则: + +- 勾选 `GISMap` 时,不能只做显示/隐藏。 +- 如果地图实例中不存在该图层,需要先重新加载,再显示。 + +### 10.2 点位图层 + +这类图层由接口返回锚点数据,最终进入 `PointLayerManager` 维护的 `VectorLayer`。 + +### 10.3 其他专题图层 + +例如梯级流域、区域裁切辅助层等,这些更多由 `map.ol.ts` 单独控制。 + +## 11. 图层数据加载机制 + +### 11.1 配置与数据分离 + +当前初始化时,配置加载与点位数据加载是两条链: + +- 图层树配置决定有哪些层、哪些默认勾选 +- 图例配置决定图例结构和图例项元数据 +- 点位数据接口决定地图上实际锚点内容 + +### 11.2 加载顺序 + +`loadAllLayerData()` 的主要逻辑是: + +1. 先收集所有带 URL 的图层 +2. 把默认勾选图层作为高优先级任务 +3. 并发加载,默认并发数为 4 +4. 每个图层完成后立即写入缓存 +5. 每个图层命中缓存或返回成功后,立即增量更新 `pointData` +6. 全部图层结束后,再统一: + - 回填每层原始数据 + - 挂载近邻点运行时元数据 + - 刷新各图层显示数据 + - 用最新运行态收尾 + +### 11.3 初始化阶段的图例绝对性 + +当前已修复一个关键问题: + +- 图例虽然可能在界面上还在 loading +- 但只要图例配置已经返回,默认隐藏的图例项在点位首批入图时就必须生效 + +也就是说,现在不会再出现: + +- loading 过程中先显示默认隐藏点 +- loading 结束后才消失 + +## 12. 图例机制 + +### 12.1 图例来源 + +图例运行态由以下数据共同决定: + +- 原始全量图例配置 +- 当前页面图例配置 +- 当前已勾选图层 +- 当前图例勾选状态 + +### 12.2 图例派生 + +图例最终显示的是 `legendDataSelected`,而不是全量图例。 + +派生逻辑核心原则如下: + +- 只有当前勾选图层对应的图例项才进入页面图例 +- 图例勾选状态是运行态绝对值 +- 环保设施分组有单独规则 + +### 12.3 图例加载时机 + +当前图例面板的 loading 只跟图例接口绑定: + +- 图例接口返回后立即停转 +- 图例内容在图层默认勾选同步后立即派生 +- 不再等待所有锚点接口完成 + +## 13. 筛选器机制 + +### 13.1 `MapFilter` 的职责 + +筛选器当前包含以下能力: + +- 时间范围筛选 +- 装机容量筛选 +- 搜索定位 +- 个别页面的专题筛选项 + +### 13.2 搜索下拉来源 + +搜索下拉使用 `mapDataStore.pointData` 作为数据源。 + +当前行为已经调整为: + +- 某个图层一旦命中缓存,筛选器即可出现该图层点位 +- 某个图层接口一旦返回成功,筛选器也会立即出现该图层点位 + +因此筛选器当前是“渐进展示”,而不是“全量完成后一次性展示”。 + +### 13.3 装机容量筛选 + +装机容量筛选针对 `eng_point` 图层,当前规则如下: + +- `all` + - 恢复大型与中型图例 +- `large_eng_built` + - 只保留 `large_eng_` 前缀相关图例 + - 地图上只显示大型电站点位 +- `mid_eng_built` + - 只保留 `mid_eng_` 前缀相关图例 + - 地图上只显示中型电站点位 + +当前容量切换不再只是影响筛选器本地下拉,而是会同步驱动: + +- 图例运行态 +- `eng_point` 点位显示 + +## 14. `eng_point` 专项规则 + +`eng_point` 是当前地图模块中的特殊图层,存在独立显示规则。 + +### 14.1 缩放规则 + +在数据入图阶段就生效: + +- 大型电站始终允许显示 +- 中型电站仅在 `zoom > 7.5` 时允许进入显示候选 + +这不是单纯的图例控制,而是点位入图时的过滤逻辑。 + +### 14.2 图例规则 + +点位入图之后,仍然必须再经过图例运行态控制: + +- 允许入图,不等于最终可见 +- 只要图例勾选为隐藏,该类点位就必须隐藏 + +### 14.3 容量筛选规则 + +装机容量下拉会进一步批量控制 `eng_point` 的图例项,使图例与点位同步切换。 + +## 15. 点位显示裁决链 + +当前点位最终是否显示,至少会经过以下几层判断: + +1. 图层是否勾选 +2. 图例是否勾选 +3. 基地范围是否允许 +4. `eng_point` 缩放过滤是否通过 +5. `distance` 密度分档是否通过 +6. 近邻点显示阶段是否通过 +7. 图标是否已加载完成 +8. 当前缩放下是否进入碰撞控制 + +这几层不是互斥关系,而是叠加裁决。 + +## 16. 点位渲染与样式 + +### 16.1 基础样式 + +当前点位渲染包括: - 图标 - 文字标签 -- 字号随缩放动态变化 -- 图标大小随缩放动态变化 -- 图例显隐状态 -- 基地区域显隐状态 -- 按 `distance` 字段做缩放级别抽稀 +- 标签换行与截断 +- 缩放相关的图标与文字样式变化 -### 9.1 当前样式生成逻辑 +### 16.2 图标加载约束 -当前渲染时会综合判断: +当前已经补充图标加载状态缓存: -- 是否有图标 URL -- 图例是否可见 -- 区域是否可见 -- 是否满足当前缩放级别下的距离阈值 +- 图标未加载完成时,不显示该点 +- 图标未加载完成时,文字也不先显示 +- 图标未加载完成时,不参与碰撞候选 -满足后才会返回 `Style`。 +这样可以避免“文字先出来、图标还是空的”的问题。 -### 9.2 当前标签处理 +## 17. 抽吸、密度与碰撞 -当前标签会做以下处理: +### 17.1 专项文档 -- 去掉括号 -- 最多显示两行 -- 每行长度受限 -- 超长文本截断并加省略号 -- 根据单行或多行情况设置不同偏移 +抽吸与密度分档的详细参数请查看: -这部分属于当前地图的统一标签显示策略。 +- `docs/地图模块-抽吸规则与参数说明.md` -## 10. Popup 机制 +### 17.2 当前裁决顺序 -文件: +当前点位显示顺序可以理解为: -- [popup-manager.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/ol/popup-manager.ts) +1. 密度分档 +2. 近邻抽吸 +3. 图标碰撞 +4. 文字碰撞 -Popup 分为两类: +补充约束: -- hover 时显示的单点 popup -- 高缩放下批量显示的 popup +- 只有满足可见性条件的点,才能进入碰撞候选集 +- 被图例或缩放过滤掉的点,不能再去占用碰撞资格 +- 视图内无近邻的孤立点允许局部放行 -### 10.1 Hover Popup +### 17.3 高缩放特例 -基础 hover popup 由地图鼠标移动事件驱动: +当前在高缩放层级 `zoom >= 15` 时,行为与中低缩放不同: -- 检测当前 hover 的要素 -- 更新悬停状态 -- 在对应坐标显示 popup +- 只处理视口内点位 +- 点与文字不再做碰撞淘汰 +- 视口内符合基础显示条件的点和文字全部显示 +- 同时进入批量 popup 模式 -### 10.2 批量 Popup +这条规则的目标是: -在较高缩放级别下,会进入批量 popup 模式: +- 保证深度放大后信息完整可见 +- 避免高缩放下还因为碰撞把点或文字压掉 -- 获取当前视口内所有可见点位 -- 逐个测量 popup 尺寸 -- 做边界框碰撞判断 -- 只渲染不冲突的 popup +## 18. Popup 机制 -这个机制的主要目的是避免高缩放下 popup 过多时完全覆盖地图。 +### 18.1 两类 popup -## 11. 基础底图与 GIS 图层 +当前 popup 分成两类: -基础底图和 GIS 叠加图层相关能力主要集中在: +- hover popup +- batch popup -- `map.ol.ts` -- `mapurlManage.ts` -- `gisUtils.ts` +### 18.2 hover popup -### 11.1 `mapurlManage.ts` +鼠标悬停时由 `pointermove` 驱动,只在非 batch popup 模式下显示。 -文件: +### 18.3 batch popup -- [mapurlManage.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/mapurlManage.ts) +在 `zoom >= 15` 时进入批量 popup 模式: -主要维护: +- 只基于当前视口内点位 +- 拖动地图时持续刷新 +- 但通过 `requestAnimationFrame` 做节流 +- `moveend` 后立即再做一次强制刷新 -- GeoServer 服务地址 -- WMTS 配置 -- XYZ 配置 -- 矢量图层相关配置 -- 某些专题图层和图例显示参数 +当前这样做是为了兼顾两件事: -### 11.2 `gisUtils.ts` +- 保留“拖动时 popup 跟着更新”的原交互 +- 避免拖动过程中每个事件都重建整批 DOM -文件: +### 18.4 惯性平移已关闭 -- [gisUtils.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/gisUtils.ts) +当前地图拖动已经关闭 OpenLayers 默认惯性平移: -主要负责: +- 普通拖动仍然保留 +- 松手后不再继续滑动一小段 -- 图层树拍平 -- 地图配置补全 -- 底图配置转换 -- 部分标签/偏移相关工具逻辑 +这样可以减少高缩放 popup 场景下“松手还轻微位移”的观感问题。 -## 12. 图层树与图例 +## 19. 基地与区域过滤 -### 12.1 图层树 UI +基地切换是当前地图模块的关键能力之一,主要影响: -文件: +- 地图裁切范围 +- 当前基地相关点位的可见性 +- 部分专题图层显示 -- [LayerController.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapController/LayerController.vue) +整体过程通常是: -主要职责: +1. 外部基地选择源变化 +2. 编排器接收到基地 ID +3. 地图实例切换基地范围 +4. 点位更新 `_regionVisible` +5. 样式函数按区域可见性决定是否显示 -- 展示右侧图层树 -- 响应勾选变化 -- 把勾选结果转发给编排器和 Store +## 20. 典型交互链路 -当前图层树里还包含若干业务互斥规则,例如: +### 20.1 图层树勾选 -- 视频站和 AI 视频站互斥 -- 环保设施和环保设施在建互斥 -- 珍稀鱼类和沿程鱼类互斥 - -### 12.2 图例 UI - -文件: - -- [mapLegend/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapLegend/index.vue) - -主要职责: - -- 展示当前页面和当前已选图层对应的图例 -- 维护图例项勾选状态 -- 把图例点击行为分发给编排器 - -图例不是固定写死的,而是由后端返回配置和当前已选图层共同决定。 - -## 13. 地图筛选与控制器 - -### 13.1 `MapFilter` - -文件: - -- [mapFilter/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapFilter/index.vue) - -当前承担的筛选功能包括: - -- 基地筛选 -- 时间筛选 -- 装机容量筛选 -- 关键字搜索 -- 个别专题下的额外筛选 - -### 13.2 `MapController` - -文件: - -- [mapController/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapController/index.vue) - -当前承担的地图操作包括: - -- 放大缩小 -- 图层树显隐 -- 2D / 3D 切换 -- 量算 -- 截图 -- 梯级流域专题显示 - -### 13.3 `BaseLayerSwitcher` - -文件: - -- [BaseLayerSwitcher/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/BaseLayerSwitcher/index.vue) - -用于切换: - -- 矢量底图 -- 地形底图 -- 影像底图 - -## 14. Store 分层 - -### 14.1 旧主 Store - -文件: - -- [map.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/store/modules/map.ts) - -当前仍然承担大量地图运行态逻辑,例如: - -- 图层选中状态 -- 图例选中状态 -- 图层数据加载 -- 图例联动 -- 点位缓存 -- 图层更新 - -### 14.2 新拆分 Store - -文件: - -- [map-config.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-config.store.ts) -- [map-data.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-data.store.ts) -- [map-view.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-view.store.ts) - -当前已经开始按职责拆分: - -- `map-config.store.ts` - - 维护图层树和图例配置 - -- `map-data.store.ts` - - 维护图层数据缓存和点位数据 - -- `map-view.store.ts` - - 维护视图相关运行态,例如选中图层、图例勾选、缩放级别、基地状态 - -这说明地图模块已经在往“编排层 + Store 分层 + 引擎执行层”方向演进。 - -## 15. 基地裁切与区域过滤 - -基地逻辑是当前地图模块里的关键能力之一。 - -入口主要在: - -- `GisView.vue` -- `map-orchestrator.ts` -- `map.ol.ts` - -整体过程大致如下: - -1. 选择基地 -2. 编排器接收基地变化 -3. 调用地图实例更新基地范围 -4. 请求或读取基地边界 GeoJSON -5. 对点位执行区域内外判断 -6. 更新 Feature 的 `_regionVisible` -7. 地图样式函数据此决定是否显示 - -除了点位控制外,基地切换还会影响底图裁切和专题图层显示。 - -## 16. 后台配置与前台运行时关系 - -地图模块不仅依赖前台代码,也强依赖后台配置。 - -### 16.1 后台管理入口 - -主要页面位于: - -- [views/system/map/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/views/system/map/index.vue) -- [views/system/map/components/LayerManagement/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/views/system/map/components/LayerManagement/index.vue) - -### 16.2 后台管理内容 - -后台可维护的内容主要包括: - -- 图层树结构 -- 图层是否启用 -- 图层接口地址 -- 图层基础参数 -- 图例元数据 - -### 16.3 前后台关系 - -运行时不是前端把所有图层写死,而是: - -- 后台维护元数据 -- 前端进入页面后加载配置 -- 再根据配置请求对应图层数据 -- 最后交给地图引擎渲染 - -因此地图模块本质上是“配置驱动 + 运行时渲染”的模式。 - -## 17. 当前调用链总结 - -### 17.1 页面初始化链路 - -整体调用链如下: - -1. `AppMain` 挂载 `GisView` -2. `GisView` 调用 `mapOrchestrator.mountView` -3. `mapOrchestrator` 调用 `MapClass.init` -4. `MapClass` 实际调用 `MapOl.init` -5. 编排器加载图层树和图例配置 -6. 编排器初始化底图 -7. 编排器加载点位数据 -8. `PointLayerManager` 创建点图层并写入 Feature -9. `MapOl` 通过样式函数完成渲染 - -### 17.2 图层勾选链路 - -整体调用链如下: - -1. 用户勾选 `LayerController` -2. 编排器接收图层选择结果 -3. Store 更新图层选中状态 +1. `LayerController` 触发勾选变化 +2. 编排器归一化图层 key +3. `map.ts` 更新运行态图层勾选 4. 地图实例更新图层显隐 5. 图例重新派生 -### 17.3 图例点击链路 +补充: -整体调用链如下: +- `GISMap` 勾选时,如果地图里没有该图层,会先补载,再显示 -1. 用户点击 `MapLegend` -2. 编排器接收图例切换 -3. Store 更新图例选中状态 -4. 地图实例更新图例对应点位显隐 +### 20.2 图例勾选 -### 17.4 筛选链路 +1. 图例组件触发点击 +2. 编排器转发图例切换 +3. `map.ts` 更新图例运行态 +4. 对应点位立即更新显隐 +5. 图例树同步更新灰态 -整体调用链如下: +### 20.3 筛选器操作 -1. 用户操作 `MapFilter` -2. 编排器接收搜索、时间、基地或容量变化 -3. Store 更新运行态 -4. 必要时删图层、清缓存、重新请求数据 -5. 地图引擎重新渲染 +1. 用户切换时间、容量、搜索等条件 +2. 编排器接收命令 +3. 视情况更新视图运行态或重载数据 +4. 地图实例与图例运行态同步刷新 -## 18. 当前模块特点 +### 20.4 缩放操作 -从现有实现看,地图模块有以下几个明显特点: +1. OpenLayers 视图触发 `change:resolution` +2. 编排器更新当前缩放级别 +3. 处理 `eng_point` 阈值联动 +4. 水电开发菜单下处理动态图层联动 +5. `map.ol.ts` 处理高缩放 popup 与点位显示策略 -- 地图容器是全局常驻的 -- 页面切换主要依赖 `pageKey` 重新编排 -- OpenLayers 是当前 2D 主引擎 -- 地图运行态是配置驱动的 -- 点位图层按业务图层分层组织 -- 图层树、图例、筛选、基地切换之间耦合较深 -- 编排层已经出现,但部分旧逻辑仍在 Store 和引擎层混合存在 - -## 19. 相关文件清单 - -地图模块当前重点文件如下: +## 21. 当前重点文件清单 - `src/components/gis/GisView.vue` - `src/components/gis/map.class.ts` @@ -605,18 +595,27 @@ Popup 分为两类: - `src/components/mapController/index.vue` - `src/components/mapController/LayerController.vue` - `src/components/BaseLayerSwitcher/index.vue` -- `src/views/system/map/index.vue` -- `src/views/system/map/components/LayerManagement/index.vue` -## 20. 结论 +## 22. 当前结论 -当前地图模块已经形成了比较清晰的主干结构: +当前地图模块已经形成比较清晰的主干结构: -- `GisView` 作为入口层 -- `map-orchestrator` 作为编排层 -- `MapClass` 作为统一门面 -- `MapOl` 作为 2D 主执行层 -- 多个 Store 共同维护配置、数据和视图状态 -- 图层树、图例、筛选器、底图切换器作为前台交互入口 +- `GisView` 负责挂载与页面入口 +- `map-orchestrator` 负责业务编排 +- `MapClass` 负责能力门面 +- `MapOl` 负责 2D 主执行 +- `map.ts + 三个拆分 store` 共同维护配置、数据与视图运行态 +- 图层树、图例、筛选器、popup、高缩放规则都已经围绕这条主干收口 -如果后续继续扩展专题图层、复杂筛选、点位布局和高缩放交互,建议都优先沿着这条主干扩展,而不是再回到页面组件里直接堆业务逻辑。 +相较旧版说明,当前最重要的现状变化有 8 点: + +1. 图例 loading 已与锚点全量加载解耦 +2. 筛选器数据已支持渐进展示 +3. 初始化阶段默认隐藏图例项会立即生效 +4. 运行态勾选不会在 loading 完成后被默认值覆盖 +5. `eng_point` 有独立的缩放与容量规则 +6. 高缩放下视口内点和文字全部显示,不再做碰撞淘汰 +7. batch popup 保留拖动跟随,但增加了节流 +8. 地图拖动惯性已关闭 + +后续如果继续扩展地图功能,建议仍然优先沿着“编排层 + Store 分层 + OpenLayers 执行层”这条主干扩展,不要再把复杂业务回填到页面组件里。 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 01f126a5..36f4ab18 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,6 +7,10 @@ const appStore = useAppStore();