将数据管理子系统的页面挪到全过程新框架,自定义筛选列打开时会自己刷新表格一下,
环保数据改为监测数据,操作列去掉,,,把地图配置,无用api,无用代码,组件去掉。
@ -1,479 +0,0 @@
|
||||
# 地图模块 3D Cesium 实施计划
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在现有地图体系下补齐 `3D(Cesium)` 视图能力,使其尽量复用 `2D(OpenLayers)` 的业务编排结果,满足以下要求:
|
||||
|
||||
- 3D 默认进入中国视角,不再落到美国区域。
|
||||
- 3D 初始化时显示过渡动画,再定位到中国。
|
||||
- 3D 保留基础底图加载能力,但不提供底图切换面板。
|
||||
- 3D 支持锚点展示。
|
||||
- 3D 支持与 2D 一致的鼠标悬停 popup,样式复用现有 popup。
|
||||
- 3D 支持与 2D 一致的锚点点击行为,点击后打开同一套详情弹框逻辑。
|
||||
- 菜单切换、图层树勾选、图例勾选后,3D 锚点与 2D 一样联动变化。
|
||||
- 3D 必须复用 `eng_point` 的层级显示规则,中型水电站仅在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。
|
||||
|
||||
## 2. 2D 现状梳理
|
||||
|
||||
当前 `2D` 的能力并不是散落在页面里,而是已经形成了比较清晰的分层:
|
||||
|
||||
### 2.1 页面入口
|
||||
|
||||
- `src/components/gis/GisView.vue`
|
||||
- 负责挂载地图容器、popup 容器、地图控制器、图例、图层树。
|
||||
- 页面只调用 `mapOrchestrator.mountView()` 和 `handlePageChange()`,不直接拼地图业务。
|
||||
|
||||
### 2.2 地图编排层
|
||||
|
||||
- `src/modules/map/application/map-orchestrator.ts`
|
||||
- 负责页面初始化、菜单切换、图层配置加载、图例配置加载、基地切换、缩放联动。
|
||||
- 2D/3D 都应尽量复用这一层,避免在 Cesium 侧另起一套业务流程。
|
||||
|
||||
### 2.3 地图统一门面
|
||||
|
||||
- `src/components/gis/map.class.ts`
|
||||
- 对外暴露统一接口,如:
|
||||
- `addBaseDataLayer`
|
||||
- `addInitDataLayer`
|
||||
- `mdLayerTreeShowOrHidden`
|
||||
- `setLegendPointVisible`
|
||||
- `controlBaseLayerTreeShowAndHidden`
|
||||
- `initPopupOverlay`
|
||||
- `flyTopanto`
|
||||
- 业务层只认这套接口,不应感知底层是 `OpenLayers` 还是 `Cesium`。
|
||||
|
||||
### 2.4 2D 地图能力落点
|
||||
|
||||
- `src/components/gis/map.ol.ts`
|
||||
- 已实现的关键能力:
|
||||
- 默认中国视角初始化
|
||||
- 基础底图加载与切换
|
||||
- 点位图层注册和显隐
|
||||
- 图例控制单类锚点显隐
|
||||
- 鼠标 hover popup
|
||||
- click 打开详情弹窗
|
||||
- 基地裁切与底图遮罩
|
||||
- 缩放/移动后的批量 popup 和标签刷新
|
||||
|
||||
### 2.5 2D 锚点与 popup 支撑
|
||||
|
||||
- `src/components/gis/ol/point-layer-manager.ts`
|
||||
- 负责将点位数据转成地图要素、分层、索引、图例显隐。
|
||||
- `src/components/gis/ol/popup-manager.ts`
|
||||
- 负责 hover 命中、popup DOM 内容复用、批量 popup。
|
||||
|
||||
### 2.6 图层树和图例交互
|
||||
|
||||
- `src/components/mapController/LayerController.vue`
|
||||
- 图层树勾选后走 `mapOrchestrator.applyLayerSelection()`
|
||||
- `src/components/mapLegend/index.vue`
|
||||
- 图例点击后走 `mapOrchestrator.toggleLegend()` / `toggleLegendBatch()`
|
||||
- `src/store/modules/map.ts`
|
||||
- 真正把缓存数据、图层状态、图例状态应用到地图实例
|
||||
- 也就是说,`3D` 只要把统一接口补齐,就能直接接住现有业务流
|
||||
|
||||
### 2.7 2D 已有特殊业务规则
|
||||
|
||||
- 锚点点击逻辑已在 `src/components/gis/map.ol.ts` 实现:
|
||||
- 命中锚点后,`ylfb` 走 `ylfbModalVisible`
|
||||
- 其他锚点走 `modalVisible`
|
||||
- 标题和参数都直接取当前锚点数据
|
||||
- `eng_point` 的中型水电站显示阈值已在现有链路中实现:
|
||||
- 阈值常量位于 `src/modules/map/application/map-orchestrator.ts`
|
||||
- 过滤逻辑位于 `src/store/modules/map.ts` 的 `filterPointLayerDataForDisplay()`
|
||||
- `3D` 不应重新发明一套规则,而应复用这套现有过滤结果
|
||||
- 部分点位会随着缩放级别提升显示得更多,不是简单的“图层开/关”:
|
||||
- `eng_point` 的中型电站要到 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上才显示
|
||||
- 水电开发页在缩放跨过 `12` 级时会自动补开一批动态图层
|
||||
- `2D` 内部还存在点位密度阈值、近邻点展开、批量 popup 阈值等缩放联动规则
|
||||
- `2D` 对锚点存在碰撞检测与密度控制,不是所有点始终全量显示:
|
||||
- 图标和标签会分别经过碰撞计算
|
||||
- 低缩放下会因为密度、碰撞、区域裁切、图例状态而隐藏部分点
|
||||
- 高缩放下会逐步放开限制,显示更多锚点和标签
|
||||
|
||||
## 3. 3D 当前现状与缺口
|
||||
|
||||
### 3.1 已有内容
|
||||
|
||||
- `src/components/gis/map.cesium.ts`
|
||||
- 当前只完成了最小化 `Cesium.Viewer` 初始化。
|
||||
- 已有基础缩放、截图、`flyTopanto()` 雏形。
|
||||
|
||||
### 3.2 当前主要缺口
|
||||
|
||||
- `init()` 里虽然定义了 `flyToChina()`,但没有真正执行,也没有 `resolve(viewer)`,初始化流程不完整。
|
||||
- 默认底图仍是 ArcGIS 全球影像,因此启动时容易落到美国默认视角。
|
||||
- `addBaseDataLayer()` 为空,无法接住当前 `GISMap/customBaseLayer` 逻辑。
|
||||
- `addInitDataLayer()` 为空,无法展示锚点。
|
||||
- `mdLayerTreeShowOrHidden()` 为空,菜单切换时 3D 锚点不会同步显隐。
|
||||
- `setLegendPointVisible()` 为空,图例勾选不会影响 3D 锚点。
|
||||
- `initPopupOverlay()` 为空,无法复用 2D popup 容器。
|
||||
- 缺少点击命中与详情弹框联动,3D 当前无法做到“点击锚点与 2D 同行为”。
|
||||
- `controlBaseLayerTreeShowAndHidden()` 为空,3D 基础底图和叠加层无法受现有图层树控制。
|
||||
- 尚未验证 `eng_point` 的缩放阈值规则在 3D 下是否能完全复用,存在实现时遗漏中型站点显隐的风险。
|
||||
- 目前没有承接 `2D` 中“放大到具体层级显示更多锚点”的缩放联动显示机制。
|
||||
- 目前没有承接 `2D` 中图标/标签的碰撞检测逻辑,后续若直接全量渲染,视觉效果会与 `2D` 明显不一致。
|
||||
- 当前没有建立 `Cesium entity / datasource / imageryLayer` 的注册表,后续联动无从下手。
|
||||
|
||||
## 4. 实施原则
|
||||
|
||||
- 不重做业务编排,优先复用 `map-orchestrator.ts` 和 `src/store/modules/map.ts`。
|
||||
- 不额外引入一套 3D 专用图例和图层状态,仍以现有 store 为唯一运行态来源。
|
||||
- 不单独复制 `eng_point` 的中型站显示规则,优先复用现有 `mapStore` 过滤结果。
|
||||
- 不单独新建一套点击弹框协议,优先保持与 `map.ol.ts` 相同的命中后行为。
|
||||
- 对“缩放越大显示越多锚点”这类规则,优先复用现有缩放阈值与显示门闩,不做拍脑袋的新规则。
|
||||
- 对碰撞检测,优先与 `2D` 保持同方向行为:低缩放控量,高缩放放开,而不是 3D 全量堆点。
|
||||
- 先做最小闭环,再逐步补能力:
|
||||
- 第一步保证 3D 能正确初始化并定位中国。
|
||||
- 第二步保证基础底图能显示。
|
||||
- 第三步保证锚点能显示。
|
||||
- 第四步保证 popup、图层树、图例联动打通。
|
||||
- 3D 不做“基础地图切换 UI”,但底层仍要支持 `customBaseLayer + addBaseDataLayer(..., true)` 这一逻辑。
|
||||
|
||||
## 5. 分步骤实施计划
|
||||
|
||||
### Step 1:修正 3D 初始化并默认定位中国 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 切到 3D 后稳定初始化 viewer。
|
||||
- 首屏出现一个轻量加载动画。
|
||||
- 初始化完成后飞到中国默认视角。
|
||||
|
||||
计划:
|
||||
|
||||
- 修复 `map.cesium.ts` 的 `init()`:
|
||||
- 保证初始化后一定 `resolve(this.viewer)`。
|
||||
- 执行 `flyToChina()`。
|
||||
- 补一个可控的启动过渡动画。
|
||||
- 统一中国默认参数:
|
||||
- 中心经纬度建议复用 `104.5, 36.5`
|
||||
- 相机高度先按全国视角收口,后续再微调
|
||||
- 如果切回 2D 再切到 3D,仍然要能正确重建 viewer。
|
||||
|
||||
验收:
|
||||
|
||||
- 点击 `3D` 按钮后,不再出现美国默认视角。
|
||||
- 首屏有加载过渡。
|
||||
- 进入后稳定落在中国。
|
||||
|
||||
### Step 2:展示 3D 基础底图 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 不显示空球。
|
||||
- 保留“基础底图 + 3D 专用底图地址”能力。
|
||||
- 不提供底图切换面板,但底层能吃现有配置。
|
||||
|
||||
计划:
|
||||
|
||||
- 在 `MapCesium` 内建立基础图层注册表。
|
||||
- 实现 `addBaseDataLayer(layer, isShow)`:
|
||||
- 对 `customBaseLayer` 使用 `url_3d`
|
||||
- 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力
|
||||
- **必须处理 `wmts` 类型图层**:2D 通过 `WMTS` source 加载,3D 使用 `Cesium.WebMapTileServiceImageryProvider`,从 URL 参数中提取 LAYER/TILEMATRIXSET
|
||||
- **必须提供 `tileMatrixLabels`**:Cesium 默认用纯数字作为 tileMatrix 参数值,但本系统 WMTS 服务器要求 `TileMatrixSet:zoom` 格式(如 `EPSG:3857_hbb_zrbhq_l13:4`)。需构建 `["TileMatrixSet:0", "TileMatrixSet:1", ...]` 传入 `tileMatrixLabels`。
|
||||
- 其他类型:`raster-dem`→XYZ、MapServer→ArcGIS、其余 →WMS
|
||||
- **注意**:还存在一个 `key = "powerBaseStation"` 的基地底图图层,该图层**初始化时不加载**(已在 `IGNORED_BASE_LAYER_KEYS` 中忽略),但后续可通过图层树手动勾选显示。
|
||||
- 实现 `controlBaseLayerTreeShowAndHidden()`:
|
||||
- 让图层树勾选能控制 3D 底图显隐
|
||||
- 根据你的要求,UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。
|
||||
|
||||
验收:
|
||||
|
||||
- 切到 3D 后可稳定看到基础底图。
|
||||
- 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。
|
||||
|
||||
### Step 3:打通 3D 锚点展示 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 能展示和 2D 同源的锚点数据。
|
||||
- 菜单切换后,3D 能跟随页面数据切换。
|
||||
|
||||
计划:
|
||||
|
||||
- 在 `MapCesium` 内建立点位图层注册结构,按 `layerKey -> entity[]` 管理。
|
||||
- 实现 `addInitDataLayer(pointData, layerType)`:
|
||||
- 复用当前点位数据结构
|
||||
- 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml`
|
||||
- 复用现有图标资源路径
|
||||
- Entity ID 使用 `${layerKey}:${item._id || `${iconType || 'point'}_${stcd}`}`,与 2D 对齐,确保同 stcd 不同 sttpMap 的点不被覆盖
|
||||
- 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现
|
||||
- 为后续缩放驱动的增量显示预留运行态字段(`_legendVisible`、`_regionVisible`、`_densityVisible`),不把所有点简单一次性硬渲染
|
||||
- 实现 `mdLayerTreeShowOrHidden(layerType, checked)`:
|
||||
- 图层树控制整个点图层显隐
|
||||
- 实现 `removePointLayer()` / `hasLayer()`
|
||||
- 便于后续页面切换和重载
|
||||
|
||||
验收:
|
||||
|
||||
- 切到 3D 后当前页默认勾选图层的锚点能显示。
|
||||
- 切换菜单后,3D 锚点跟随变化。
|
||||
- 取消勾选图层后,对应锚点消失。
|
||||
- `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。
|
||||
|
||||
### Step 4:打通 3D 基地裁切(jdPanelControlShowAndHidden) ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 点击基地面板后,与 2D 行为一致。
|
||||
- 加载基地边界 GeoJSON,只显示区域内锚点,隐藏区域外锚点。
|
||||
- 取消基地时恢复显示所有锚点。
|
||||
|
||||
计划:
|
||||
|
||||
- 在 `MapCesium` 中捕获 `hydropBase` 配置,与 2D 一样在 `addBaseDataLayer` 中记录。
|
||||
- 实现 `jdPanelControlShowAndHidden(baseid, isAll)`:
|
||||
- `isAll=false`:清除 3D 球面遮罩实体、恢复所有锚点区域可见性、清除裁切状态。
|
||||
- `isAll=true`:先隐藏所有锚点,异步加载基地边界 GeoJSON,执行点在面判断后仅显示区域内锚点,同时在球面绘制裁切边界多边形(`applyRegionMask`),最后飞行到区域视野。
|
||||
- 在 `createEntityPropertiesBag` 中新增 `_regionVisible` 属性,初始为 `true`。
|
||||
- 在 `applyPointVisibilityRefresh` 中跳过 `_regionVisible !== false` 的实体,确保裁切后碰撞检测不会把区域外锚点重新显示出来。
|
||||
- 复用 `extractPolygonCoords` / `isPointInRing` 射线法判断逻辑(与 `region-mask-manager.ts` 一致)。
|
||||
- 支持 `AbortController` 取消前一次未完成的请求,避免响应顺序错乱。
|
||||
- `applyRegionMask` 将 GeoJSON 多边形转换为 Cesium `PolygonHierarchy`,在球面绘制半透明填充 + 可见边界的区域多边形,模拟 2D 底图 Canvas 裁切遮罩的视觉效果。
|
||||
|
||||
验收:
|
||||
|
||||
- 点击基地后,3D 只显示该基地区域内的锚点,区域外锚点隐藏。
|
||||
- 取消基地后,所有锚点恢复正常显示。
|
||||
- 快速切换基地不会出现旧数据覆盖新数据的问题。
|
||||
- 多次切换 2D/3D 后基地裁切行为正常。
|
||||
|
||||
### Step 5:打通 3D popup 与 hover ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 鼠标悬停时显示与 2D 一样的 popup。
|
||||
- popup 样式复用现有 DOM 和样式,不再单独做一版。
|
||||
|
||||
计划:
|
||||
|
||||
- 实现 `initPopupOverlay(popupContainer)`
|
||||
- 继续复用 `GisView.vue` 中现有 popup 容器
|
||||
- 在 Cesium 中增加鼠标移动命中:
|
||||
- 基于 `ScreenSpaceEventHandler`
|
||||
- hover 到锚点时读取 entity 附带属性
|
||||
- popup 内容生成逻辑复用 2D 现有字段:
|
||||
- 优先使用 `popupHtml`
|
||||
- 否则走通用字段渲染
|
||||
- 处理离开锚点时隐藏 popup。
|
||||
|
||||
验收:
|
||||
|
||||
- 鼠标移入锚点出现 popup。
|
||||
- 样式和 2D 保持一致。
|
||||
- 移出后 popup 正常消失。
|
||||
|
||||
### Step 6:补齐缩放驱动显示与碰撞检测
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 在低缩放下控制锚点数量,避免全量堆叠。
|
||||
- 3D 在放大到具体层级后,逐步显示更多锚点。
|
||||
- 3D 的图标/标签显示效果尽量接近 2D,不出现严重遮挡。
|
||||
|
||||
计划:
|
||||
|
||||
- 先承接现有明确可复用的缩放规则:
|
||||
- `eng_point` 的 `7.5` 阈值
|
||||
- 水电开发页的 `12` 级动态图层联动
|
||||
- 再补 3D 侧的显示门闩:
|
||||
- 低缩放下根据缩放级别、图层状态、图例状态控制 entity 显隐
|
||||
- 高缩放下逐步放开限制,显示更多锚点
|
||||
- 为 3D 补一版轻量碰撞控制:
|
||||
- 至少先控制标签碰撞,避免名称完全叠在一起
|
||||
- 如有必要,再补图标级碰撞或屏幕网格抽稀
|
||||
- 明确与 2D 对齐的目标不是“算法完全一样”,而是“视觉行为一致”:
|
||||
- 低缩放控量
|
||||
- 高缩放增量显示
|
||||
- 关键锚点不被完全淹没
|
||||
|
||||
验收:
|
||||
|
||||
- 3D 在全国视角下不会一次性挤满所有锚点。
|
||||
- 放大到具体层级后会看到更多锚点逐步出现。
|
||||
- 标签和锚点不会大面积重叠到不可读。
|
||||
|
||||
### Step 7:打通 3D 锚点点击详情 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 3D 点击锚点后,行为与 2D 一致。
|
||||
- 继续复用现有详情弹框状态和参数结构。
|
||||
|
||||
计划:
|
||||
|
||||
- 在 Cesium 中增加点击命中:
|
||||
- 基于 `ScreenSpaceEventHandler`
|
||||
- 命中锚点后读取 entity 绑定的原始业务数据
|
||||
- 复用 `2D` 当前点击行为:
|
||||
- `sttpMap === 'ylfb'` 时打开 `ylfbModalVisible`
|
||||
- 其他锚点打开 `modalVisible`
|
||||
- `params`、`title` 赋值逻辑与 `map.ol.ts` 保持一致
|
||||
- 避免 hover popup 与 click 弹框互相干扰。
|
||||
|
||||
验收:
|
||||
|
||||
- 点击 3D 锚点后可打开与 2D 一致的详情弹框。
|
||||
- `ylfb` 和普通锚点分支行为一致。
|
||||
- 点击空白区域不会误触发弹框。
|
||||
|
||||
### Step 8:打通图例联动 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 图例勾选后,3D 只显示对应状态的锚点。
|
||||
|
||||
计划:
|
||||
|
||||
- 实现 `setLegendPointVisible(layerKey, anchoPointState, checked)`:
|
||||
- 复用 `anchoPointState` 作为图例匹配字段
|
||||
- entity 级别维护显示状态
|
||||
- 如果后续需要,也可补一个 3D 版本的图例索引表,避免每次全量遍历。
|
||||
|
||||
验收:
|
||||
|
||||
- 图例单项勾选/取消时,3D 对应类别锚点同步显隐。
|
||||
- 图例分组勾选/取消时,3D 行为与 2D 保持一致。
|
||||
|
||||
### Step 9:补足收口与稳定性 ✅️
|
||||
|
||||
目标:
|
||||
|
||||
- 让 3D 能稳定参与现有地图生命周期。
|
||||
|
||||
计划:
|
||||
|
||||
- 清理 Cesium 事件监听、viewer、entity、datasource,避免重复切换内存泄漏。
|
||||
- 检查 `switchView('2D' | '3D')` 前后 popup、底图、图层注册是否被正确重置。
|
||||
- 检查 hover、click、图例、图层树、缩放阈值这几条联动链路在 3D 是否完全闭环。
|
||||
- 视情况补最小诊断日志,确认菜单切换与图层联动没有漏项。
|
||||
|
||||
验收:
|
||||
|
||||
- 2D/3D 多次来回切换不报错。
|
||||
- 菜单切换、图层切换、图例切换行为稳定。
|
||||
|
||||
## 6. 推荐落地顺序
|
||||
|
||||
按你的要求,建议严格按下面顺序推进:
|
||||
|
||||
1. 默认定位到中国 + 启动动画
|
||||
2. 展示基础底图
|
||||
3. 展示锚点 3.5. 补基地裁切(jdPanelControlShowAndHidden)
|
||||
4. 补 hover popup
|
||||
5. 补缩放驱动显示与碰撞检测
|
||||
6. 补锚点点击详情
|
||||
7. 补图层树联动
|
||||
8. 补图例联动
|
||||
9. 做切换稳定性收口
|
||||
|
||||
## 7. 预计修改文件
|
||||
|
||||
- `src/components/gis/map.cesium.ts`
|
||||
- 3D 地图主体实现,主改动文件
|
||||
- `src/components/gis/map.class.ts`
|
||||
- 视情况补齐 2D/3D 切换后 popup 初始化和服务切换细节
|
||||
- `src/components/gis/GisView.vue`
|
||||
- 视情况控制 3D 下的底图切换器显示策略、加载态挂载位置
|
||||
- `src/components/mapController/index.vue`
|
||||
- 若需要补 3D 初始化后的交互控制
|
||||
|
||||
## 8. 当前建议
|
||||
|
||||
建议先只做 `Step 1`,也就是:
|
||||
|
||||
- 修正 Cesium 初始化完成逻辑
|
||||
- 增加 3D 启动加载动画
|
||||
- 默认飞到中国
|
||||
|
||||
这一步最小、最容易验证,也能先把“打开 3D 看起来不对”的核心问题解决掉。
|
||||
|
||||
## 9. 必须补齐的隐藏逻辑清单
|
||||
|
||||
下面这些逻辑不是表面接口能直接看出来的,但如果 `3D` 不补,最终效果就会和 `2D` 明显不一致。
|
||||
|
||||
### 9.1 图层与菜单切换 ✅️
|
||||
|
||||
- ✅ 菜单切换时,只隐藏旧页点图层,不隐藏底图。(编排器 `handlePageChange` 控制,Cesium 被动接收数据)
|
||||
- ✅ 图层树勾选前要先经过互斥规则归一化:(store 层 `updateLayerData` 处理,Cesium 不感知)
|
||||
- `rare_fish_point` 与 `fish_along_point` 互斥
|
||||
- `stinfo/stinfo_video_point` 与 `stinfo_ai_video_point` 互斥
|
||||
- `facilities` 与 `facilities_built` 两个分支互斥
|
||||
- ✅ 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。`mdLayerTreeShowOrHidden` 设置 `_layerVisible` 标志,不删除 `pointLayerRegistry` 条目。
|
||||
- ✅ **图层树勾选时按 `type` 字段走不同分支**(见 `updateLayerData`,store 层处理):
|
||||
- `type === 'pointMap'` → 走 `handlePointMapLayer` → 调用 `mdLayerTreeShowOrHidden`(已实现,L1624)
|
||||
- `type === 'GISMap'` → 走 `controlBaseLayerTreeShowAndHidden`(已实现,L1345)
|
||||
- 3D Cesium 已同时实现这两个方法。
|
||||
|
||||
### 9.2 图例与点内显隐 ✅️
|
||||
|
||||
- ✅ 图例控制的是图层内部锚点是否可见,不是图层本身是否可见。`setLegendPointVisible` 使用 `_legendVisible` 标志,与 `_layerVisible` 独立。
|
||||
- ✅ 图层恢复显示时,图例要先整层全关,再按当前 checked 状态逐项恢复。支持 `anchoPointState=''` 全关 → 逐项 `(state, true)` 恢复两步模式(L1764-1787)。
|
||||
- ✅ 环保设施图例组当前是只展示不交互。(编排器层面控制,Cesium 不感知)
|
||||
|
||||
### 9.3 缩放驱动显示 ✅️
|
||||
|
||||
- ✅ `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。(store `filterPointLayerDataForDisplay` 在数据进入 Cesium 前过滤)
|
||||
- ✅ 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。(编排器层面处理)
|
||||
- ✅ `2D` 存在"缩放越大显示越多"的行为。碰撞检测中密度门控逐级放开。
|
||||
- ✅ `2D` 还存在 hover、批量 popup、密度控制等缩放阈值:
|
||||
- hover 低于 ~zoom 4.5 不触发(`HOVER_POPUP_MAX_HEIGHT = 9_000_000`)
|
||||
- 高缩放进入批量 popup 模式(`BATCH_POPUP_MODE_ZOOM = 14.9`)
|
||||
- 点位随着缩放提升逐步放开密度限制(密度门控 + `DENSITY_ISOLATION_THRESHOLD`)
|
||||
- ✅ **图标动态缩放**:`CallbackProperty` 实现 `billboard.scale`,公式 `clamp(0.7 + (zoom - 4.5) * 0.035, 0.4, 1.2)`(L1522)。
|
||||
- ✅ **字体动态大小**:`CallbackProperty` 实现 `label.font`,钳位 `[10, 20]`(L1535)。
|
||||
- ✅ **文字在图标上方**:`CallbackProperty` 实现 `label.pixelOffset`,负 Y 偏移(L1549)。
|
||||
- ✅ **Cesium 中必须使用 `CallbackProperty` 实现动态缩放**:已使用 3 个 `CallbackProperty`(scale/font/offset),`isConstant=false` 每帧重算。
|
||||
|
||||
### 9.4 锚点碰撞与控量 ✅️
|
||||
|
||||
- ✅ `2D` 中锚点是否可见,不只受图层开关控制,还受以下门闩共同影响:
|
||||
- 图例可见性 `_legendVisible`
|
||||
- 区域裁切可见性 `_regionVisible`
|
||||
- 密度阈值(碰撞循环中 inline 计算,逐级放开)
|
||||
- 图标碰撞结果 `_iconCollisionVisible`
|
||||
- 标签碰撞结果 `_labelCollisionVisible`
|
||||
- ✅ 低缩放下,为避免堆叠,部分锚点或标签会被主动隐藏。(`refreshPointLabelVisibility` 碰撞检测,L2117-2455)
|
||||
- ✅ 高缩放下,会逐步放开图标/标签限制,显示更多内容。(密度门控 + 碰撞容差随 zoom 调整)
|
||||
- ✅ 因此 `3D` 不能简单把所有 entity 永久全开。所有 entity 初始 `_iconCollisionVisible = _labelCollisionVisible = true`,碰撞检测运行后动态调整。
|
||||
|
||||
### 9.5 Popup 与点击 ✅️
|
||||
|
||||
- ✅ hover popup 不是全时开启的,有缩放门槛。相机高度 > `HOVER_POPUP_MAX_HEIGHT` 时不触发 hover。
|
||||
- ✅ 点击和 hover 的命中对象是图标本体,不是任意文字区域。`ScreenSpaceEventHandler` 基于 `scene.pick` 命中 entity。
|
||||
- ✅ `ylfb` 与普通锚点的点击详情弹框走不同业务分支。`sttpMap === 'ylfb'` 打开 `ylfbModalVisible`,其他走 `modalVisible`(L366-375)。
|
||||
|
||||
### 9.6 基地与区域裁切 ✅️
|
||||
|
||||
- ✅ 基地切换不仅影响搜索候选,还会影响地图上的点位可见范围。`jdPanelControlShowAndHidden` 实现(L1684-1748)。
|
||||
- ✅ 区域裁切会直接把区域外点位隐藏,不是只改底图遮罩。`filterPointsByRegion` 设置 `_regionVisible = false`(L1824-1848),遮罩多边形 + 裁剪双重处理。
|
||||
|
||||
### 9.7 筛选器特殊逻辑 ✅️
|
||||
|
||||
- ✅ 装机容量筛选本质是批量切 `eng_point` 图例状态,不是简单切图层。(store 层处理,Cesium 通过 `setLegendPointVisible` 响应)
|
||||
- ✅ 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。(store 层处理)
|
||||
- ✅ 时间筛选当前只重载指定时间相关图层,不是全量重载。(store 层处理)
|
||||
|
||||
### 9.8 数据进入 addInitDataLayer 前的预处理 ✅️
|
||||
|
||||
- ✅ 所有点位数据在进入 `addInitDataLayer` 前会经过 `normalizePointLayerItems()` 过滤:(store 层 `map.ts` 执行,Cesium 接收时已过滤)
|
||||
- 没有 `iconCode` 且 `code !== 'colorLayer'` 的项会被抛弃
|
||||
- `baseId == 'all'` 且 `anchoPointState` 结尾为 `_nbuilt` 的项会被抛弃
|
||||
- ✅ `_id` 默认值生成规则:store 层生成 `${sttpMap || iconType || 'point'}_${stcd}`。
|
||||
- ✅ **stcd+经纬度重复锚点处理**:Cesium entity ID 使用 `${layerKey}:${item._id}`,与 2D 对齐,同 stcd 不同 sttpMap 的点不会互相覆盖。
|
||||
|
||||
### 9.9 图标渐进加载机制
|
||||
|
||||
- ❌ 2D 中图标不是一次性全加载完。有 `loading → loaded → error` 三种状态跟踪。
|
||||
- ❌ 只有 `loaded` 状态的图标才参与样式渲染,loader 失败的不渲染。
|
||||
- ❌ 如果在图标加载过程中就触发了样式刷新,需要通过 `requestAnimationFrame` 回刷。
|
||||
- ❌ 3D Cesium 中当前所有 entity 同步创建,billboard `image` URL 直接赋值,无渐进加载。Cesium 内部纹理加载无显式状态跟踪。
|
||||
|
||||
### 9.10 密度分档与近邻点元数据 ✅️
|
||||
|
||||
- ✅ 每个锚点的 `distance` 字段表示密度分档值(不是物理距离),值越大表示越稀疏。(已使用 `getEntityDensityValue` 从 `_rawData.distance` 读取,L2460)
|
||||
- ✅ 不同 density 值对应不同的 `minZoom` 显示门槛。`getEntityDensityMinZoom` 映射 `nearby-point-rules.ts` 的 `densityDisplayRules`(L2491)。
|
||||
- ✅ 全量数据加载后,store 会调用 `attachNearbyPointRuntimeMeta()` 给所有点打上 `_nearbyRule`、`_nearbyGroupId`、`_nearbyPriority` 等元数据。(store 层处理,Cesium 碰撞检测中读取)
|
||||
- ✅ **孤立点例外规则**:如果一个点在当前视口内 ~56px 范围内没有同 layerKey 的其他可见点,它可以绕过密度门槛直接显示。`DENSITY_ISOLATION_THRESHOLD = 56` + `shouldBypassDensityGateForIsolatedEntity`(L2506-2556)。
|
||||
@ -1,343 +0,0 @@
|
||||
# 地图模块-OpenLayers抽吸与碰撞分析
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前地图 2D 主引擎已经切换为 OpenLayers,项目依赖版本为 `ol@^10.8.0`。
|
||||
本次关注的问题有两个:
|
||||
|
||||
- 点位“抽吸”逻辑不符合业务预期;
|
||||
- 文字碰撞时把整个点位一起隐藏,而不是仅隐藏文字。
|
||||
|
||||
典型场景是两个相邻电站,例如“积石峡”和“公伯峡”:
|
||||
|
||||
- 可用代号 `1`、`2` 表示两个相邻点;
|
||||
- 默认缩放下只展示 `1`;
|
||||
- 放大到某个层级后,`1` 隐藏,`2` 展示;
|
||||
- 再继续放大后,`1` 和 `2` 都展示,但 `2` 需要“抽吸/拉开”显示,避免与 `1` 重叠;
|
||||
- 如果只是文字发生碰撞,希望点图标保留,仅文字隐藏或延后显示。
|
||||
|
||||
本文件先整理当前代码现状和问题成因,不直接修改实现。
|
||||
如果需要查看**当前已经落地的抽吸规则、默认参数和调参入口**,请同时参考:
|
||||
|
||||
- `docs/地图模块-抽吸规则与参数说明.md`
|
||||
|
||||
## 2. 当前代码结构
|
||||
|
||||
当前前端 GIS 运行链路如下:
|
||||
|
||||
1. `src/components/gis/GisView.vue`
|
||||
2. `src/modules/map/application/map-orchestrator.ts`
|
||||
3. `src/components/gis/map.class.ts`
|
||||
4. `src/components/gis/map.ol.ts`
|
||||
5. `src/components/gis/ol/point-layer-manager.ts`
|
||||
6. `src/components/gis/ol/popup-manager.ts`
|
||||
|
||||
各层职责简述:
|
||||
|
||||
- `GisView.vue`
|
||||
- 地图入口组件;
|
||||
- 挂载地图容器、图例、筛选器、控制器;
|
||||
- 通过 `mapOrchestrator.mountView()` 初始化地图;
|
||||
- 菜单切换时通过 `handlePageChange()` 触发页面地图重载。
|
||||
|
||||
- `map.class.ts`
|
||||
- 地图门面类;
|
||||
- 当前 2D 实现实际绑定的是 `MapOl`;
|
||||
- 3D 实现才会切到 `MapCesium`。
|
||||
|
||||
- `map.ol.ts`
|
||||
- OpenLayers 主实现;
|
||||
- 负责地图初始化、底图加载、点位样式、Popup、区域裁切、量算等;
|
||||
- 当前“点位渲染规则”和“碰撞相关逻辑”主要集中在这个文件。
|
||||
|
||||
- `point-layer-manager.ts`
|
||||
- 负责点图层创建、Feature 灌入、按图层控制显隐;
|
||||
- 点位图层统一使用 `VectorLayer + VectorSource`。
|
||||
|
||||
- `popup-manager.ts`
|
||||
- 管理 hover popup 和高缩放下的批量 popup;
|
||||
- 内部额外做了一套基于边界框的碰撞判断。
|
||||
|
||||
## 3. 当前点位渲染链路
|
||||
|
||||
### 3.1 点图层创建
|
||||
|
||||
`point-layer-manager.ts` 中的 `ensureLayer()` 会统一创建点图层:
|
||||
|
||||
- 图层类型为 `VectorLayer<VectorSource>`;
|
||||
- 图层上开启了 `declutter: true`;
|
||||
- 样式函数统一走 `map.ol.ts` 中的 `createPointStyle()`。
|
||||
|
||||
也就是说,当前所有点位的“是否显示”和“图标/文字如何避让”,都收敛到 OpenLayers 的 declutter 机制和样式函数里。
|
||||
|
||||
### 3.2 点样式生成
|
||||
|
||||
`map.ol.ts` 的 `createPointStyle()` 当前做了这些事:
|
||||
|
||||
- 读取 `_iconUrl` 和 `_labelText`;
|
||||
- 如果图例不可见、区域不可见,则直接返回 `null`;
|
||||
- 如果 `distance` 不满足当前缩放级别要求,也直接返回 `null`;
|
||||
- 图标使用 `Icon`;
|
||||
- 文字使用 `Text`;
|
||||
- 图标和文字都设置了 `declutterMode: 'declutter'`;
|
||||
- 最终把图标和文字一起放进同一个 `Style` 返回。
|
||||
|
||||
这意味着:
|
||||
|
||||
- 点图标和文字不是两个独立的渲染对象;
|
||||
- 它们在当前实现中属于同一个样式结果;
|
||||
- 只要这组样式参与碰撞判定,图标和文字的命运就是绑定的。
|
||||
|
||||
### 3.3 当前已有的“抽稀”逻辑
|
||||
|
||||
当前项目中确实有一层“缩放越小,显示越少”的处理,但它不是精细的“抽吸”。
|
||||
|
||||
`map.ol.ts` 的 `shouldRenderFeatureByDistance()` 逻辑是:
|
||||
|
||||
- 小缩放下要求 `distance` 更大才允许显示;
|
||||
- 缩放越大,允许显示的点逐渐增多;
|
||||
- 本质上是基于后端数据字段 `distance` 做全局抽稀。
|
||||
|
||||
这个逻辑的特点是:
|
||||
|
||||
- 是全局阈值,不是成对点位或同组点位的专属规则;
|
||||
- 只能决定“这个点显示还是不显示”;
|
||||
- 不能表达“1 和 2 是近邻关系,并且在不同缩放阶段按业务优先级切换显示”。
|
||||
|
||||
### 3.4 当前批量 Popup 的碰撞检测
|
||||
|
||||
`popup-manager.ts` 在高缩放批量 popup 模式下,又额外做了一层碰撞判断:
|
||||
|
||||
- 先估算图标和文字合并后的锚点边界框;
|
||||
- 如果边界框碰撞,则当前要素直接不参与 popup 渲染;
|
||||
- 随后再判断 popup 矩形之间是否碰撞。
|
||||
|
||||
这说明当前“图标 + 文字”被视为一个整体,不仅体现在 OpenLayers 的 declutter 里,也体现在 popup 的辅助判断里。
|
||||
|
||||
## 4. 当前仓库里“抽吸”相关代码现状
|
||||
|
||||
仓库中确实存在历史上的“抽吸”实现,但不在当前 OpenLayers 主链路里。
|
||||
|
||||
相关文件:
|
||||
|
||||
- `src/utils/leaflet/leaflet.inflatable-markers-group.js`
|
||||
- `src/components/gis/map.leaflet.ts`
|
||||
|
||||
现状判断:
|
||||
|
||||
- `leaflet.inflatable-markers-group.js` 是旧 Leaflet 时代的抽吸插件;
|
||||
- 它内部有 `inflateAsManyAsPossible()`、`_zoomend()` 等典型抽吸逻辑;
|
||||
- 但当前 `map.class.ts` 实际实例化的是 `new MapOl()`;
|
||||
- `map.leaflet.ts` 已经不在主运行路径上。
|
||||
|
||||
结论是:
|
||||
|
||||
- 当前页面上看到的点位行为,不是 Leaflet 抽吸插件在工作;
|
||||
- 现在的显示结果主要来自 OpenLayers 的 `declutter`、当前样式函数和 `distance` 抽稀逻辑;
|
||||
- 因此“已经有抽吸但是不太对”,更准确地说是“历史上有抽吸代码,但现在主链路没有真正使用那套机制”。
|
||||
|
||||
## 5. 问题分析
|
||||
|
||||
### 5.1 为什么当前实现做不到你要的分阶段抽吸
|
||||
|
||||
你描述的目标并不是简单“避让”,而是一个明确的分阶段显示策略:
|
||||
|
||||
1. 默认只显示 `1`;
|
||||
2. 缩放到阶段 A 时,隐藏 `1`,改为显示 `2`;
|
||||
3. 缩放到阶段 B 时,`1` 和 `2` 都显示;
|
||||
4. 阶段 B 下,`2` 还要相对 `1` 做抽吸/偏移,避免完全重叠。
|
||||
|
||||
而当前代码缺少以下能力:
|
||||
|
||||
- 没有“近邻点关系”配置
|
||||
- 代码里没有看到类似 `groupId`、`neighborIds`、`priority`、`displayLevel` 这样的结构;
|
||||
- 系统不知道“积石峡”和“公伯峡”是一组特殊近邻点。
|
||||
|
||||
- 没有“分阶段显示规则”配置
|
||||
- 当前只有全局的 `distance` 阈值判断;
|
||||
- 没有按缩放阶段表达“谁替代谁显示、谁先隐藏、谁后展示”。
|
||||
|
||||
- 没有“抽吸偏移”计算
|
||||
- 当前点要素坐标就是原始经纬度转换结果;
|
||||
- 没有根据缩放级别和邻近关系生成偏移坐标;
|
||||
- 也没有“展开后沿圆周/沿固定方向偏移”的逻辑。
|
||||
|
||||
- 当前 declutter 是被动避让,不是主动编排
|
||||
- declutter 只会尽量避免重叠;
|
||||
- 它不会理解业务上的“1 是主点、2 是附点”;
|
||||
- 也不会自动实现“先 1,后 2,再两个都展示且 2 偏移”的规则。
|
||||
|
||||
所以当前看到的结果不稳定,本质原因不是某一个参数不对,而是“现有机制和目标能力不是一类问题”。
|
||||
|
||||
### 5.2 为什么文字碰撞会把整个点隐藏
|
||||
|
||||
这是当前实现里最关键的原因:
|
||||
|
||||
- 点图标和文字是在同一个 `Style` 里返回的;
|
||||
- 图层开启了 `declutter: true`;
|
||||
- 图标和文字又都设置了 `declutterMode: 'declutter'`。
|
||||
|
||||
在这种组织方式下,当前要素的碰撞判定是按“一个整体渲染单元”处理的,而不是“图标一套规则,文字一套规则”。
|
||||
|
||||
因此会出现下面的现象:
|
||||
|
||||
- 图标本身其实不冲突;
|
||||
- 但文字边界框发生碰撞;
|
||||
- 最终被隐藏的是整条样式结果;
|
||||
- 用户视觉上就会觉得“明明只是字挤了,为什么点也没了”。
|
||||
|
||||
这和你现在观察到的问题是一致的。
|
||||
|
||||
### 5.3 为什么当前逻辑还会进一步放大这个问题
|
||||
|
||||
除了 OpenLayers 自身的 declutter 组织方式,代码里还有两层因素会继续放大问题:
|
||||
|
||||
- `shouldRenderFeatureByDistance()` 会先根据 `distance` 直接过滤点位;
|
||||
- `popup-manager.ts` 在批量 popup 模式下,也把“图标 + 文字”当作合并边界框做碰撞模拟。
|
||||
|
||||
也就是说,当前系统对近邻点位的处理是:
|
||||
|
||||
- 先用全局 `distance` 规则筛掉一部分;
|
||||
- 再用 OpenLayers declutter 继续过滤;
|
||||
- 高缩放时 popup 层再做一轮碰撞跳过。
|
||||
|
||||
这三层叠加后,显示行为更偏向“谁先撞谁消失”,而不是“按业务规则分阶段展开”。
|
||||
|
||||
## 6. 结合你的案例做具体映射
|
||||
|
||||
如果以近邻点 `1`、`2` 为例,当前代码里实际上缺的是下面这些业务语义:
|
||||
|
||||
- `1` 和 `2` 属于同一近邻组;
|
||||
- 组内有主次优先级;
|
||||
- 在不同 zoom 区间内有不同展示策略;
|
||||
- 在最大放大阶段需要给 `2` 一个偏移位;
|
||||
- 偏移后图标和文字还要分别控制碰撞策略。
|
||||
|
||||
当前实现只能表达:
|
||||
|
||||
- 某个点在当前 zoom 下是否允许渲染;
|
||||
- 某个点图例是否可见;
|
||||
- 某个点是否被区域裁切隐藏;
|
||||
- 某个点的图标和文字整体是否参与 declutter。
|
||||
|
||||
它并不能表达:
|
||||
|
||||
- “1 替代 2 展示”;
|
||||
- “2 替代 1 展示”;
|
||||
- “1 和 2 一起展示,但 2 抽吸展开”;
|
||||
- “只隐藏文字,不隐藏图标”。
|
||||
|
||||
所以你现在遇到的问题是结构性问题,不是简单调一个 `declutter` 参数就能彻底解决。
|
||||
|
||||
## 7. 后续修改时建议优先动的层
|
||||
|
||||
如果下一步要真正改这块,建议优先从下面几层入手:
|
||||
|
||||
### 7.1 先补“近邻点显示规则”
|
||||
|
||||
建议不要直接把业务规则写死在样式函数里,而是先增加一层近邻点编排规则,例如:
|
||||
|
||||
- `groupId`
|
||||
- `priority`
|
||||
- `showZoomMin`
|
||||
- `showZoomMax`
|
||||
- `replaceAtZoom`
|
||||
- `expandAtZoom`
|
||||
- `expandOffset`
|
||||
|
||||
这样才能表达:
|
||||
|
||||
- 默认只显示主点;
|
||||
- 中间层级切换成副点;
|
||||
- 更大层级两个都显示;
|
||||
- 副点在展示时做偏移。
|
||||
|
||||
### 7.2 抽吸不要依赖 declutter 自动完成
|
||||
|
||||
抽吸本质上是“主动布局”问题,不是“被动避让”问题。
|
||||
|
||||
建议后续把近邻点抽吸拆成单独逻辑:
|
||||
|
||||
- 先根据原始点数据识别近邻组;
|
||||
- 再根据当前 zoom 产出“本次真正参与渲染的点集合”;
|
||||
- 对需要展开的点,生成偏移后的渲染坐标;
|
||||
- 最后再交给图层渲染。
|
||||
|
||||
这样会比直接指望 OpenLayers 的 declutter 更可控。
|
||||
|
||||
### 7.3 图标层和文字层要分开控制
|
||||
|
||||
要实现“文字碰撞时只隐藏文字,不隐藏点图标”,后续最重要的一点是把图标和文字的渲染职责拆开。
|
||||
|
||||
至少要做到下面二选一:
|
||||
|
||||
- 图标层和文字层拆成两个独立图层;
|
||||
- 或者把图标与文字拆成可独立决策的渲染单元。
|
||||
|
||||
目标是:
|
||||
|
||||
- 图标优先保留;
|
||||
- 文字单独避让;
|
||||
- 文本碰撞时只压文字,不压图标。
|
||||
|
||||
如果仍然维持“同一个样式对象里同时放 image 和 text”的组织方式,这个问题大概率还会持续存在。
|
||||
|
||||
### 7.4 Popup 碰撞策略也要和新规则同步
|
||||
|
||||
后续如果点位抽吸和文字单独避让改了,`popup-manager.ts` 里的边界框逻辑也要一起调整。
|
||||
|
||||
否则会出现:
|
||||
|
||||
- 图标已经正常显示;
|
||||
- 文字也按新规则显示了;
|
||||
- 但 popup 层仍然沿用旧的合并边界框;
|
||||
- 最终又在 popup 阶段把结果打回去。
|
||||
|
||||
## 8. 建议的改造顺序
|
||||
|
||||
为了降低风险,建议后续改造按以下顺序推进:
|
||||
|
||||
1. 先梳理近邻点数据结构
|
||||
- 确定近邻点如何分组;
|
||||
- 确定主次优先级和缩放阈值从哪里来。
|
||||
|
||||
2. 再实现“显示决策层”
|
||||
- 在真正生成 Style 之前,先决定哪些点应该显示;
|
||||
- 哪些点应该隐藏;
|
||||
- 哪些点需要偏移展开。
|
||||
|
||||
3. 然后拆分图标和文字碰撞
|
||||
- 先保证点图标稳定显示;
|
||||
- 再单独做文字避让。
|
||||
|
||||
4. 最后调整 popup 逻辑
|
||||
- 保证 popup 的碰撞判定和最终可见点保持一致。
|
||||
|
||||
## 9. 涉及文件清单
|
||||
|
||||
本次分析重点涉及以下文件:
|
||||
|
||||
- `src/components/gis/GisView.vue`
|
||||
- `src/components/gis/map.class.ts`
|
||||
- `src/components/gis/map.ol.ts`
|
||||
- `src/components/gis/ol/point-layer-manager.ts`
|
||||
- `src/components/gis/ol/popup-manager.ts`
|
||||
- `src/utils/leaflet/leaflet.inflatable-markers-group.js`
|
||||
- `src/components/gis/map.leaflet.ts`
|
||||
- `package.json`
|
||||
|
||||
## 10. 当前结论
|
||||
|
||||
当前地图问题可以归纳为两点:
|
||||
|
||||
1. 现在的 OpenLayers 主链路并没有真正使用旧 Leaflet 的抽吸机制;
|
||||
2. 当前点图标和文字被作为一个整体参与 declutter,所以文字碰撞时会连点一起隐藏。
|
||||
|
||||
所以后续如果要改,方向不应该只是“调避让参数”,而应该是:
|
||||
|
||||
- 增加近邻点分阶段显示规则;
|
||||
- 增加主动抽吸/偏移布局;
|
||||
- 把图标和文字拆成可独立控制的渲染单元;
|
||||
- 同步修正 popup 的碰撞判断。
|
||||
|
||||
在这个基础上,再进入代码修改会更稳。
|
||||
@ -1,316 +0,0 @@
|
||||
# 地图模块-抽吸规则与参数说明
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文档用于记录当前地图点位“抽吸”功能的现行实现、默认参数、分层级显示规则和后续调参方式。
|
||||
本文件描述的是**当前已经落地并验证通过**的规则,适合作为后续维护和继续调参的依据。
|
||||
|
||||
相关文档:
|
||||
|
||||
- `docs/地图模块-详细说明.md`:地图整体架构说明
|
||||
- `docs/地图模块-OpenLayers抽吸与碰撞分析.md`:问题成因与改造分析
|
||||
|
||||
## 2. 当前实现范围
|
||||
|
||||
当前抽吸逻辑运行在 OpenLayers 主链路中,核心文件如下:
|
||||
|
||||
1. `src/modules/map/domain/nearby-point-rules.ts`
|
||||
2. `src/store/modules/map.ts`
|
||||
3. `src/components/gis/map.ol.ts`
|
||||
4. `src/components/gis/ol/point-layer-manager.ts`
|
||||
|
||||
各文件职责:
|
||||
|
||||
- `nearby-point-rules.ts`
|
||||
- 近邻点规则入口
|
||||
- 自动识别配置入口
|
||||
- 近邻点运行时元数据挂载
|
||||
- `map.ts`
|
||||
- 点位数据加载完成后,统一触发近邻点分组和元数据写回
|
||||
- `map.ol.ts`
|
||||
- 决定某个点在当前缩放级别是否显示
|
||||
- 决定文字是否允许显示
|
||||
- 承担图标与文字的手工碰撞控制
|
||||
- `point-layer-manager.ts`
|
||||
- 负责展开阶段的几何偏移
|
||||
- 负责把同组点位按扇形/环形布局散开
|
||||
|
||||
## 3. 当前抽吸规则
|
||||
|
||||
### 3.1 近邻点自动识别
|
||||
|
||||
当前采用自动识别近邻点,不再靠固定站名手工枚举。
|
||||
|
||||
识别原则:
|
||||
|
||||
- 只在同一 `layerKey` 内做近邻识别
|
||||
- 按点位空间距离自动聚组
|
||||
- 满足最小组大小后,才认为是近邻组
|
||||
- 组内自动生成显示优先级 `priority`
|
||||
|
||||
这样做的目的:
|
||||
|
||||
- 避免跨图层混组
|
||||
- 避免后续新增点位还要手工补名字
|
||||
- 让抽吸能力可以覆盖整批业务点
|
||||
|
||||
### 3.2 分层级显示规则
|
||||
|
||||
当前显示规则以 `replaceAtZoom` 和 `expandAtZoom` 为核心:
|
||||
|
||||
1. `zoom < replaceAtZoom`
|
||||
- 只显示组内主点
|
||||
- 即 `priority = 1`
|
||||
2. `replaceAtZoom <= zoom < expandAtZoom`
|
||||
- 显示组内前两个点
|
||||
- 即 `priority <= 2`
|
||||
- 当前这样设计是为了避免主点在放大过程中突然消失
|
||||
3. `zoom >= 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. 当前结论
|
||||
|
||||
当前这版抽吸实现已经形成了比较稳定的主线:
|
||||
|
||||
- 自动识别近邻点
|
||||
- 统一缩放阶段显示规则
|
||||
- 展开阶段几何偏移
|
||||
- 文字依赖点显示
|
||||
- 文字避让图标与文字
|
||||
|
||||
后续如果继续调优,建议先从参数层开始,不要先动主逻辑结构。
|
||||
@ -1,621 +0,0 @@
|
||||
# 地图模块-详细说明
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文档用于说明当前地图模块的真实运行结构、初始化时序、图层与图例联动、点位渲染规则、筛选与缩放行为,以及近期已经落地的关键约束。
|
||||
|
||||
本文档描述的是“当前可运行版本”的现状,不展开抽吸参数细节与专项问题分析。相关补充文档如下:
|
||||
|
||||
- `docs/地图模块-抽吸规则与参数说明.md`
|
||||
- `docs/地图模块-OpenLayers抽吸与碰撞分析.md`
|
||||
|
||||
## 2. 当前技术栈
|
||||
|
||||
- 前端框架:Vue 3 + TypeScript + Ant Design Vue
|
||||
- 状态管理:Pinia
|
||||
- 2D 地图引擎:OpenLayers 10.8.0
|
||||
- 3D 地图引擎:Cesium
|
||||
- 地图服务:GeoServer / WMTS / XYZ
|
||||
- 运行模式:后台配置驱动 + 前台运行时编排
|
||||
|
||||
说明:
|
||||
|
||||
- 当前 2D 主链路已经完全围绕 OpenLayers 组织。
|
||||
- 项目中仍保留 Leaflet 相关依赖和旧代码,但不是当前主执行路径。
|
||||
|
||||
## 3. 模块分层
|
||||
|
||||
当前地图模块可以按 6 层理解:
|
||||
|
||||
1. 页面入口层
|
||||
2. 应用编排层
|
||||
3. 地图能力门面层
|
||||
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.1 地图容器全局常驻
|
||||
|
||||
地图由 `GisView.vue` 常驻挂载,不会随着菜单切换被销毁重建。页面切换本质上是:
|
||||
|
||||
- 重新计算 `pageKey`
|
||||
- 重新加载该页图层配置和图例配置
|
||||
- 用新的运行态覆盖旧页面显示内容
|
||||
|
||||
### 4.2 配置驱动
|
||||
|
||||
前端不把全部图层和图例硬编码死,而是依赖后端返回的:
|
||||
|
||||
- 图层树配置
|
||||
- 图例配置
|
||||
- 页面级图例配置
|
||||
- 各业务点位接口数据
|
||||
|
||||
### 4.3 运行态绝对优先
|
||||
|
||||
初始化默认值只在第一次派生时生效;一旦进入运行态,后续图层勾选、图例勾选、筛选器切换,都必须以当前用户操作后的状态为准,不能在加载完成后回退到默认状态。
|
||||
|
||||
## 5. 页面入口层
|
||||
|
||||
### 5.1 `GisView.vue`
|
||||
|
||||
`GisView.vue` 的职责主要是:
|
||||
|
||||
- 提供地图容器 `#mapContainer`
|
||||
- 提供 popup 挂载容器
|
||||
- 挂载图例、筛选器、控制器、底图切换器
|
||||
- 根据路由计算 `pageKey`
|
||||
- 在挂载时调用 `mapOrchestrator.mountView()`
|
||||
- 在页面切换时交给编排器重载页面
|
||||
|
||||
当前页面层已经不再直接承载复杂业务逻辑,复杂链路都收敛到了编排器和 Store。
|
||||
|
||||
## 6. 应用编排层
|
||||
|
||||
### 6.1 `map-orchestrator.ts` 的职责
|
||||
|
||||
`map-orchestrator.ts` 是当前地图业务的总调度中心,主要负责:
|
||||
|
||||
- 初始化地图壳与 popup
|
||||
- 加载页面图层配置和图例配置
|
||||
- 初始化 GIS 基础图层
|
||||
- 触发点位图层加载
|
||||
- 管理缩放监听和基地切换监听
|
||||
- 处理图层树勾选
|
||||
- 处理图例勾选
|
||||
- 处理时间筛选、容量筛选、搜索定位
|
||||
- 处理水电开发菜单下的缩放联动图层
|
||||
|
||||
### 6.2 页面初始化链路
|
||||
|
||||
当前初始化顺序如下:
|
||||
|
||||
1. `GisView.vue` 调用 `mapOrchestrator.mountView()`
|
||||
2. `MapClass.init()` 初始化地图实例
|
||||
3. 页面级 popup 容器挂到地图实例
|
||||
4. 绑定缩放监听和基地监听
|
||||
5. 并行请求图层树配置与图例配置
|
||||
6. 图例接口先返回时,立即生成图例运行态
|
||||
7. 图层树接口返回后,生成默认勾选图层并初始化 GIS 基础图层
|
||||
8. 再按图层配置批量加载点位数据
|
||||
|
||||
这里有两个当前已落地的关键时序调整:
|
||||
|
||||
- 图例转圈只跟图例接口绑定,不再等待所有锚点接口完成。
|
||||
- 筛选器数据会随着已加载图层逐步出现,不再必须等全量点位接口结束。
|
||||
|
||||
### 6.3 页面切换
|
||||
|
||||
页面切换时地图不销毁,只重载当前页面的:
|
||||
|
||||
- 图层树配置
|
||||
- 图例配置
|
||||
- 点位数据
|
||||
- 当前页面的默认运行态
|
||||
|
||||
如果仍处于同一个 `pageKey` 且用户之前修改过勾选态,会尽量复用当前运行态,而不是重新回放默认勾选。
|
||||
|
||||
### 6.4 缩放联动
|
||||
|
||||
当前编排层主要有两类缩放联动:
|
||||
|
||||
- `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()`
|
||||
- `hasLayer()`
|
||||
- `hasBaseLayer()`
|
||||
- `removePointLayer()`
|
||||
- `flyTopanto()`
|
||||
- `switchView()`
|
||||
|
||||
当前业务主执行层仍然是 `MapOl`。
|
||||
|
||||
## 8. 地图引擎执行层
|
||||
|
||||
### 8.1 OpenLayers 主执行层
|
||||
|
||||
`map.ol.ts` 负责 OpenLayers 地图的实际渲染与交互,是当前 2D 地图主执行文件。
|
||||
|
||||
当前承担的核心能力包括:
|
||||
|
||||
- 地图实例初始化
|
||||
- 底图与 GIS 图层管理
|
||||
- 点位图层渲染
|
||||
- 图标与文字样式生成
|
||||
- hover popup 与批量 popup
|
||||
- 基地范围裁切
|
||||
- 高缩放交互
|
||||
- 测量、截图、飞行定位等基础地图能力
|
||||
|
||||
### 8.2 Cesium 执行层
|
||||
|
||||
`map.cesium.ts` 目前更多是保留 3D 入口和部分基础能力,复杂业务点位与碰撞规则并未与 OpenLayers 完全对齐。
|
||||
|
||||
结论上,当前真实业务规则仍然以 OpenLayers 链路为准。
|
||||
|
||||
## 9. Store 分层
|
||||
|
||||
### 9.1 `map-config.store.ts`
|
||||
|
||||
负责配置态数据:
|
||||
|
||||
- 图层树配置
|
||||
- 图层配置索引
|
||||
- 全量原始图例配置
|
||||
- 图例索引
|
||||
- 页面级图例配置
|
||||
- 配置接口加载状态
|
||||
|
||||
当前这里拆分出了两个加载态:
|
||||
|
||||
- `configLoading`
|
||||
- `legendLoading`
|
||||
|
||||
其中 `legendLoading` 单独控制图例转圈。
|
||||
|
||||
### 9.2 `map-data.store.ts`
|
||||
|
||||
负责数据态:
|
||||
|
||||
- `pointData`
|
||||
- `pointDataCache`
|
||||
- `layerLoadState`
|
||||
- 地图整体 `loading`
|
||||
|
||||
这里的关键设计是:
|
||||
|
||||
- 每个点图层有独立缓存
|
||||
- `pointData` 是由缓存合并出来的运行态数据
|
||||
- 单个图层命中缓存或接口返回后,就会立即回填 `pointData`
|
||||
|
||||
因此当前搜索筛选下拉已经支持“渐进可用”,不再只能等全部接口结束。
|
||||
|
||||
### 9.3 `map-view.store.ts`
|
||||
|
||||
负责视图运行态:
|
||||
|
||||
- 当前勾选图层 `checkedLayerKeys`
|
||||
- 图例勾选状态 `legendCheckedState`
|
||||
- 搜索时间范围
|
||||
- 当前基地
|
||||
- 当前缩放级别
|
||||
|
||||
### 9.4 `src/store/modules/map.ts`
|
||||
|
||||
这是当前地图运行逻辑最重的 Store,负责串联配置、缓存和地图实例。
|
||||
|
||||
主要职责包括:
|
||||
|
||||
- 生成图例运行态
|
||||
- 批量加载点位数据
|
||||
- 控制图层显隐
|
||||
- 控制图例显隐
|
||||
- 管理请求缓存与并发加载
|
||||
- 刷新点图层显示数据
|
||||
- 处理 `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 基础样式
|
||||
|
||||
当前点位渲染包括:
|
||||
|
||||
- 图标
|
||||
- 文字标签
|
||||
- 标签换行与截断
|
||||
- 缩放相关的图标与文字样式变化
|
||||
|
||||
### 16.2 图标加载约束
|
||||
|
||||
当前已经补充图标加载状态缓存:
|
||||
|
||||
- 图标未加载完成时,不显示该点
|
||||
- 图标未加载完成时,文字也不先显示
|
||||
- 图标未加载完成时,不参与碰撞候选
|
||||
|
||||
这样可以避免“文字先出来、图标还是空的”的问题。
|
||||
|
||||
## 17. 抽吸、密度与碰撞
|
||||
|
||||
### 17.1 专项文档
|
||||
|
||||
抽吸与密度分档的详细参数请查看:
|
||||
|
||||
- `docs/地图模块-抽吸规则与参数说明.md`
|
||||
|
||||
### 17.2 当前裁决顺序
|
||||
|
||||
当前点位显示顺序可以理解为:
|
||||
|
||||
1. 密度分档
|
||||
2. 近邻抽吸
|
||||
3. 图标碰撞
|
||||
4. 文字碰撞
|
||||
|
||||
补充约束:
|
||||
|
||||
- 只有满足可见性条件的点,才能进入碰撞候选集
|
||||
- 被图例或缩放过滤掉的点,不能再去占用碰撞资格
|
||||
- 视图内无近邻的孤立点允许局部放行
|
||||
|
||||
### 17.3 高缩放特例
|
||||
|
||||
当前在高缩放层级 `zoom >= 15` 时,行为与中低缩放不同:
|
||||
|
||||
- 只处理视口内点位
|
||||
- 点与文字不再做碰撞淘汰
|
||||
- 视口内符合基础显示条件的点和文字全部显示
|
||||
- 同时进入批量 popup 模式
|
||||
|
||||
这条规则的目标是:
|
||||
|
||||
- 保证深度放大后信息完整可见
|
||||
- 避免高缩放下还因为碰撞把点或文字压掉
|
||||
|
||||
## 18. Popup 机制
|
||||
|
||||
### 18.1 两类 popup
|
||||
|
||||
当前 popup 分成两类:
|
||||
|
||||
- hover popup
|
||||
- batch popup
|
||||
|
||||
### 18.2 hover popup
|
||||
|
||||
鼠标悬停时由 `pointermove` 驱动,只在非 batch popup 模式下显示。
|
||||
|
||||
### 18.3 batch popup
|
||||
|
||||
在 `zoom >= 15` 时进入批量 popup 模式:
|
||||
|
||||
- 只基于当前视口内点位
|
||||
- 拖动地图时持续刷新
|
||||
- 但通过 `requestAnimationFrame` 做节流
|
||||
- `moveend` 后立即再做一次强制刷新
|
||||
|
||||
当前这样做是为了兼顾两件事:
|
||||
|
||||
- 保留“拖动时 popup 跟着更新”的原交互
|
||||
- 避免拖动过程中每个事件都重建整批 DOM
|
||||
|
||||
### 18.4 惯性平移已关闭
|
||||
|
||||
当前地图拖动已经关闭 OpenLayers 默认惯性平移:
|
||||
|
||||
- 普通拖动仍然保留
|
||||
- 松手后不再继续滑动一小段
|
||||
|
||||
这样可以减少高缩放 popup 场景下“松手还轻微位移”的观感问题。
|
||||
|
||||
## 19. 基地与区域过滤
|
||||
|
||||
基地切换是当前地图模块的关键能力之一,主要影响:
|
||||
|
||||
- 地图裁切范围
|
||||
- 当前基地相关点位的可见性
|
||||
- 部分专题图层显示
|
||||
|
||||
整体过程通常是:
|
||||
|
||||
1. 外部基地选择源变化
|
||||
2. 编排器接收到基地 ID
|
||||
3. 地图实例切换基地范围
|
||||
4. 点位更新 `_regionVisible`
|
||||
5. 样式函数按区域可见性决定是否显示
|
||||
|
||||
## 20. 典型交互链路
|
||||
|
||||
### 20.1 图层树勾选
|
||||
|
||||
1. `LayerController` 触发勾选变化
|
||||
2. 编排器归一化图层 key
|
||||
3. `map.ts` 更新运行态图层勾选
|
||||
4. 地图实例更新图层显隐
|
||||
5. 图例重新派生
|
||||
|
||||
补充:
|
||||
|
||||
- `GISMap` 勾选时,如果地图里没有该图层,会先补载,再显示
|
||||
|
||||
### 20.2 图例勾选
|
||||
|
||||
1. 图例组件触发点击
|
||||
2. 编排器转发图例切换
|
||||
3. `map.ts` 更新图例运行态
|
||||
4. 对应点位立即更新显隐
|
||||
5. 图例树同步更新灰态
|
||||
|
||||
### 20.3 筛选器操作
|
||||
|
||||
1. 用户切换时间、容量、搜索等条件
|
||||
2. 编排器接收命令
|
||||
3. 视情况更新视图运行态或重载数据
|
||||
4. 地图实例与图例运行态同步刷新
|
||||
|
||||
### 20.4 缩放操作
|
||||
|
||||
1. OpenLayers 视图触发 `change:resolution`
|
||||
2. 编排器更新当前缩放级别
|
||||
3. 处理 `eng_point` 阈值联动
|
||||
4. 水电开发菜单下处理动态图层联动
|
||||
5. `map.ol.ts` 处理高缩放 popup 与点位显示策略
|
||||
|
||||
## 21. 当前重点文件清单
|
||||
|
||||
- `src/components/gis/GisView.vue`
|
||||
- `src/components/gis/map.class.ts`
|
||||
- `src/components/gis/map.ol.ts`
|
||||
- `src/components/gis/map.cesium.ts`
|
||||
- `src/components/gis/gisUtils.ts`
|
||||
- `src/components/gis/mapurlManage.ts`
|
||||
- `src/components/gis/ol/point-layer-manager.ts`
|
||||
- `src/components/gis/ol/popup-manager.ts`
|
||||
- `src/modules/map/application/map-orchestrator.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/store/modules/map.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`
|
||||
|
||||
## 22. 当前结论
|
||||
|
||||
当前地图模块已经形成比较清晰的主干结构:
|
||||
|
||||
- `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 执行层”这条主干扩展,不要再把复杂业务回填到页面组件里。
|
||||
@ -1,380 +0,0 @@
|
||||
# 地图模块重构实施清单
|
||||
|
||||
## 1. 使用说明
|
||||
|
||||
本文档是地图模块重构的执行清单,只保留当前已经明确、可以直接落地的实施项。
|
||||
|
||||
以下内容暂不纳入本清单:
|
||||
|
||||
- 仍需进一步讨论的方案;
|
||||
- 依赖后端配合后才能确定的优化项;
|
||||
- 暂未确定收益与改造成本的预研项;
|
||||
- 调试面板、埋点体系、长期性能平台化建设等扩展项。
|
||||
|
||||
执行原则:
|
||||
|
||||
- 先止血,再拆层,再提速;
|
||||
- 每一步都要保证现有业务可用;
|
||||
- 每个阶段结束后都要做功能回归;
|
||||
- 不在同一阶段同时改动 UI、Store、地图渲染层的大块逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 2. 本轮重构目标
|
||||
|
||||
本轮只完成以下确定目标:
|
||||
|
||||
- 收口当前明显缺陷和遗留代码;
|
||||
- 建立基础类型和索引;
|
||||
- 将分散在组件中的核心业务规则收口;
|
||||
- 拆分地图 Store,建立更清晰的数据职责;
|
||||
- 抽离统一编排层;
|
||||
- 拆分 `map.ol.ts` 的核心管理职责;
|
||||
- 保证图层树、图例、锚点显隐链路更清晰;
|
||||
- 为后续继续重构预留稳定结构。
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段一:现状收口与问题修复
|
||||
|
||||
## 3.1 清理明显遗留问题
|
||||
|
||||
- [ ] 删除 `map.ol.ts` 中无意义代码和重复日志
|
||||
- [ ] 清理大段失效注释,保留必要说明
|
||||
- [ ] 统一地图相关文件中的临时 `console.log`
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/gis/map.ol.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 页面可正常编译;
|
||||
- 首次进入地图页面不报错;
|
||||
- 控制台不再出现未定义函数或变量错误;
|
||||
- 默认图层、图例、锚点仍可正常显示。
|
||||
|
||||
## 3.2 补充基础类型定义
|
||||
|
||||
- [ ] 新增地图图层类型定义
|
||||
- [ ] 新增图例结构类型定义
|
||||
- [ ] 新增锚点数据类型定义
|
||||
- [ ] 新增筛选参数类型定义
|
||||
- [ ] 为 `map.ts`、`map.class.ts`、`map.ol.ts` 替换第一批高频 `any`
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/types/map-layer.ts`
|
||||
- `src/modules/map/types/map-legend.ts`
|
||||
- `src/modules/map/types/map-point.ts`
|
||||
- `src/modules/map/types/map-filter.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 核心地图模块类型边界可读;
|
||||
- 新增类型后不影响现有运行;
|
||||
- `map.ts`、`map.ol.ts` 中关键方法参数不再全部使用 `any`。
|
||||
|
||||
## 3.3 建立基础索引
|
||||
|
||||
- [x] 建立 `layerByKey` 索引
|
||||
- [x] 建立 `legendByNameEn` 索引
|
||||
- [x] 建立 `legendByLayerCode` 索引
|
||||
- [x] 将 `findLayerByKey()` 的高频调用逐步替换为索引访问
|
||||
- [x] 将图例派生中的重复递归改为“初始化索引 + 增量查询”
|
||||
|
||||
优先改造位置:
|
||||
|
||||
- `src/store/modules/map.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 图层查找和图例查找不再完全依赖全量递归;
|
||||
- 图层勾选、图例切换后响应速度不下降;
|
||||
- 功能表现与现状一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 阶段二:规则收口
|
||||
|
||||
## 4.1 收口互斥规则
|
||||
|
||||
- [x] 将图层互斥逻辑从 `LayerController.vue` 中移出
|
||||
- [x] 将视频监控站与 AI 视频监控站互斥统一到规则函数
|
||||
- [x] 将环保设施与环保设施在建互斥统一到规则函数
|
||||
- [x] 将珍稀鱼类与沿程鱼类互斥统一到规则函数
|
||||
- [x] 保证互斥规则只有一个入口执行
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/domain/map-layer-rules.ts`
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/mapController/LayerController.vue`
|
||||
- `src/store/modules/map.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 图层树勾选时互斥结果稳定;
|
||||
- 不再同时在组件和 Store 两处维护同一套互斥规则;
|
||||
- 勾选结果、图例结果、地图显示结果保持一致。
|
||||
|
||||
## 4.2 收口图例派生规则
|
||||
|
||||
- [x] 将“图层选中后生成图例”逻辑提炼为独立函数
|
||||
- [x] 将“环保设施图例自动补充”逻辑提炼为独立规则
|
||||
- [x] 分离图例原始结构与图例选中状态
|
||||
- [x] 保证 `legendDataOriginal` 不再被运行态逻辑污染
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/domain/legend-deriver.ts`
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/store/modules/map.ts`
|
||||
- `src/components/mapLegend/index.vue`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 图例显示仍按当前选中图层正确联动;
|
||||
- 图例点击后锚点显隐正确;
|
||||
- 图例原始结构与运行时状态职责更清晰。
|
||||
|
||||
---
|
||||
|
||||
## 5. 阶段三:拆分 Store
|
||||
|
||||
## 5.1 拆分配置状态
|
||||
|
||||
- [ ] 抽离图层树配置状态
|
||||
- [ ] 抽离图例原始配置状态
|
||||
- [ ] 抽离页面地图配置加载逻辑
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/stores/map-config.store.ts`
|
||||
|
||||
当前迁移来源:
|
||||
|
||||
- `src/store/modules/map.ts`
|
||||
- `src/api/map.ts`
|
||||
|
||||
## 5.2 拆分运行状态
|
||||
|
||||
- [ ] 抽离当前选中图层 key
|
||||
- [ ] 抽离当前选中图例 key
|
||||
- [ ] 抽离当前基地、时间范围、缩放级别等运行态
|
||||
- [ ] 统一“当前地图状态”的唯一来源
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/stores/map-view.store.ts`
|
||||
|
||||
## 5.3 拆分数据缓存
|
||||
|
||||
- [ ] 抽离锚点缓存
|
||||
- [ ] 抽离图层加载状态
|
||||
- [ ] 抽离锚点合并数据 `pointData`
|
||||
- [ ] 为每个图层维护独立 `loading / loaded / error`
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/stores/map-data.store.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 原 `map.ts` 体积明显下降;
|
||||
- 配置、运行态、数据缓存职责分离;
|
||||
- 现有组件仍可通过兼容方式读取数据;
|
||||
- 默认图层、图例、锚点加载不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 6. 阶段四:建立统一编排层
|
||||
|
||||
## 6.1 新增地图编排器
|
||||
|
||||
- [x] 新增地图模块统一编排入口
|
||||
- [x] 将地图初始化流程迁移到编排器
|
||||
- [x] 将图层勾选、图例勾选、基地切换、时间筛选、搜索定位改为统一命令
|
||||
- [x] 收口 `GisView.vue` 中的配置加载与监听联动逻辑
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/modules/map/application/map-orchestrator.ts`
|
||||
|
||||
## 6.2 统一入口命令
|
||||
|
||||
- [x] 定义 `initialize(pageKey)`
|
||||
- [x] 定义 `toggleLayer(layerKey)`
|
||||
- [x] 定义 `toggleLegend(layerKey, legendKey)`
|
||||
- [x] 定义 `changeBaseId(baseId)`
|
||||
- [x] 定义 `changeTimeRange(range)`
|
||||
- [x] 定义 `focusPoint(pointId)`
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/gis/GisView.vue`
|
||||
- `src/components/mapController/LayerController.vue`
|
||||
- `src/components/mapLegend/index.vue`
|
||||
- `src/components/mapFilter/index.vue`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 地图相关 UI 组件不再直接拼复杂业务逻辑;
|
||||
- 组件交互都通过统一入口触发;
|
||||
- 图层树、图例、筛选的联动链路更容易追踪。
|
||||
|
||||
---
|
||||
|
||||
## 7. 阶段五:拆分 OpenLayers 渲染层
|
||||
|
||||
## 7.1 拆分点图层管理
|
||||
|
||||
- [ ] 从 `map.ol.ts` 中抽离锚点图层创建逻辑
|
||||
- [ ] 从 `map.ol.ts` 中抽离锚点显隐逻辑
|
||||
- [ ] 从 `map.ol.ts` 中抽离图例控制锚点逻辑
|
||||
- [ ] 在点图层管理模块中维护图层注册表
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/components/gis/ol/point-layer-manager.ts`
|
||||
|
||||
当前迁移来源:
|
||||
|
||||
- `src/components/gis/map.ol.ts`
|
||||
|
||||
## 7.2 拆分 Popup 管理
|
||||
|
||||
- [ ] 抽离 `initPopupOverlay()`
|
||||
- [ ] 抽离 `detectFeatureAtPixel()`
|
||||
- [ ] 抽离 `showPopup()`
|
||||
- [ ] 收口 hover 和 click 交互逻辑
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/components/gis/ol/popup-manager.ts`
|
||||
|
||||
## 7.3 拆分区域过滤管理
|
||||
|
||||
- [x] 抽离基地切换后的区域裁切逻辑
|
||||
- [x] 抽离点在面内判断逻辑
|
||||
- [x] 抽离区域外锚点显隐逻辑
|
||||
- [x] 抽离遮罩应用与清理逻辑
|
||||
|
||||
建议新增文件:
|
||||
|
||||
- `src/components/gis/ol/region-mask-manager.ts`
|
||||
|
||||
验收标准:
|
||||
|
||||
- `map.ol.ts` 文件长度和职责明显收缩;
|
||||
- 点图层、Popup、基地裁切三类逻辑各自独立;
|
||||
- 现有地图交互行为不变。
|
||||
|
||||
---
|
||||
|
||||
## 8. 阶段六:组件瘦身
|
||||
|
||||
## 8.1 `GisView.vue`
|
||||
|
||||
- [x] 仅保留地图容器、地图初始化调用、组件挂载
|
||||
- [x] 移除配置请求和复杂 watch 编排逻辑
|
||||
- [x] 只通过编排器触发初始化和页面切换
|
||||
|
||||
## 8.2 `LayerController.vue`
|
||||
|
||||
- [x] 只保留树渲染与勾选事件
|
||||
- [x] 移除互斥规则
|
||||
- [x] 移除基础图层初始化编排逻辑
|
||||
|
||||
## 8.3 `MapLegend.vue`
|
||||
|
||||
- [x] 只保留图例渲染与点击事件分发
|
||||
- [x] 不直接调用地图实例
|
||||
|
||||
## 8.4 `MapFilter.vue`
|
||||
|
||||
- [x] 只保留表单与用户输入
|
||||
- [x] 搜索、容量、时间、基地变化全部通过编排器分发
|
||||
- [x] 不直接修改图例运行态和锚点显隐
|
||||
|
||||
验收标准:
|
||||
|
||||
- 组件文件职责单一;
|
||||
- UI 组件中不再出现大段业务逻辑;
|
||||
- 地图相关行为主要集中在编排层和 Store。
|
||||
|
||||
---
|
||||
|
||||
## 9. 阶段七:性能优化
|
||||
|
||||
## 9.1 请求与缓存优化
|
||||
|
||||
- [x] 将 `GisUrlList.ts` 预编译为索引字典,而不是每次请求时重建
|
||||
- [x] 实现真实请求去重
|
||||
- [x] 增加请求取消能力,避免菜单切换时旧请求回写
|
||||
- [x] 对图层加载增加并发控制
|
||||
- [x] 将缓存 key 与图层 key、筛选条件绑定
|
||||
|
||||
## 9.2 锚点显示优化
|
||||
|
||||
- [x] 图例切换时只处理目标图例对应的锚点集合
|
||||
- [x] 图层切换时优先切换图层可见性,不重复加点
|
||||
- [x] 减少 hover 时全量点图层重绘
|
||||
- [x] 减少样式函数中的业务判断复杂度
|
||||
|
||||
验收标准:
|
||||
|
||||
- 首屏加载时长不高于当前;
|
||||
- 图层勾选、图例勾选、基地切换响应速度优于当前;
|
||||
- 菜单快速切换时不出现旧数据回写。
|
||||
|
||||
---
|
||||
|
||||
## 10. 回归测试清单
|
||||
|
||||
每完成一个阶段,至少回归以下功能:
|
||||
|
||||
- [x] 地图初始化正常
|
||||
- [x] 默认基础图层正常显示
|
||||
- [x] 图层树勾选与取消勾选正常
|
||||
- [x] 图例随图层选中动态显示
|
||||
- [x] 图例点击能正确控制锚点显隐
|
||||
- [x] 基地切换后区域裁切和锚点过滤正常
|
||||
- [x] 锚点 hover Popup 正常
|
||||
- [x] 锚点点击详情弹窗正常
|
||||
- [x] 时间筛选、容量筛选、搜索定位正常
|
||||
- [x] 视频监控站与 AI 视频监控站互斥正常
|
||||
- [x] 菜单切换后地图数据不串页
|
||||
|
||||
---
|
||||
|
||||
## 11. 实施顺序建议
|
||||
|
||||
推荐严格按以下顺序执行:
|
||||
|
||||
1. 阶段一:现状收口与问题修复
|
||||
2. 阶段二:规则收口
|
||||
3. 阶段三:拆分 Store
|
||||
4. 阶段四:建立统一编排层
|
||||
5. 阶段五:拆分 OpenLayers 渲染层
|
||||
6. 阶段六:组件瘦身
|
||||
7. 阶段七:性能优化
|
||||
|
||||
不建议直接跳到渲染层拆分或性能优化,否则会在旧结构上重复返工。
|
||||
|
||||
---
|
||||
|
||||
## 12. 本轮交付标准
|
||||
|
||||
当本清单全部完成后,应达到以下结果:
|
||||
|
||||
- 地图模块核心职责划分清晰;
|
||||
- 图层树、图例、锚点、筛选、基地切换链路可追踪;
|
||||
- 关键规则不再散落在多个组件里;
|
||||
- `map.ts` 和 `map.ol.ts` 体积明显下降;
|
||||
- 新增地图业务时有明确扩展点;
|
||||
- 后续继续做底图体系优化、长期性能建设时不需要再推翻本轮结构。
|
||||
@ -3,8 +3,8 @@
|
||||
"version": "1.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite serve --mode development",
|
||||
"build": "vite build --mode production",
|
||||
"build:mvn": "vite build --mode production",
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production",
|
||||
"build:mvn": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint src/**/*.{ts,js,vue} --fix",
|
||||
"prettier": "prettier --write ."
|
||||
@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@element-plus/icons-vue": "^2.0.10",
|
||||
"@turf/turf": "^7.3.5",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@univerjs-pro/exchange-client": "0.15.0",
|
||||
"@univerjs/core": "0.14.0",
|
||||
@ -25,24 +24,20 @@
|
||||
"antdv-draggable-modal": "^1.1.6",
|
||||
"axios": "^1.2.0",
|
||||
"better-scroll": "^2.4.2",
|
||||
"cesium": "^1.141.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"echarts": "^5.2.2",
|
||||
"element-plus": "^2.2.27",
|
||||
"esri-leaflet": "^3.0.19",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"js-base64": "^3.7.5",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.18.1",
|
||||
"moment": "^2.30.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"ol": "^10.8.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pdfjs-dist": "^6.0.227",
|
||||
@ -75,6 +70,7 @@
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
@ -87,7 +83,6 @@
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "latest",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-cesium": "^1.2.22",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取图例列表
|
||||
export function getModuleMapLegendList(params?: { moduleId?: string }) {
|
||||
const url = params?.moduleId
|
||||
? `/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}`
|
||||
: '/mapLegend/getModuleMapLegendList';
|
||||
return request({
|
||||
url,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
// 获取地图配置列表
|
||||
export function getMapList(data: any) {
|
||||
return request({
|
||||
url: '/mapmodule/getMapData',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 获取梯级流域地图
|
||||
export function getQgcRvcd(data: any) {
|
||||
return request({
|
||||
url: '/eng/base/rsvrcscdb/getQgcRvcd',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 获取梯级流域下拉框列表
|
||||
export function getRvcdList(data: any) {
|
||||
return request({
|
||||
url: '/eng/base/rsvrcscdb/rvcd',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取梯级流域下拉框图表数据
|
||||
export function getKendoList(data: any) {
|
||||
return request({
|
||||
url: '/eng/base/rsvrcscdb/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 鱼类分布查询 - 站点查询
|
||||
export function getFishPointList(data: any) {
|
||||
return request({
|
||||
url: '/wte/we/fishPoint/qgc/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 鱼类分布查询 -鱼查询
|
||||
export function getFishList(data: any) {
|
||||
return request({
|
||||
url: '/wte/we/fishList/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 162 B |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 303 KiB |
@ -1,16 +1,3 @@
|
||||
// import {
|
||||
// NormalDatePickerFilter,
|
||||
// NormalFishFilter,
|
||||
// NormalFishFilter1,
|
||||
// NormalSelectFilter,
|
||||
// NormalStcdFilter,
|
||||
// NormalWEFilter,
|
||||
// NormalYearPickerFilter
|
||||
// } from '@zebras/qgc-share/components/mapModal/index'
|
||||
// import { Session } from '@zebras/qgc-share/service/Session'
|
||||
// import getUrl from '@zebras/qgc-share/utils/isQGCrul'
|
||||
// import { Utility } from '@zebras/qgc-share/utils/Utility'
|
||||
|
||||
// // 水电站 √
|
||||
const ENGTabs: Array<any> = [
|
||||
{
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div class="gis-view">
|
||||
<div id="mapContainer" />
|
||||
<div ref="popupRef" class="map-popup-container" style="display: none"></div>
|
||||
<!-- - 1. 切换菜单的时候图层没有切换 ,是因为接口的原因,现在没有接口
|
||||
2. 切换菜单的时候图例现在是不对的,默认选中的数据他们没有做处理
|
||||
-->
|
||||
<!-- 地图图例 -->
|
||||
<MapLegend />
|
||||
|
||||
<!-- 地图筛选器 -->
|
||||
<MapFilter v-if="showMapFilter" :map="mapClass" />
|
||||
|
||||
<!-- 地图控制器 -->
|
||||
<MapController :map="mapClass" :onClick="handleMapController" />
|
||||
|
||||
<!-- 基础图层切换器 -->
|
||||
<BaseLayerSwitcher :map="mapClass" />
|
||||
<!-- 梯级弹框 -->
|
||||
<TjLayerModal v-model:open="tjModalVisible" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
|
||||
import MapLegend from '@/components/mapLegend/index.vue';
|
||||
import MapFilter from '@/components/mapFilter/index.vue';
|
||||
import MapController from '@/components/mapController/index.vue';
|
||||
import BaseLayerSwitcher from '@/components/BaseLayerSwitcher/index.vue';
|
||||
import TjLayerModal from './TjLayerModal.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
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();
|
||||
const mapIsInited = ref(false);
|
||||
const tlyLayerVisible = ref(false);
|
||||
const tjModalVisible = ref(false);
|
||||
const popupRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
// 备注:根据当前路由生成页面配置 key,供统一编排器加载页面地图配置。
|
||||
const pageKey = computed(() => {
|
||||
const path = route.path;
|
||||
const parts = path.split('/');
|
||||
if (parts.length >= 3) {
|
||||
return parts[1] + '_' + parts[2];
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
// 备注:仅根据页面路径控制筛选器显隐,不在组件内承载地图业务逻辑。
|
||||
const showMapFilter = computed(() => {
|
||||
const path = route.path;
|
||||
return !path.includes('dianZhanZhuanTi/dianZhanZhuanTi');
|
||||
});
|
||||
|
||||
// 备注:仅保留当前菜单类型判断,缩放联动细节由编排器内部处理。
|
||||
const isShuiDianKaiFaMenu = computed(() => {
|
||||
return route.path.includes('home/shuiDianKaiFaZhuangKuang');
|
||||
});
|
||||
|
||||
// 备注:页面只负责触发地图初始化,初始化后的缩放监听和基地监听由编排器接管。
|
||||
const init = async () => {
|
||||
const container = document.getElementById('mapContainer') as HTMLElement;
|
||||
if (!container) return;
|
||||
|
||||
await mapOrchestrator.mountView({
|
||||
container,
|
||||
popupContainer: popupRef.value,
|
||||
pageKey: pageKey.value,
|
||||
getIsHydroMenu: () => isShuiDianKaiFaMenu.value
|
||||
});
|
||||
|
||||
mapIsInited.value = true;
|
||||
};
|
||||
|
||||
// 备注:地图控制器只分发简单视图切换命令,避免在页面入口堆叠复杂业务逻辑。
|
||||
const handleMapController = async (e: any, mapType: any) => {
|
||||
switch (e) {
|
||||
case 'dim':
|
||||
await mapOrchestrator.switchMapType(mapType, {
|
||||
popupContainer: popupRef.value,
|
||||
getIsHydroMenu: () => isShuiDianKaiFaMenu.value
|
||||
});
|
||||
uiStore.markMapSwitchCompleted();
|
||||
break;
|
||||
case 4: // 梯级流域显示
|
||||
tlyLayerVisible.value = !tlyLayerVisible.value;
|
||||
if (tlyLayerVisible.value) {
|
||||
tjModalVisible.value = true;
|
||||
fetchTjData();
|
||||
} else {
|
||||
tjModalVisible.value = false;
|
||||
mapClass.hideTertiarybasinLayer(servers.Tertiarybasin);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const fetchTjData = async () => {
|
||||
const res = await getQgcRvcd({});
|
||||
if (res && res.data) {
|
||||
let datas = [];
|
||||
for (let i = 0; i < res.data.data.length; i++) {
|
||||
datas.push(res.data.data[i].rvcd);
|
||||
}
|
||||
mapClass.addTertiarybasinLayer(
|
||||
servers.Tertiarybasin,
|
||||
'#4DFFDD',
|
||||
'#92A0A5',
|
||||
datas
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 备注:页面切换后仅触发编排器重载页面,不在组件内继续拼装业务联动。
|
||||
watch(
|
||||
() => pageKey.value,
|
||||
newVal => {
|
||||
if (newVal && mapIsInited.value) {
|
||||
void mapOrchestrator.handlePageChange(newVal, isShuiDianKaiFaMenu.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
window.mapClass = mapClass;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mapOrchestrator.unmountView();
|
||||
mapClass.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gis-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
visibility: visible;
|
||||
}
|
||||
#mapContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
cursor: grab;
|
||||
z-index: 1;
|
||||
}
|
||||
/* 消除瓦片之间的缝隙和默认边框 */
|
||||
.leaflet-tile {
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* 确保地图容器背景色与瓦片一致,避免透出底色 */
|
||||
.leaflet-container {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 如果使用 Canvas 模式,确保 Canvas 没有边框 */
|
||||
.leaflet-pane canvas {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.map-popup-container {
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 10px;
|
||||
}
|
||||
.custom-popup h4 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #333;
|
||||
}
|
||||
.custom-popup p {
|
||||
margin: 2px 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -1,673 +0,0 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="tj-cascade-chart"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts, EChartsOption } from 'echarts';
|
||||
import Gth from '@/assets/icons/gth.svg';
|
||||
|
||||
interface BasinStepItem {
|
||||
dmhg: number | null;
|
||||
ennm: string;
|
||||
normz: number | null;
|
||||
srcdis: number | null;
|
||||
stcd: string;
|
||||
bldsttCcode: string | number;
|
||||
estrydis: number | null;
|
||||
retentionExponent: number | null;
|
||||
qo?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
dataArr: BasinStepItem[];
|
||||
hideText?: boolean;
|
||||
specially?: boolean;
|
||||
}>(),
|
||||
{
|
||||
hideText: false,
|
||||
specially: true
|
||||
}
|
||||
);
|
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
const chartInstance = ref<ECharts | null>(null);
|
||||
let removeChartListeners: (() => void) | null = null;
|
||||
let pendingWheelFallbackTimer: number | null = null;
|
||||
|
||||
const getValueArrayByKey = (
|
||||
dataArr: BasinStepItem[],
|
||||
key: keyof BasinStepItem
|
||||
) => {
|
||||
return dataArr.map(item => {
|
||||
let value = item[key];
|
||||
if (value === undefined || value === null || isNaN(Number(value))) {
|
||||
return 0;
|
||||
}
|
||||
return Number(value);
|
||||
}) as number[];
|
||||
};
|
||||
|
||||
const generateCascaderData = (valArr: number[], valArrEstrydis: number[]) => {
|
||||
const result: number[][] = [];
|
||||
if (!valArr.length || !valArrEstrydis.length) return result;
|
||||
|
||||
const len = valArr.length + 1;
|
||||
const valMax = Math.max(...valArr) * 1.1;
|
||||
const estrydisMax = Math.max(...valArrEstrydis) * 1.05;
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
if (i === len - 1) {
|
||||
result.push([estrydisMax, valMax]);
|
||||
} else {
|
||||
result.push([valArrEstrydis[i], valArr[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const generateMarkAreaData = (
|
||||
valArr: number[],
|
||||
valArrEstrydis: number[],
|
||||
labelArrQo: Array<string | undefined>,
|
||||
valArrDmhg: number[],
|
||||
dataArr: BasinStepItem[]
|
||||
) => {
|
||||
const markAreaArr: any[] = [];
|
||||
if (!valArr.length || !valArrEstrydis.length || !valArrDmhg.length) {
|
||||
return markAreaArr;
|
||||
}
|
||||
|
||||
const len = valArr.length;
|
||||
const minArr = Math.min(...valArr);
|
||||
const maxArr = Math.max(...valArr);
|
||||
const valArea = maxArr - minArr;
|
||||
const criticalValue = valArea * 0.05;
|
||||
const estrydisMax = Math.max(...valArrEstrydis) * 1.05;
|
||||
const estrydisMin = Math.min(...valArrEstrydis) * 1.05;
|
||||
const step = (maxArr - minArr) / 20;
|
||||
const xStep = (estrydisMax - estrydisMin) / 1440;
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const current = dataArr[i];
|
||||
const bldsttCcode = current?.bldsttCcode;
|
||||
const normz = current?.normz;
|
||||
const isLast = i === len - 1;
|
||||
const color = String(bldsttCcode) === '2' ? '#6E7079' : '#D49CAE';
|
||||
|
||||
if (isLast) {
|
||||
const tempBlue = [
|
||||
{
|
||||
label: props.hideText
|
||||
? undefined
|
||||
: {
|
||||
color: '#58b438',
|
||||
formatter: [
|
||||
`{a|▽${valArr[i] || 0}}`,
|
||||
`{b|${labelArrQo[i] || ''}}`
|
||||
].join('\n'),
|
||||
distance: normz ? 0 : 30,
|
||||
rich: {
|
||||
a: { color: '#2F6B98', lineHeight: 24 },
|
||||
b: { color: '#2F6B98', height: 14 }
|
||||
}
|
||||
},
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
coord: [estrydisMax, maxArr]
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
coord: [valArrEstrydis[i], valArrDmhg[i - 1] ?? valArrDmhg[i] ?? 0]
|
||||
}
|
||||
];
|
||||
|
||||
const tempText = [
|
||||
{
|
||||
itemStyle: { color: '#D49CAE' },
|
||||
coord: [valArrEstrydis[i] + xStep, maxArr + step],
|
||||
label: props.hideText
|
||||
? undefined
|
||||
: {
|
||||
color,
|
||||
formatter: current?.ennm?.split('')?.join('\n'),
|
||||
distance: 20,
|
||||
fontSize: 16
|
||||
},
|
||||
sdt: current
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#D49CAE' },
|
||||
coord: [valArrEstrydis[i], valArrDmhg[i - 1] ?? valArrDmhg[i] ?? 0],
|
||||
label: props.hideText
|
||||
? undefined
|
||||
: {
|
||||
color,
|
||||
formatter: current?.ennm?.split('')?.join('\n'),
|
||||
distance: 20,
|
||||
fontSize: 16
|
||||
},
|
||||
sdt: current
|
||||
}
|
||||
];
|
||||
|
||||
markAreaArr.push(tempBlue, tempText);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tempBlue = [];
|
||||
const nextNormz = valArr[i + 1];
|
||||
|
||||
tempBlue.push({
|
||||
label: props.hideText
|
||||
? undefined
|
||||
: {
|
||||
color: '#58b438',
|
||||
formatter: [
|
||||
`{a|▽${valArr[i] || 0}}`,
|
||||
`{b|${labelArrQo[i] || ''}}`
|
||||
].join('\n'),
|
||||
distance: normz ? 0 : 30,
|
||||
rich: {
|
||||
a: { color: '#2F6B98', lineHeight: 24 },
|
||||
b: { color: '#2F6B98', height: 14 }
|
||||
}
|
||||
},
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
coord: [
|
||||
valArrEstrydis[i + 1],
|
||||
nextNormz - valArr[i] < criticalValue ? valArr[i] : valArr[i] || 0
|
||||
]
|
||||
});
|
||||
tempBlue.push({
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
coord: [valArrEstrydis[i], valArrDmhg[i] ?? 0]
|
||||
});
|
||||
|
||||
const tempText = [
|
||||
{
|
||||
itemStyle: { color: '#D49CAE' },
|
||||
coord: [valArrEstrydis[i] + xStep, valArr[i] + step],
|
||||
label: props.hideText
|
||||
? undefined
|
||||
: {
|
||||
color,
|
||||
formatter: current?.ennm?.split('')?.join('\n'),
|
||||
distance: 30,
|
||||
fontSize: 16
|
||||
},
|
||||
sdt: current
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#D49CAE' },
|
||||
coord: [valArrEstrydis[i], valArrDmhg[i - 1] ?? 0]
|
||||
}
|
||||
];
|
||||
|
||||
markAreaArr.push(tempBlue, tempText);
|
||||
}
|
||||
|
||||
return markAreaArr;
|
||||
};
|
||||
|
||||
const disposeChart = () => {
|
||||
if (pendingWheelFallbackTimer !== null) {
|
||||
window.clearTimeout(pendingWheelFallbackTimer);
|
||||
pendingWheelFallbackTimer = null;
|
||||
}
|
||||
if (removeChartListeners) {
|
||||
removeChartListeners();
|
||||
removeChartListeners = null;
|
||||
}
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose();
|
||||
chartInstance.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clampNumber = (value: number, min: number, max: number) => {
|
||||
if (Number.isNaN(value)) return min;
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const cloneMarkAreaData = (markAreaData: any[]) => {
|
||||
return JSON.parse(JSON.stringify(markAreaData ?? []));
|
||||
};
|
||||
|
||||
const renderChart = async () => {
|
||||
if (!chartRef.value || !props.dataArr.length) {
|
||||
disposeChart();
|
||||
return;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
const chartEl = chartRef.value;
|
||||
if (!chartEl) return;
|
||||
|
||||
const valArrNormz = getValueArrayByKey(props.dataArr, 'normz');
|
||||
const valArrDdz = getValueArrayByKey(props.dataArr, 'retentionExponent');
|
||||
const valArrDmhg = getValueArrayByKey(props.dataArr, 'dmhg');
|
||||
let valArrEstrydis = getValueArrayByKey(props.dataArr, 'estrydis');
|
||||
const labelArrQo = props.dataArr.map(item => item.qo);
|
||||
|
||||
if (!valArrNormz.length || !valArrEstrydis.length || !valArrDmhg.length) {
|
||||
disposeChart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.specially) {
|
||||
valArrEstrydis = [...valArrEstrydis];
|
||||
}
|
||||
|
||||
const valMin = Math.floor(Math.min(...valArrDmhg));
|
||||
const xMin = Math.min(...valArrEstrydis);
|
||||
const xDeduce = 50;
|
||||
const estrydisMaxForMarkArea = Math.max(...valArrEstrydis) * 1.05;
|
||||
let xMax = Math.max(...valArrEstrydis) + xDeduce;
|
||||
xMax = Math.max(xMax, estrydisMaxForMarkArea);
|
||||
|
||||
generateCascaderData(valArrNormz, valArrEstrydis);
|
||||
generateCascaderData(valArrDdz, valArrEstrydis);
|
||||
|
||||
const markAreaArr = generateMarkAreaData(
|
||||
valArrNormz,
|
||||
valArrEstrydis,
|
||||
labelArrQo,
|
||||
valArrDmhg,
|
||||
props.dataArr
|
||||
);
|
||||
|
||||
if (!markAreaArr.length) {
|
||||
disposeChart();
|
||||
return;
|
||||
}
|
||||
|
||||
const lineData = props.dataArr.map(item => [item.srcdis, item.dmhg]);
|
||||
lineData.push([
|
||||
xMax,
|
||||
(markAreaArr[markAreaArr.length - 2]?.[0]?.coord?.[1] ?? 0) + 100
|
||||
]);
|
||||
const baseMarkAreaArr = cloneMarkAreaData(markAreaArr);
|
||||
|
||||
const option: EChartsOption = {
|
||||
title: {
|
||||
text: `图中 " ▽ " 表示正常蓄水位。电站高程指坝址高程,里程指河口里程。 已建电站{a|(黑色)} 在建电站{b|(红色)} `,
|
||||
top: 10,
|
||||
left: '3%',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal',
|
||||
rich: {
|
||||
a: {
|
||||
color: '#6E7079',
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
b: {
|
||||
color: '#D49CAE',
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'image',
|
||||
z: 3,
|
||||
style: {
|
||||
image: Gth,
|
||||
width: 16,
|
||||
height: 16
|
||||
},
|
||||
left: '2%',
|
||||
top: 13,
|
||||
cursor: ' '
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
z: 3,
|
||||
style: {
|
||||
text: '高\n程\n︵\nm\n︶',
|
||||
fontSize: 16,
|
||||
fill: '#6E7079'
|
||||
},
|
||||
left: 20,
|
||||
top: 100,
|
||||
cursor: ' '
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: {
|
||||
boundaryGap: false,
|
||||
type: 'value',
|
||||
inverse: true,
|
||||
min: xMin,
|
||||
max: xMax,
|
||||
splitLine: {
|
||||
show: true
|
||||
},
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisTick: {
|
||||
show: true
|
||||
},
|
||||
name: '里程(km)',
|
||||
nameLocation: 'start'
|
||||
},
|
||||
grid: {
|
||||
top: 100,
|
||||
right: 140,
|
||||
left: 100,
|
||||
bottom: 30
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
filterMode: 'none',
|
||||
zoomOnMouseWheel: false,
|
||||
moveOnMouseWheel: false,
|
||||
moveOnMouseMove: false
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const sdt = params?.data?.sdt;
|
||||
const distance = params?.data?.coord?.[1]?.[0] ?? sdt?.srcdis ?? 0;
|
||||
if (sdt) {
|
||||
return (
|
||||
`${sdt.ennm}</br>` +
|
||||
`里程:${distance}km</br>` +
|
||||
`高程:${sdt.dmhg ?? ''}m`
|
||||
);
|
||||
}
|
||||
return null as any;
|
||||
}
|
||||
},
|
||||
toolbox: {
|
||||
right: 20,
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
name: '梯级图',
|
||||
title: '保存为图片'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
offset: 2,
|
||||
splitNumber: 5,
|
||||
type: 'value',
|
||||
min: valMin - 100,
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 14
|
||||
},
|
||||
axisLine: {
|
||||
onZero: false
|
||||
},
|
||||
axisTick: {}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
smooth: 0.4,
|
||||
showSymbol: false,
|
||||
lineStyle: { normal: { color: '#000000' } },
|
||||
areaStyle: { normal: { color: '#FEFCF0', opacity: 1 } },
|
||||
markArea: {
|
||||
data: markAreaArr,
|
||||
itemStyle: {
|
||||
opacity: 1,
|
||||
color: 'rgba(0,0,0,0)'
|
||||
}
|
||||
},
|
||||
data: lineData
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const primarySeries = (option as any).series[0] || {};
|
||||
const { markArea: _unusedMarkArea, ...secondarySeriesBase } = primarySeries;
|
||||
|
||||
(option as any).series[1] = {
|
||||
...secondarySeriesBase,
|
||||
yAxisIndex: 1,
|
||||
lineStyle: {
|
||||
color: '',
|
||||
opacity: 0
|
||||
}
|
||||
};
|
||||
|
||||
(option as any).yAxis[1] = {
|
||||
...(option as any).yAxis[0],
|
||||
offset: 70,
|
||||
position: 'right'
|
||||
};
|
||||
|
||||
const applyZoomVisualState = (
|
||||
startValue?: number | null,
|
||||
endValue?: number | null
|
||||
) => {
|
||||
if (!chartInstance.value) return;
|
||||
|
||||
const viewStart = Math.min(
|
||||
Number(startValue ?? xMin),
|
||||
Number(endValue ?? xMax)
|
||||
);
|
||||
const viewEnd = Math.max(
|
||||
Number(startValue ?? xMin),
|
||||
Number(endValue ?? xMax)
|
||||
);
|
||||
const visibleRange = Math.max(viewEnd - viewStart, 1);
|
||||
const chartWidth = Math.max(chartEl.offsetWidth - 80, 1);
|
||||
const xPx = chartWidth / visibleRange;
|
||||
|
||||
let prePoint = viewStart;
|
||||
const nextMarkAreaArr = baseMarkAreaArr.map((pair: any) => {
|
||||
if (!Array.isArray(pair)) return pair;
|
||||
return pair.map((point: any) => {
|
||||
const nextPoint = {
|
||||
...point,
|
||||
coord: Array.isArray(point?.coord) ? [...point.coord] : point?.coord,
|
||||
itemStyle: point?.itemStyle
|
||||
? { ...point.itemStyle }
|
||||
: point?.itemStyle,
|
||||
label: point?.label ? { ...point.label } : point?.label
|
||||
};
|
||||
|
||||
if (nextPoint?.label && Array.isArray(nextPoint?.coord)) {
|
||||
const currentX = Number(nextPoint.coord[0]);
|
||||
if (currentX >= viewStart && currentX <= viewEnd) {
|
||||
if ((currentX - prePoint) * xPx > 30) {
|
||||
nextPoint.label.show = true;
|
||||
prePoint = currentX;
|
||||
}
|
||||
} else {
|
||||
nextPoint.label.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
return nextPoint;
|
||||
});
|
||||
});
|
||||
|
||||
const visibleYValues: number[] = [];
|
||||
lineData.forEach(point => {
|
||||
const [x, y] = point;
|
||||
if (x >= viewStart && x <= viewEnd) {
|
||||
visibleYValues.push(y);
|
||||
}
|
||||
});
|
||||
nextMarkAreaArr.forEach((pair: any) => {
|
||||
if (!Array.isArray(pair) || pair.length < 2) return;
|
||||
const firstCoord = pair[0]?.coord;
|
||||
const secondCoord = pair[1]?.coord;
|
||||
if (!Array.isArray(firstCoord) || !Array.isArray(secondCoord)) return;
|
||||
const pairMinX = Math.min(Number(firstCoord[0]), Number(secondCoord[0]));
|
||||
const pairMaxX = Math.max(Number(firstCoord[0]), Number(secondCoord[0]));
|
||||
const intersects = pairMaxX >= viewStart && pairMinX <= viewEnd;
|
||||
if (!intersects) return;
|
||||
visibleYValues.push(Number(firstCoord[1]), Number(secondCoord[1]));
|
||||
});
|
||||
|
||||
const fallbackMin = valMin - 100;
|
||||
const fallbackMax = Math.max(
|
||||
...baseMarkAreaArr.flatMap((pair: any) => {
|
||||
if (!Array.isArray(pair)) return [];
|
||||
return pair
|
||||
.map((point: any) =>
|
||||
Array.isArray(point?.coord) ? Number(point.coord[1]) : null
|
||||
)
|
||||
.filter((value: number | null) => value != null) as number[];
|
||||
})
|
||||
);
|
||||
const minVisibleY =
|
||||
visibleYValues.length > 0 ? Math.min(...visibleYValues) : fallbackMin;
|
||||
const maxVisibleY =
|
||||
visibleYValues.length > 0 ? Math.max(...visibleYValues) : fallbackMax;
|
||||
const yPadding = Math.max((maxVisibleY - minVisibleY) * 0.08, 80);
|
||||
const nextYAxisMin = Math.floor(minVisibleY - yPadding);
|
||||
const nextYAxisMax = Math.ceil(maxVisibleY + yPadding);
|
||||
|
||||
chartInstance.value.setOption({
|
||||
yAxis: [
|
||||
{
|
||||
min: nextYAxisMin,
|
||||
max: nextYAxisMax
|
||||
},
|
||||
{
|
||||
min: nextYAxisMin,
|
||||
max: nextYAxisMax
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
markArea: {
|
||||
data: nextMarkAreaArr
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
disposeChart();
|
||||
chartInstance.value = echarts.init(chartEl);
|
||||
chartInstance.value.setOption(option, true);
|
||||
chartInstance.value.on('datazoom', (event: any) => {
|
||||
const currentOption = chartInstance.value?.getOption();
|
||||
const currentDataZoom = Array.isArray(currentOption?.dataZoom)
|
||||
? currentOption.dataZoom[0]
|
||||
: null;
|
||||
const nextStartValue =
|
||||
event?.batch?.[0]?.startValue ?? currentDataZoom?.startValue ?? xMin;
|
||||
const nextEndValue =
|
||||
event?.batch?.[0]?.endValue ?? currentDataZoom?.endValue ?? xMax;
|
||||
applyZoomVisualState(nextStartValue, nextEndValue);
|
||||
});
|
||||
|
||||
const zr = chartInstance.value.getZr();
|
||||
const handleWheel = (event: any) => {
|
||||
if (pendingWheelFallbackTimer !== null) {
|
||||
window.clearTimeout(pendingWheelFallbackTimer);
|
||||
}
|
||||
pendingWheelFallbackTimer = window.setTimeout(() => {
|
||||
pendingWheelFallbackTimer = null;
|
||||
if (!chartInstance.value) return;
|
||||
try {
|
||||
const currentOption = chartInstance.value.getOption();
|
||||
const currentDataZoom = Array.isArray(currentOption?.dataZoom)
|
||||
? currentOption.dataZoom[0]
|
||||
: null;
|
||||
const currentXAxis = Array.isArray(currentOption?.xAxis)
|
||||
? currentOption.xAxis[0]
|
||||
: currentOption?.xAxis;
|
||||
const axisMin = Number(currentXAxis?.min ?? xMin);
|
||||
const axisMax = Number(currentXAxis?.max ?? xMax);
|
||||
const startValue = Number(currentDataZoom?.startValue ?? axisMin);
|
||||
const endValue = Number(currentDataZoom?.endValue ?? axisMax);
|
||||
const totalRange = axisMax - axisMin;
|
||||
const currentRange = endValue - startValue;
|
||||
if (!totalRange || !currentRange) return;
|
||||
|
||||
const delta =
|
||||
Number(event?.wheelDelta ?? event?.zrDelta ?? event?.zrDeltaY ?? 0) ||
|
||||
0;
|
||||
const zoomFactor = delta > 0 ? 0.9 : 1 / 0.9;
|
||||
const nextRange = clampNumber(
|
||||
currentRange * zoomFactor,
|
||||
totalRange * 0.05,
|
||||
totalRange
|
||||
);
|
||||
const offsetX = Number(event?.offsetX ?? chartEl.offsetWidth / 2);
|
||||
const anchorRatio = clampNumber(
|
||||
offsetX / Math.max(chartEl.offsetWidth, 1),
|
||||
0,
|
||||
1
|
||||
);
|
||||
const anchorValue = startValue + currentRange * anchorRatio;
|
||||
let nextStartValue = anchorValue - nextRange * anchorRatio;
|
||||
let nextEndValue = nextStartValue + nextRange;
|
||||
|
||||
if (nextStartValue < axisMin) {
|
||||
nextEndValue += axisMin - nextStartValue;
|
||||
nextStartValue = axisMin;
|
||||
}
|
||||
if (nextEndValue > axisMax) {
|
||||
nextStartValue -= nextEndValue - axisMax;
|
||||
nextEndValue = axisMax;
|
||||
}
|
||||
|
||||
nextStartValue = clampNumber(nextStartValue, axisMin, axisMax);
|
||||
nextEndValue = clampNumber(nextEndValue, axisMin, axisMax);
|
||||
|
||||
chartInstance.value.dispatchAction({
|
||||
type: 'dataZoom',
|
||||
dataZoomIndex: 0,
|
||||
startValue: nextStartValue,
|
||||
endValue: nextEndValue
|
||||
});
|
||||
} catch (_error: any) {}
|
||||
}, 32);
|
||||
};
|
||||
zr.on('mousewheel', handleWheel);
|
||||
removeChartListeners = () => {
|
||||
zr.off('mousewheel', handleWheel);
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.dataArr],
|
||||
async () => {
|
||||
await renderChart();
|
||||
},
|
||||
{ deep: true, immediate: true, flush: 'post' }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await renderChart();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disposeChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tj-cascade-chart {
|
||||
width: 100%;
|
||||
height: 32vh;
|
||||
min-height: 280px;
|
||||
}
|
||||
</style>
|
||||
@ -1,537 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
:width="modalWidth"
|
||||
:footer="null"
|
||||
:closable="false"
|
||||
:destroy-on-close="false"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<template #title>
|
||||
<div class="tj-modal-header">
|
||||
<div class="tj-modal-title">梯级图</div>
|
||||
<div class="tj-modal-actions">
|
||||
<ExpandAltOutlined
|
||||
v-if="!isExpanded"
|
||||
class="tj-modal-action-icon"
|
||||
@click.stop="toggleExpand"
|
||||
/>
|
||||
<ShrinkOutlined
|
||||
v-else
|
||||
class="tj-modal-action-icon"
|
||||
@click.stop="toggleExpand"
|
||||
/>
|
||||
<CloseOutlined
|
||||
class="tj-modal-action-icon ml-3"
|
||||
@click.stop="handleClose"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="tj-modal">
|
||||
<a-form :model="{ rvcd }" class="tj-form">
|
||||
<a-form-item label="梯级流域">
|
||||
<a-select
|
||||
v-model:value="rvcd"
|
||||
style="width: 220px"
|
||||
placeholder="请选择流域"
|
||||
:options="options"
|
||||
:loading="loadingOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
@change="handleRvcdChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div v-if="tableShowType === 'dataZoom'" class="tj-toolbar">
|
||||
<RollbackOutlined
|
||||
class="tj-back-icon"
|
||||
@click="handleBackTableShowType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-empty
|
||||
v-if="!rvcd"
|
||||
class="tj-empty"
|
||||
description="请先选择流域"
|
||||
:image="false"
|
||||
/>
|
||||
|
||||
<a-spin v-else :spinning="loadingChart">
|
||||
<a-empty
|
||||
v-show="rvcd && !currentDisplayData.length && !loadingChart"
|
||||
class="tj-empty"
|
||||
description="暂无数据"
|
||||
:image="false"
|
||||
/>
|
||||
|
||||
<div v-show="currentDisplayData.length > 0" class="tj-content">
|
||||
<!-- <div class="tj-tip">
|
||||
<ExclamationCircleOutlined class="tj-tip-icon" />
|
||||
<span style="font-size: 14px; font-weight: normal; color: #464646">
|
||||
图中 " ▽ "
|
||||
表示正常蓄水位。电站高程指坝址高程,里程指河口里程。已建电站
|
||||
<span style="color: #6e7079; font-size: 14px; font-weight: normal"
|
||||
>(黑色)</span
|
||||
>
|
||||
在建电站
|
||||
<span style="color: #d49cae; font-size: 14px; font-weight: normal"
|
||||
>(红色)</span
|
||||
>
|
||||
</span>
|
||||
</div> -->
|
||||
|
||||
<TjCascadeChart
|
||||
:key="`${rvcd || 'empty'}-${isExpanded ? 'expanded' : 'normal'}`"
|
||||
class="tj-chart"
|
||||
:data-arr="basinStepChartData"
|
||||
:specially="true"
|
||||
/>
|
||||
|
||||
<div class="tj-table-wrap">
|
||||
<div class="tj-table-left">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>电站</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>高程(m)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>里程(km)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tj-table-right">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, index) in displayDataReversed"
|
||||
:key="`${item.stcd || item.ennm || index}-name`"
|
||||
:style="{ color: getStationColor(item) }"
|
||||
>
|
||||
<a-tooltip :title="item.ennm">
|
||||
<span class="tj-station-name">
|
||||
<template
|
||||
v-for="(char, charIndex) in getTitleChars(
|
||||
item.ennm
|
||||
)"
|
||||
:key="`${item.ennm || 'station'}-${charIndex}`"
|
||||
>
|
||||
<span>{{ char }}</span>
|
||||
<br v-if="char !== '...'" />
|
||||
</template>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, index) in displayDataReversed"
|
||||
:key="`${item.stcd || item.ennm || index}-dmhg`"
|
||||
>
|
||||
<span>{{ item.dmhg ?? '' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, index) in displayDataReversed"
|
||||
:key="`${item.stcd || item.ennm || index}-srcdis`"
|
||||
>
|
||||
<span>{{ item.estrydis ?? '' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
CloseOutlined,
|
||||
ExpandAltOutlined,
|
||||
RollbackOutlined,
|
||||
ShrinkOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { getKendoList, getRvcdList } from '@/api/map';
|
||||
import TjCascadeChart from './TjCascadeChart.vue';
|
||||
|
||||
interface BasinOption {
|
||||
label: string;
|
||||
value: string;
|
||||
objId: string;
|
||||
}
|
||||
|
||||
interface BasinStepItem {
|
||||
dmhg: number | null;
|
||||
ennm: string;
|
||||
normz: number | null;
|
||||
srcdis: number | null;
|
||||
stcd: string;
|
||||
bldsttCcode: string | number;
|
||||
estrydis: number | null;
|
||||
retentionExponent: number | null;
|
||||
qo?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const modalWidth = computed(() => (isExpanded.value ? '95%' : '75%'));
|
||||
const rvcd = ref<string | undefined>();
|
||||
const loadingOptions = ref(false);
|
||||
const loadingChart = ref(false);
|
||||
const options = ref<BasinOption[]>([]);
|
||||
const basinStepChartData = ref<BasinStepItem[]>([]);
|
||||
const dataZoomBasinStepChartData = ref<BasinStepItem[]>([]);
|
||||
const tableShowType = ref<'table' | 'dataZoom'>('table');
|
||||
|
||||
const currentDisplayData = computed(() =>
|
||||
tableShowType.value === 'dataZoom'
|
||||
? dataZoomBasinStepChartData.value
|
||||
: basinStepChartData.value
|
||||
);
|
||||
|
||||
const displayDataReversed = computed(() =>
|
||||
[...currentDisplayData.value].reverse()
|
||||
);
|
||||
|
||||
const resetModalState = () => {
|
||||
rvcd.value = undefined;
|
||||
basinStepChartData.value = [];
|
||||
dataZoomBasinStepChartData.value = [];
|
||||
tableShowType.value = 'table';
|
||||
loadingChart.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetModalState();
|
||||
emit('update:open', false);
|
||||
};
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
};
|
||||
|
||||
const toNumber = (value: any): number | null => {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
};
|
||||
|
||||
const getStationColor = (item: BasinStepItem) =>
|
||||
String(item?.bldsttCcode) === '2' ? '#262626' : '#D49CAE';
|
||||
|
||||
const getTitleChars = (title?: string) => {
|
||||
const chars = (title || '').split('');
|
||||
if (chars.length <= 6) return chars;
|
||||
return [...chars.slice(0, 6), '...'];
|
||||
};
|
||||
|
||||
const parseZoomValues = (event: any) => {
|
||||
const batch = event?.batch?.[0] || event || {};
|
||||
let { startValue, endValue } = batch;
|
||||
|
||||
if (startValue === undefined || endValue === undefined) {
|
||||
const srcdisValues = basinStepChartData.value
|
||||
.map(item => item.srcdis)
|
||||
.filter(value => value !== null && value !== undefined) as number[];
|
||||
if (!srcdisValues.length) return null;
|
||||
|
||||
const min = Math.min(...srcdisValues);
|
||||
const max = Math.max(...srcdisValues);
|
||||
const start = Number(batch.start ?? 0);
|
||||
const end = Number(batch.end ?? 100);
|
||||
|
||||
startValue = min + ((max - min) * start) / 100;
|
||||
endValue = min + ((max - min) * end) / 100;
|
||||
}
|
||||
|
||||
return {
|
||||
min: Math.min(Number(startValue), Number(endValue)),
|
||||
max: Math.max(Number(startValue), Number(endValue)),
|
||||
start: Number(batch.start ?? 0),
|
||||
end: Number(batch.end ?? 100)
|
||||
};
|
||||
};
|
||||
|
||||
const handleChartDataZoom = (event: any) => {
|
||||
const zoomValues = parseZoomValues(event);
|
||||
if (!zoomValues) return;
|
||||
|
||||
if (zoomValues.start <= 0 && zoomValues.end >= 100) {
|
||||
tableShowType.value = 'table';
|
||||
dataZoomBasinStepChartData.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = basinStepChartData.value.filter(item => {
|
||||
const distance = Number(item.srcdis ?? 0);
|
||||
return distance >= zoomValues.min && distance <= zoomValues.max;
|
||||
});
|
||||
|
||||
if (!filtered.length) return;
|
||||
|
||||
tableShowType.value = 'dataZoom';
|
||||
dataZoomBasinStepChartData.value = filtered;
|
||||
};
|
||||
|
||||
const loadRvcdOptions = async () => {
|
||||
if (options.value.length > 0) return;
|
||||
|
||||
loadingOptions.value = true;
|
||||
try {
|
||||
const res = await getRvcdList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'wbsType',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'PSB_RVCD'
|
||||
},
|
||||
{
|
||||
field: 'treeLevel',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: '1'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const list = Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: Array.isArray(res?.data)
|
||||
? res.data
|
||||
: [];
|
||||
|
||||
options.value = list.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode,
|
||||
objId: item.objId
|
||||
}));
|
||||
} finally {
|
||||
loadingOptions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBasinStepChart = async (value?: string) => {
|
||||
rvcd.value = value;
|
||||
basinStepChartData.value = [];
|
||||
dataZoomBasinStepChartData.value = [];
|
||||
tableShowType.value = 'table';
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingChart.value = true;
|
||||
try {
|
||||
const res = await getKendoList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'rvcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
field: 'srcdis',
|
||||
dir: 'desc'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const list = Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: Array.isArray(res?.data)
|
||||
? res.data
|
||||
: [];
|
||||
|
||||
basinStepChartData.value = list
|
||||
.map((item: any) => ({
|
||||
dmhg: toNumber(item.dmhg),
|
||||
ennm: item.stnm || item.ennm || '',
|
||||
normz: toNumber(item.normz),
|
||||
srcdis: toNumber(item.srcdis),
|
||||
stcd: item.stcd || '',
|
||||
bldsttCcode: item.bldsttCcode ?? '',
|
||||
estrydis: toNumber(item.srcdis),
|
||||
retentionExponent: toNumber(item.dmhg),
|
||||
qo: item.qo
|
||||
}))
|
||||
.filter(
|
||||
item =>
|
||||
item.dmhg !== null && item.normz !== null && item.srcdis !== null
|
||||
)
|
||||
.reverse();
|
||||
} finally {
|
||||
loadingChart.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRvcdChange = async (value: string | undefined) => {
|
||||
await fetchBasinStepChart(value);
|
||||
};
|
||||
|
||||
const handleBackTableShowType = () => {
|
||||
tableShowType.value = 'table';
|
||||
dataZoomBasinStepChartData.value = [];
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
async newVal => {
|
||||
if (newVal) {
|
||||
await loadRvcdOptions();
|
||||
} else {
|
||||
resetModalState();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tj-modal {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tj-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tj-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tj-modal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tj-modal-action-icon {
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tj-form :deep(.ant-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tj-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tj-back-icon {
|
||||
font-size: 16px;
|
||||
color: #00000073;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tj-empty {
|
||||
height: 250px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tj-content {
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tj-chart {
|
||||
width: 100%;
|
||||
height: 32vh;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.tj-table-wrap {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
padding-right: 64px;
|
||||
}
|
||||
|
||||
.tj-table-left {
|
||||
width: 94px;
|
||||
flex: 0 0 94px;
|
||||
}
|
||||
|
||||
.tj-table-right {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tj-table-left table,
|
||||
.tj-table-right table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tj-table-left td,
|
||||
.tj-table-right td {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
border: 1px solid #a9afb3;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tj-table-left tr:nth-child(1) td,
|
||||
.tj-table-right tr:nth-child(1) td {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.tj-table-left tr:nth-child(2) td,
|
||||
.tj-table-left tr:nth-child(3) td,
|
||||
.tj-table-right tr:nth-child(2) td,
|
||||
.tj-table-right tr:nth-child(3) td {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.tj-station-name {
|
||||
display: inline-block;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@ -1,537 +0,0 @@
|
||||
// import { Session } from '@zebras/qgc-share/service/Session'
|
||||
import domtoimage from 'dom-to-image';
|
||||
import {
|
||||
offset2,
|
||||
drawDotImg2,
|
||||
drawDotImg1,
|
||||
offset1,
|
||||
drawDotImg3,
|
||||
offset3,
|
||||
drawDotImg5,
|
||||
offset5
|
||||
} from '@/utils/GisUrlList';
|
||||
declare global {
|
||||
interface Window {
|
||||
__lyConfigs?: {
|
||||
theme?: string;
|
||||
[key: string]: any; // 根据实际配置结构补充具体字段,或使用索引签名兼容其他属性
|
||||
};
|
||||
__mapMode?: string; // 建议同时声明代码中用到的其他全局变量
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据镜头高度获取地图级别
|
||||
* @param {Number} height
|
||||
*/
|
||||
const A = 40487.57;
|
||||
const B = 0.00007096758;
|
||||
const C = 91610.74;
|
||||
const D = -40467.74;
|
||||
|
||||
/**
|
||||
* 图例数据转对象
|
||||
* @param data 图例数据
|
||||
* @returns 以nameEn为下标的图例数据
|
||||
*/
|
||||
export const legendData2Obj = (data: any[]) => {
|
||||
const _tempData: any = {};
|
||||
const f = (_data: any[]) => {
|
||||
_data.forEach(item => {
|
||||
// childrenList有值表示这是一个分组
|
||||
if (item?.childrenList && item.childrenList?.length > 0) {
|
||||
f(item.childrenList);
|
||||
} else {
|
||||
_tempData[item.nameEn] = item;
|
||||
}
|
||||
});
|
||||
};
|
||||
f(data);
|
||||
return _tempData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加标签偏移量等属性到item中
|
||||
* @param item - 待添加属性的item
|
||||
* @param index - item的索引
|
||||
* @param labelType - 标签类型
|
||||
*/
|
||||
export const appendOffsetPropties = (item: any, index: any, labelType = 2) => {
|
||||
let drawDotImg = null;
|
||||
let offset = null;
|
||||
if (labelType == 2) {
|
||||
drawDotImg = drawDotImg1;
|
||||
offset = offset1;
|
||||
addItemProperty(item, index, drawDotImg, offset);
|
||||
} else if (labelType == 3) {
|
||||
drawDotImg = drawDotImg2;
|
||||
offset = offset2;
|
||||
addItemProperty(item, index, drawDotImg, offset);
|
||||
} else if (labelType == 4) {
|
||||
drawDotImg = drawDotImg3;
|
||||
offset = offset3;
|
||||
addItemProperty2(item, index, drawDotImg, offset);
|
||||
} else if (labelType == 5) {
|
||||
drawDotImg = drawDotImg5;
|
||||
offset = offset5;
|
||||
addItemProperty2(item, index, drawDotImg, offset);
|
||||
}
|
||||
};
|
||||
|
||||
const addItemProperty2 = (
|
||||
item: any,
|
||||
index: any,
|
||||
drawDotImg: any,
|
||||
offset: any
|
||||
) => {
|
||||
if (!drawDotImg || !offset) return;
|
||||
item.icon_image =
|
||||
index % 2 !== 0
|
||||
? drawDotImg[item.anchoPointState]?.left || ''
|
||||
: drawDotImg[item.anchoPointState]?.right || '';
|
||||
item.text_anchor = index % 2 !== 0 ? 'right' : 'left';
|
||||
item.text_offset = offset[item.icon_image]?.text_offset ?? [-10, -1.8];
|
||||
item.text_offset2 = offset[item.icon_image]?.text_offset2 ?? [-10, -1.8];
|
||||
item.icon_offset = [
|
||||
offset[item.icon_image]?.icon_x ?? -200,
|
||||
offset[item.icon_image]?.icon_y ?? -50
|
||||
];
|
||||
item.icon_offset2 = offset[item.icon_image]?.icon_offset2 ?? [-200, -50];
|
||||
item.billboard_offset = [
|
||||
offset[item.icon_image]?.billboard_x ?? -80,
|
||||
offset[item.icon_image]?.billboard_y ?? -15
|
||||
];
|
||||
item.label_offset = offset[item.icon_image]?.labelOffset ?? [100, 41];
|
||||
};
|
||||
|
||||
export const addItemProperty = (
|
||||
item: any,
|
||||
index: any,
|
||||
drawDotImg: any,
|
||||
offset: any
|
||||
) => {
|
||||
if (!drawDotImg || !offset) return;
|
||||
item.icon_image =
|
||||
index % 2 !== 0
|
||||
? drawDotImg[item.anchoPointState]?.left || ''
|
||||
: drawDotImg[item.anchoPointState]?.right || '';
|
||||
item.text_offset = [
|
||||
offset[item.icon_image]?.text_x ?? -10,
|
||||
offset[item.icon_image]?.text_y ?? -1.8
|
||||
];
|
||||
item.icon_offset = [
|
||||
offset[item.icon_image]?.icon_x ?? -200,
|
||||
offset[item.icon_image]?.icon_y ?? -50
|
||||
];
|
||||
item.billboard_offset = [
|
||||
offset[item.icon_image]?.billboard_x ?? -80,
|
||||
offset[item.icon_image]?.billboard_y ?? -15
|
||||
];
|
||||
item.label_offset = offset[item.icon_image]?.labelOffset ?? [100, 41];
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置popName属性
|
||||
* @param item - 待设置popName的item
|
||||
*/
|
||||
export const setPopName = (item: any) => {
|
||||
if (item.sttp === 'ENG') {
|
||||
item.popName = item.ennm || item.titleName;
|
||||
} else if (item.sttp === 'ylfb') {
|
||||
if (item?.ftp?.length > 20) {
|
||||
item.popName = item.ftp.slice(0, 20) + '...';
|
||||
} else {
|
||||
item.popName = item?.ftp;
|
||||
}
|
||||
} else if (item.sttp === 'WE_FISH') {
|
||||
item.popName =
|
||||
item.fishList?.[0]?.fishName + `(${item.fishList?.[0]?.ptypeName})`;
|
||||
item.popName1 = item.total + '尾';
|
||||
} else {
|
||||
item.popName = item.stnm || item.titleName;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取图例配置中选中项的key
|
||||
* @param data - 图例配置数据
|
||||
* @returns 选中项的key数组
|
||||
*/
|
||||
export const getCheckedLayerConfigs = (data: any[]): any[] => {
|
||||
const rs: any[] = [];
|
||||
const f = (arr: any[] = []) => {
|
||||
let count = 0;
|
||||
arr.forEach((item: any) => {
|
||||
if (item?.children && item.children.length > 0) {
|
||||
const childrenCount = f(item.children);
|
||||
if (childrenCount) {
|
||||
rs.push(item.key);
|
||||
}
|
||||
} else if (item.checked) {
|
||||
count++;
|
||||
rs.push(item.key); // 对应图例的layerCode
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
f(data);
|
||||
return rs;
|
||||
};
|
||||
|
||||
// type mapType = "" | "pointMap" | "gisLayer"
|
||||
|
||||
/**
|
||||
* 将图层配置数据转换为一维数组
|
||||
* @param data - 图层配置数据
|
||||
* @returns 一维数组
|
||||
*/
|
||||
export const layerConfig2Flat = (data: any): any[] => {
|
||||
const rs: any[] = [];
|
||||
const f = (arr: any[] = []) => {
|
||||
arr.forEach((item: any) => {
|
||||
const { type } = item;
|
||||
if (type) {
|
||||
if (type == 'GISMap') {
|
||||
if (item?.children && item.children.length > 0) {
|
||||
f(item.children);
|
||||
} else {
|
||||
rs.push(item);
|
||||
}
|
||||
} else if (type == 'pointMap') {
|
||||
if (
|
||||
item.url ||
|
||||
item.title === '国家水文站' ||
|
||||
item.title === '自建水文站'
|
||||
) {
|
||||
rs.push(item);
|
||||
} else if (item.children.length > 0) {
|
||||
f(item.children);
|
||||
}
|
||||
}
|
||||
} else if (item.children.length > 0) {
|
||||
f(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
f(data);
|
||||
return rs;
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换图层配置中的 URL
|
||||
* @param layerConfigsArr - 图层配置数组
|
||||
* @param urlList - URL 配置列表
|
||||
* @returns 更新后的图层配置数组
|
||||
*/
|
||||
export const replaceUrl = (layerConfigsArr: any[], urlList: any[]): any[] => {
|
||||
const bDict: Record<string, any[]> = {};
|
||||
|
||||
// 构建 URL 映射字典
|
||||
for (let i = 0; i < urlList.length; i++) {
|
||||
const key = urlList[i].url;
|
||||
const title = urlList[i].title;
|
||||
const keyType = urlList[i].keyType;
|
||||
|
||||
if (!bDict[key]) bDict[key] = [];
|
||||
if (!bDict[title]) bDict[title] = [];
|
||||
if (!bDict[keyType]) bDict[keyType] = [];
|
||||
|
||||
bDict[key].push(urlList[i]);
|
||||
bDict[title].push(urlList[i]);
|
||||
if (bDict[keyType]) {
|
||||
bDict[keyType].push(urlList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 替换图层配置中的 URL
|
||||
for (let i = 0; i < layerConfigsArr.length; i++) {
|
||||
const obj = layerConfigsArr[i];
|
||||
const key = obj.url;
|
||||
const title = obj.title;
|
||||
const keyType = obj.key;
|
||||
|
||||
const bObjList = bDict[keyType] || bDict[title] || bDict[key];
|
||||
|
||||
if (bObjList && bObjList.length > 0) {
|
||||
const bObj = bObjList[0];
|
||||
obj.url = bObj.url;
|
||||
obj.urlThd = bObj.url;
|
||||
obj.params = bObj.params;
|
||||
obj.orders = bObj.orders;
|
||||
} else {
|
||||
obj.url = '';
|
||||
}
|
||||
}
|
||||
|
||||
return layerConfigsArr;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取基础图层配置
|
||||
* @param config - 接口获取的配置数据
|
||||
* @returns 基础图层配置
|
||||
*/
|
||||
export const getMapConfig = (config: any) => {
|
||||
const r = { ...config };
|
||||
const baseUrlObj = import.meta.env.VITE_APP_MAP_URL;
|
||||
if (baseUrlObj) {
|
||||
r.url = baseUrlObj + r.url; // 'http://localhost:8088'
|
||||
r.url_3d = baseUrlObj + r.url_3d;
|
||||
// r.url = baseUrlObj.url + r.url //baseUrlObj.url
|
||||
// r.url_3d = baseUrlObj.url + r.url_3d
|
||||
if (r.geojson_url) {
|
||||
r.geojson_url = baseUrlObj + r.geojson_url;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置地图元素位置
|
||||
*/
|
||||
export const resetMapElPos = () => {
|
||||
const legend = document.querySelector('#qgc-legendtl') as HTMLElement; // 图例
|
||||
// const filter = document.querySelector('#map-filter-container') as HTMLElement // 全局表单
|
||||
const controller = document.querySelector('#map-controller') as HTMLElement; // 地图工具栏
|
||||
const baselayer = document.querySelector('#map-baselayer') as HTMLElement; // 底图模式切换
|
||||
|
||||
if (legend) {
|
||||
legend.style.left = '0';
|
||||
legend.style.bottom = '0';
|
||||
}
|
||||
if (controller) {
|
||||
controller.style.right = '480px';
|
||||
controller.style.bottom = '114px';
|
||||
}
|
||||
if (baselayer) {
|
||||
baselayer.style.right = '480px';
|
||||
baselayer.style.bottom = '20px';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据位置获取布局数据列表
|
||||
* @param data - 布局数据
|
||||
* @param position - 位置
|
||||
* @returns 布局数据列表
|
||||
*/
|
||||
const getListByPosition = (data: any, position: string) =>
|
||||
data?.data?.filter((el: any) => el.position === position && el.code);
|
||||
|
||||
/**
|
||||
* 设置地图组件位置
|
||||
* @param layoutType - 布局类型
|
||||
* @param data - 布局数据
|
||||
* @param offset - 偏移量
|
||||
*/
|
||||
export const setMapLegendPos = (
|
||||
layoutType: string,
|
||||
data: any,
|
||||
offset = 456
|
||||
) => {
|
||||
const menuStateString = localStorage.getItem('menuState'); //处理澜沧江左侧菜单状态
|
||||
const menuState =
|
||||
menuStateString !== null ? JSON.parse(menuStateString) : true;
|
||||
const _theme = localStorage.getItem('ly-theme') || window.__lyConfigs?.theme;
|
||||
const leftEle = document.querySelector('#page-layout-left') as HTMLElement;
|
||||
const rightEle = document.querySelector('#page-layout-right') as HTMLElement;
|
||||
const bottomEle = document.querySelector(
|
||||
'#page-layout-bottom'
|
||||
) as HTMLElement;
|
||||
const legend = document.querySelector('#qgc-legendtl') as HTMLElement; // 图例
|
||||
const filter = document.querySelector('#map-filter-container') as HTMLElement; // 全局表单
|
||||
const compassControl = document.querySelector(
|
||||
'#map-compassControl'
|
||||
) as HTMLElement; // 全局表单
|
||||
const controller = document.querySelector('#map-controller') as HTMLElement; // 地图工具栏
|
||||
const monitor = document.querySelector('#map-monitor') as HTMLElement; // 地图工具栏
|
||||
const baselayer = document.querySelector('#map-baselayer') as HTMLElement; // 底图模式切换
|
||||
// const vd = document.querySelector('#vd_operate') as HTMLElement // 底部视频
|
||||
const left = [
|
||||
'layout1',
|
||||
'layout2',
|
||||
'layout3',
|
||||
'layout4',
|
||||
'layout6',
|
||||
'layout7',
|
||||
'layout8',
|
||||
'layout9',
|
||||
'layout10',
|
||||
'layout11',
|
||||
'layout14',
|
||||
'layout15',
|
||||
'layout16',
|
||||
'layout17'
|
||||
]; // 左侧布局
|
||||
const right = [
|
||||
'layout1',
|
||||
'layout2',
|
||||
'layout3',
|
||||
'layout4',
|
||||
'layout5',
|
||||
'layout6',
|
||||
'layout8',
|
||||
'layout10',
|
||||
'layout11',
|
||||
'layout15',
|
||||
'layout16',
|
||||
'layout17'
|
||||
]; // 右侧布局
|
||||
const bottom1 = [
|
||||
'layout1',
|
||||
'layout6',
|
||||
'layout8',
|
||||
'layout9',
|
||||
'layout10',
|
||||
'layout16'
|
||||
]; // 三行底部布局
|
||||
const bottom2 = ['layout2', 'layout15']; // 四行底部布局
|
||||
const w = `${offset}px`;
|
||||
const l = `${_theme === 'ly-8' ? (menuState ? 643 : 510) : offset}px`;
|
||||
let b = `0px`;
|
||||
const le = ['layout17', 'layout10'].includes(layoutType) ? 0 : 1;
|
||||
const leftList = getListByPosition(data, 'left');
|
||||
const rightList = getListByPosition(data, 'right');
|
||||
const bottomList = getListByPosition(data, 'bottom');
|
||||
let bottom = '0';
|
||||
|
||||
if (_theme === 'ly-8') {
|
||||
if (window.__mapMode === '3D') {
|
||||
b = `200px`;
|
||||
} else {
|
||||
b = `${menuState ? 200 : 50}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (bottomList?.length > 0) {
|
||||
if (bottom1.includes(layoutType)) {
|
||||
bottom = 'calc((100% - 16px) / 3 + 8px)';
|
||||
}
|
||||
if (bottom2.includes(layoutType)) {
|
||||
bottom = 'calc((100% - 24px) / 4 + 8px)';
|
||||
}
|
||||
} else {
|
||||
// 没有底部布局时,底部高度为
|
||||
bottom = '28px';
|
||||
}
|
||||
|
||||
const rle = ['layout6'].includes(layoutType) || bottom != '28px' ? 0 : 1;
|
||||
|
||||
const leftHide = leftEle?.classList?.contains('hide');
|
||||
const rightHide = rightEle?.classList?.contains('hide');
|
||||
const bottomHide = bottomEle?.classList?.contains('hide');
|
||||
if (legend) {
|
||||
legend.style.left =
|
||||
!leftHide && left.includes(layoutType) && leftList?.length > le ? l : b;
|
||||
legend.style.bottom = bottomHide
|
||||
? '0'
|
||||
: bottomList?.length > 0
|
||||
? bottom
|
||||
: '12px';
|
||||
}
|
||||
if (filter) {
|
||||
if (layoutType === 'layout10') {
|
||||
filter.style.left =
|
||||
!leftHide && left.includes(layoutType) && leftList?.length > 1 ? l : b;
|
||||
} else {
|
||||
filter.style.left =
|
||||
!leftHide &&
|
||||
left.includes(layoutType) &&
|
||||
leftList?.length > 0 &&
|
||||
layoutType !== 'layout17'
|
||||
? l
|
||||
: b;
|
||||
}
|
||||
}
|
||||
if (compassControl) {
|
||||
if (layoutType === 'layout10') {
|
||||
compassControl.style.left =
|
||||
!leftHide && left.includes(layoutType) && leftList?.length > 1 ? l : b;
|
||||
} else {
|
||||
compassControl.style.left =
|
||||
!leftHide &&
|
||||
left.includes(layoutType) &&
|
||||
leftList?.length > 0 &&
|
||||
layoutType !== 'layout17'
|
||||
? l
|
||||
: b;
|
||||
}
|
||||
}
|
||||
if (controller) {
|
||||
controller.style.right =
|
||||
!rightHide && right.includes(layoutType) && rightList?.length > rle
|
||||
? w
|
||||
: '0';
|
||||
controller.style.bottom = bottomHide ? '0' : bottom;
|
||||
}
|
||||
if (monitor) {
|
||||
monitor.style.right =
|
||||
!rightHide && right.includes(layoutType) && rightList?.length > rle
|
||||
? w
|
||||
: '0';
|
||||
// monitor.style.bottom = bottomHide ? '0' : bottom
|
||||
}
|
||||
if (baselayer) {
|
||||
baselayer.style.right =
|
||||
!rightHide && right.includes(layoutType) && rightList?.length > rle
|
||||
? `calc(${w} + 60px)`
|
||||
: '60px';
|
||||
baselayer.style.bottom = bottomHide ? '0' : bottom;
|
||||
}
|
||||
};
|
||||
|
||||
export const altitudeToZoom = (height: number) => {
|
||||
const lv =
|
||||
Math.round(D + (A - D) / (1 + Math.pow(Number(height) / C, B))) + 1;
|
||||
return lv > -1 ? lv : 0;
|
||||
};
|
||||
/**
|
||||
* 根据地图级别获取镜头高度
|
||||
* @param {Number} zoom
|
||||
*/
|
||||
export const zoomToAltitude = (zoom: number) => {
|
||||
return Math.round(C * Math.pow((A - D) / (zoom - D) - 1, 1 / B));
|
||||
};
|
||||
|
||||
export const mapOutPut = (imageUrl: string) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const downloadElement = document.createElement('a');
|
||||
const mapElem = document.getElementById('mapContainer');
|
||||
if (mapElem == null) {
|
||||
return;
|
||||
}
|
||||
const context = canvas.getContext('2d')!;
|
||||
canvas.width = mapElem.offsetWidth;
|
||||
canvas.height = mapElem.offsetHeight;
|
||||
const image = new Image();
|
||||
image.src = imageUrl;
|
||||
image.onload = () => {
|
||||
context.drawImage(image, 0, 0);
|
||||
const elem: any = document.getElementById('qgc-legendtl');
|
||||
const l = elem?.style?.left === '' ? 0 : parseInt(elem?.style?.left);
|
||||
if (elem) {
|
||||
domtoimage
|
||||
.toPng(elem, {
|
||||
quality: 1.0,
|
||||
width: elem.offsetWidth + (l + 24),
|
||||
height: mapElem.offsetHeight - 20
|
||||
})
|
||||
.then((legendUrl: string) => {
|
||||
const image = new Image();
|
||||
image.src = legendUrl;
|
||||
image.width = elem.offsetWidth;
|
||||
image.onload = () => {
|
||||
context.drawImage(image, 0, 0);
|
||||
downloadElement.href = canvas.toDataURL('image/png');
|
||||
downloadElement.download = '地图截图';
|
||||
downloadElement.click();
|
||||
};
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
downloadElement.href = canvas.toDataURL('image/png');
|
||||
downloadElement.download = 'download';
|
||||
downloadElement.click();
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -1,297 +0,0 @@
|
||||
import type { layer, MapInterface } from './map.d';
|
||||
// import { MapLeaflet } from "./map.leaflet";
|
||||
import { MapOl } from './map.ol';
|
||||
import { MapCesium } from './map.cesium';
|
||||
|
||||
interface MapClassInterface extends MapInterface {
|
||||
layers: Map<string, layer>;
|
||||
view: any;
|
||||
}
|
||||
|
||||
//描点参数
|
||||
export type MDOptions = {
|
||||
isAllowOverlap?: boolean; // 是否允许重叠
|
||||
labelType?: number; // 标签类型
|
||||
labelminZoom?: number; // 标签最小缩放级别
|
||||
labelAltitude?: object; // 标签海拔
|
||||
isRemove?: boolean; // 是否移除标签
|
||||
};
|
||||
|
||||
export const mapServerBaseUrl =
|
||||
localStorage.getItem('gisurl') || 'http://210.72.227.199:18084/';
|
||||
|
||||
export class MapClass implements MapClassInterface {
|
||||
layers: Map<string, layer>;
|
||||
view: any;
|
||||
private static instance: MapClass;
|
||||
private service: MapInterface;
|
||||
|
||||
constructor() {
|
||||
this.layers = new Map();
|
||||
this.view = null;
|
||||
// this.service = new MapLeaflet();
|
||||
this.service = new MapOl();
|
||||
}
|
||||
static getInstance(): MapClass {
|
||||
if (!this.instance) {
|
||||
this.instance = new MapClass();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
// 地图初始化
|
||||
init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
return this.service.init(container, rectangle).then(map => {
|
||||
this.view = map;
|
||||
return map;
|
||||
});
|
||||
}
|
||||
// 基地面板控制
|
||||
jdPanelControlShowAndHidden(baseid: string, isAll: boolean): void {
|
||||
this.service.jdPanelControlShowAndHidden(baseid, isAll);
|
||||
}
|
||||
|
||||
mdLayerShowOrHidden(
|
||||
layerType: string,
|
||||
key?: string,
|
||||
baseid: string,
|
||||
checked?: boolean,
|
||||
isAll?: boolean
|
||||
): void {
|
||||
this.service.mdLayerShowOrHidden(layerType, key, baseid, checked, isAll);
|
||||
}
|
||||
// 添加基础数据图层
|
||||
addBaseDataLayer(layer: any, isShow: boolean): void {
|
||||
return this.service.addBaseDataLayer(layer, isShow);
|
||||
}
|
||||
// 基础图层显示影隐藏方法
|
||||
controlBaseLayerTreeShowAndHidden(
|
||||
layerType: string,
|
||||
key: string,
|
||||
checked: boolean
|
||||
) {
|
||||
this.service.controlBaseLayerTreeShowAndHidden(layerType, key, checked);
|
||||
}
|
||||
// 图层树控制描点数据显示隐藏方法
|
||||
mdLayerTreeShowOrHidden(layerType: string, checked?: boolean) {
|
||||
this.service.mdLayerTreeShowOrHidden(layerType, checked);
|
||||
}
|
||||
// 根据图例控制锚点显示隐藏
|
||||
setLegendPointVisible(
|
||||
layerKey: string,
|
||||
anchoPointState: string,
|
||||
checked: boolean
|
||||
): void {
|
||||
this.service.setLegendPointVisible(layerKey, anchoPointState, checked);
|
||||
}
|
||||
// 初始化加载描点数据
|
||||
addInitDataLayer = (pointData: any[], layerType: any, mdoptions?: any) => {
|
||||
return this.service.addInitDataLayer(pointData, layerType, mdoptions);
|
||||
};
|
||||
//切换底图
|
||||
baseLayerSwitcher(key: string, checked: boolean): void {
|
||||
this.service.baseLayerSwitcher(key, checked);
|
||||
}
|
||||
|
||||
// 添加梯级流域图
|
||||
addTertiarybasinLayer(
|
||||
layer: layer,
|
||||
fillcolor: any,
|
||||
outlineColor: any,
|
||||
datas: any
|
||||
): void {
|
||||
this.service.addTertiarybasinLayer(layer, fillcolor, outlineColor, datas);
|
||||
}
|
||||
// 移除梯级流域图
|
||||
hideTertiarybasinLayer(layer: layer): void {
|
||||
this.service.hideTertiarybasinLayer?.(layer);
|
||||
}
|
||||
// 缩放
|
||||
zoomToggle(type: 'out' | 'in') {
|
||||
this.service.zoomToggle(type);
|
||||
}
|
||||
/**
|
||||
* 长度量算
|
||||
*/
|
||||
lengthCalculate(): void {
|
||||
this.service.lengthCalculate();
|
||||
}
|
||||
/**
|
||||
* 面积量算
|
||||
*/
|
||||
areCalculate(): void {
|
||||
this.service.areCalculate();
|
||||
}
|
||||
/**
|
||||
* 移除量算结果
|
||||
*/
|
||||
removeQueryLayer(): void {
|
||||
this.service.removeQueryLayer();
|
||||
}
|
||||
// 地图打印
|
||||
mapOutPut() {
|
||||
this.service.mapOutPut();
|
||||
}
|
||||
// 检查图层是否存在
|
||||
hasLayer(layerKey: string): boolean {
|
||||
return this.service.hasLayer(layerKey);
|
||||
}
|
||||
// 检查 GIS/底图图层是否存在
|
||||
hasBaseLayer(layerKey: string): boolean {
|
||||
return this.service.hasBaseLayer(layerKey);
|
||||
}
|
||||
// 删除描点图层
|
||||
removePointLayer(layerKey: string): void {
|
||||
this.service.removePointLayer?.(layerKey);
|
||||
}
|
||||
// 销毁地图
|
||||
destroy(): void {
|
||||
this.service.destroy();
|
||||
}
|
||||
// 初始化弹窗
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void {
|
||||
this.service.initPopupOverlay(popupContainer);
|
||||
}
|
||||
// 切换地图视图
|
||||
switchView(type: '2D' | '3D'): Promise<any> {
|
||||
// this.service.switchView(type);
|
||||
return new Promise((resolve, reject) => {
|
||||
const container = document.getElementById('mapContainer');
|
||||
if (!container) {
|
||||
reject(new Error('Map container not found'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 1. 销毁当前地图服务
|
||||
if (this.service) {
|
||||
try {
|
||||
this.service.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying previous service:', e);
|
||||
}
|
||||
this.service = null;
|
||||
}
|
||||
this.view = null;
|
||||
|
||||
// 2. 根据类型初始化新的地图服务
|
||||
if (type === '2D') {
|
||||
this.service = new MapOl();
|
||||
|
||||
this.service
|
||||
.init(container)
|
||||
.then(map => {
|
||||
this.view = map;
|
||||
resolve(map);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
} else if (type === '3D') {
|
||||
this.service = new MapCesium();
|
||||
|
||||
this.service
|
||||
.init(container)
|
||||
.then(viewer => {
|
||||
this.view = viewer;
|
||||
resolve(viewer);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
}
|
||||
// const container = this.view._container;
|
||||
// if (type === '3D' && this.view) {
|
||||
// if (type === '2D') {
|
||||
// const rectangle = this.view.camera.computeViewRectangle()
|
||||
// const heading = this.view.camera.heading
|
||||
// const height = this.view.camera.positionCartographic.height
|
||||
// const zoom = altitudeToZoom(height) > 11 ? 11 : altitudeToZoom(height)
|
||||
// const center = this.service?.getCenterPosition()
|
||||
// const west = this.transformRadian(rectangle.west)
|
||||
// const north = this.transformRadian(rectangle.north)
|
||||
// const east = this.transformRadian(rectangle.east)
|
||||
// const south = this.transformRadian(rectangle.south)
|
||||
// const bearing = this.transformRadian(heading)
|
||||
|
||||
// this.service = new MapOl();
|
||||
// this.init(document.getElementById('mapContainer'));
|
||||
// // this.service
|
||||
// .init(document.getElementById('mapContainer'))
|
||||
// .then(map => {
|
||||
// this.view = map;
|
||||
// resolve(map);
|
||||
// });
|
||||
// } else if (type === '3D') {
|
||||
// const data: {
|
||||
// _southWest: { lat: number; lng: number }
|
||||
// _northEast: { lat: number; lng: number }
|
||||
// } = this.view.getBounds()
|
||||
// const bearing = this.view.getBearing()
|
||||
// const heading = this.transformAngle(bearing)
|
||||
// //获取相机高度
|
||||
// const altitude = zoomToAltitude(this.view.getZoom())
|
||||
// //获取地图中心点
|
||||
// let lng = 0.5 * (data._ne.lng + data._sw.lng)
|
||||
// let lat = 0.5 * (data._ne.lat + data._sw.lat)
|
||||
// let center = { lng, lat }
|
||||
// this.destroy();
|
||||
|
||||
// this.service = null;
|
||||
// this.view = null;
|
||||
// this.service = new MapCesium()
|
||||
// this.service.init(container, data, center, altitude, heading).then((viewer) => {
|
||||
// this.view = viewer
|
||||
// let removeCallback = viewer.scene.globe.tileLoadProgressEvent.addEventListener((e) => {
|
||||
// if (e == 0) {
|
||||
// removeCallback()
|
||||
// removeCallback = null
|
||||
// if (viewer && !viewer.isDestroyed()) {
|
||||
// resolve(viewer)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// //容错机制,放置地图服务挂掉或者响应非常慢
|
||||
// setTimeout(() => {
|
||||
// if (removeCallback) {
|
||||
// removeCallback()
|
||||
// removeCallback = null
|
||||
// if (viewer && !viewer.isDestroyed()) {
|
||||
// resolve(viewer)
|
||||
// }
|
||||
// }
|
||||
// }, 5000)
|
||||
// })
|
||||
// }
|
||||
} catch {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 飞行到指定的点
|
||||
fitBounds(): void {
|
||||
// this.service.fitBounds(bounds)
|
||||
}
|
||||
|
||||
// 飞行到指定的点
|
||||
flyTopanto(position: number[], zoom: number): void {
|
||||
this.service.flyTopanto(position, zoom);
|
||||
}
|
||||
|
||||
getCurrentZoom(): number | undefined {
|
||||
return this.service.getCurrentZoom();
|
||||
}
|
||||
|
||||
// ==================== 倾斜摄影 ====================
|
||||
|
||||
addQxsyLayer(item: any): void {
|
||||
this.service.addQxsyLayer?.(item);
|
||||
}
|
||||
|
||||
removeQxsyLayer(item: any): void {
|
||||
this.service.removeQxsyLayer?.(item);
|
||||
}
|
||||
|
||||
qxsyChangeClick(item: any, checked: boolean): void {
|
||||
this.service.qxsyChangeClick?.(item, checked);
|
||||
}
|
||||
|
||||
qxsyToPosition(item: any): void {
|
||||
this.service.qxsyToPosition?.(item);
|
||||
}
|
||||
}
|
||||
245
frontend-sjgl/src/components/gis/map.d.ts
vendored
@ -1,245 +0,0 @@
|
||||
import { MDOptions } from './map.class';
|
||||
export type layerType =
|
||||
| 'markers'
|
||||
| 'tiledMap'
|
||||
| 'tiledMapQuery'
|
||||
| 'geoJson'
|
||||
| 'arcgisFeature'
|
||||
| 'dynamicMapLayer'
|
||||
| 'arcgisMap'
|
||||
| 'label'
|
||||
| 'vector';
|
||||
export type layerOption = {
|
||||
opacity?: number;
|
||||
data?: Array<any>;
|
||||
zIndex?: number;
|
||||
clickEvent?: Function;
|
||||
hoverEvent?: Function;
|
||||
legendImages?: Array<any> | null | undefined;
|
||||
geoJsonLegend?: Array<any> | null | undefined;
|
||||
tiledMapType?: undefined | 'superMap';
|
||||
};
|
||||
|
||||
export interface layer {
|
||||
id: string;
|
||||
key: string;
|
||||
_layer?: any;
|
||||
type?: layerType;
|
||||
url?: string;
|
||||
url_3d?: string;
|
||||
geojson_url: string;
|
||||
label?: string;
|
||||
thumbnail?: string;
|
||||
visible?: boolean;
|
||||
option?: layerOption;
|
||||
tempobj?: any;
|
||||
layers?: any;
|
||||
rasteropacity?: any;
|
||||
imgUrl?: any;
|
||||
minZoom?: any;
|
||||
maxZoom?: any;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
/** layer 类型 */
|
||||
layerType?: string;
|
||||
matrixIds_index?: string[];
|
||||
tileMatrixSetID?: string;
|
||||
urlType: string;
|
||||
}
|
||||
|
||||
export interface MapInterface {
|
||||
/**
|
||||
* 地图初始化
|
||||
* @param container DOM容器
|
||||
* @return any 地图视图
|
||||
*/
|
||||
init(
|
||||
container: HTMLElement,
|
||||
rectangle?: any,
|
||||
center?: any,
|
||||
altitude?: number,
|
||||
bearing?: number
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* 初始化加载描点数据
|
||||
* @param pointData
|
||||
* @param layerType
|
||||
*/
|
||||
addInitDataLayer(
|
||||
pointData: any[],
|
||||
layerType: any,
|
||||
mdoptions: MDOptions
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 初始化弹窗
|
||||
* @param popupContainer
|
||||
*/
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void;
|
||||
|
||||
/**
|
||||
* 初始化加载基础图层
|
||||
* @param layer
|
||||
*/
|
||||
addBaseDataLayer(layer: any, isShow: boolean): void;
|
||||
|
||||
/**
|
||||
* 切换底图
|
||||
* @param layer
|
||||
*/
|
||||
baseLayerSwitcher(key: string, checked: boolean): void;
|
||||
|
||||
/**
|
||||
* 切换2D或者3D视图
|
||||
* @param type '2D' | '3D'
|
||||
*/
|
||||
switchView(type: '2D' | '3D'): any | null;
|
||||
|
||||
/**
|
||||
* 缩放
|
||||
* out 缩小
|
||||
* in 放大
|
||||
*/
|
||||
zoomToggle(type: 'out' | 'in'): void;
|
||||
|
||||
/**
|
||||
* 检查图层是否存在
|
||||
* @param layerKey 图层 key
|
||||
* @returns 是否存在
|
||||
*/
|
||||
hasLayer(layerKey: string): boolean;
|
||||
/**
|
||||
* 检查 GIS/底图图层是否存在
|
||||
* @param layerKey 图层 key
|
||||
* @returns 是否存在
|
||||
*/
|
||||
hasBaseLayer(layerKey: string): boolean;
|
||||
/**
|
||||
* 地图输出打印
|
||||
*/
|
||||
mapOutPut(): void;
|
||||
|
||||
//飞行到指定的点
|
||||
fitBounds(bbox, bearing): void;
|
||||
|
||||
flyTopanto(positon, zoom): void;
|
||||
|
||||
/**
|
||||
* 获取当前地图缩放级别语义
|
||||
* 2D 直接返回原生 zoom,3D 由相机高度换算后返回
|
||||
*/
|
||||
getCurrentZoom(): number | undefined;
|
||||
|
||||
/**
|
||||
* 加载倾斜摄影数据
|
||||
*/
|
||||
addQxsyLayer(item: any): void;
|
||||
|
||||
/**
|
||||
* 移除倾斜摄影数据
|
||||
*/
|
||||
removeQxsyLayer(item: any): void;
|
||||
|
||||
/**
|
||||
* 倾斜摄影显隐切换
|
||||
*/
|
||||
qxsyChangeClick(item: any, checked: boolean): void;
|
||||
|
||||
/**
|
||||
* 倾斜摄影定位
|
||||
*/
|
||||
qxsyToPosition(item: any): void;
|
||||
|
||||
/**
|
||||
* 基地面板控制
|
||||
* @param baseid
|
||||
* @param isAll
|
||||
*/
|
||||
jdPanelControlShowAndHidden(baseid: string, isAll: boolean): void;
|
||||
/**
|
||||
* 图例和基地面板控制描点数据显示隐藏方法
|
||||
* @param layerType
|
||||
* @param key
|
||||
* @param checked
|
||||
* @param isAll
|
||||
*/
|
||||
mdLayerShowOrHidden(
|
||||
layerType: string,
|
||||
key?: string,
|
||||
baseid: string,
|
||||
checked?: boolean,
|
||||
isAll?: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 基础图层显示影隐藏方法
|
||||
* @param layerType
|
||||
* @param checked
|
||||
*/
|
||||
controlBaseLayerTreeShowAndHidden(
|
||||
layerType: string,
|
||||
key: string,
|
||||
checked: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 图层树控制描点数据显示隐藏方法
|
||||
* @param layerType
|
||||
* @param checked
|
||||
*/
|
||||
mdLayerTreeShowOrHidden(layerType: string, checked?: boolean): void;
|
||||
|
||||
/**
|
||||
* 删除描点图层
|
||||
* @param layerKey 图层 key
|
||||
*/
|
||||
removePointLayer(layerKey: string): void;
|
||||
|
||||
/**
|
||||
* 根据 anchoPointState 控制单个图例的锚点显示隐藏
|
||||
* @param layerKey 图层 key
|
||||
* @param anchoPointState 锚点状态(对应图例的 nameEn)
|
||||
* @param checked 是否显示
|
||||
*/
|
||||
setLegendPointVisible(
|
||||
layerKey: string,
|
||||
anchoPointState: string,
|
||||
checked: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 长度量算
|
||||
*/
|
||||
lengthCalculate(): void;
|
||||
/**
|
||||
* 面积量算
|
||||
*/
|
||||
areCalculate(): void;
|
||||
|
||||
/**
|
||||
* 移除量算结果
|
||||
*/
|
||||
removeQueryLayer(): void;
|
||||
/**
|
||||
* 添加梯级流域图
|
||||
* @param layer
|
||||
* @param fillcolor
|
||||
*/
|
||||
addTertiarybasinLayer(
|
||||
layer: layer,
|
||||
fillcolor: any,
|
||||
outlineColor: any,
|
||||
datas: any
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 移除梯级流域图
|
||||
* @param layer
|
||||
*/
|
||||
hideTertiarybasinLayer(layer: layer): void;
|
||||
/**
|
||||
* 移除地图对象
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
@ -1,440 +0,0 @@
|
||||
// import { MapInterface } from './map';
|
||||
// import * as L from 'leaflet';
|
||||
// import * as esriLeaflet from 'esri-leaflet';
|
||||
// import '@/utils/leaflet/leaflet-tilelayer-wmts-src.js';
|
||||
// import { mapServerBaseUrl } from './map.class';
|
||||
// import '@/utils/leaflet/leaflet.inflatable-markers-group.js';
|
||||
// import {MDOptions} from './map.class';
|
||||
// import { getIconPath } from "@/utils/index";
|
||||
// // @ts-ignore
|
||||
// import axios from 'axios';
|
||||
// // import "@/components/thematicMap/leaflet/leaflet.inflatable-markers-group.js"
|
||||
|
||||
// const tiledMapGroup = L.layerGroup();
|
||||
// const chartMapGroup = L.layerGroup();
|
||||
// const overlayGroup = L.layerGroup();
|
||||
// const CENTER_positionCN: any = [38, 114.17112499999996]; //中心纬经度 中国
|
||||
// const basinCenter = {
|
||||
// DA_HHGLSX: [103.343357, 35.931812],
|
||||
// FA_CJGLSX: [111.001911, 30.821327]
|
||||
// };
|
||||
// let boundCavansLayer: any = null;
|
||||
// let ganliulist: any = [];
|
||||
// export class MapLeaflet implements MapInterface {
|
||||
// map: any = null;
|
||||
// htmlMakerLayer: any = [];
|
||||
// defaultScale = 10;
|
||||
// minimumZoom = 7;
|
||||
// setDrawPlug: any = null;
|
||||
// layermarkers: any = [];
|
||||
// rainlayerslist: any = [];
|
||||
// private layerRegistry: Map<string, any> = new Map(); // ✅ 新增:存储 key -> layer 实例
|
||||
|
||||
// private currentBaseLayerKey: string | null = null; // ✅ 新增:记录当前激活的底图 Key
|
||||
|
||||
// temperatureMapObj: any = [];
|
||||
// //地图初始化
|
||||
// init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
// try {
|
||||
// console.log('init初始化container', container);
|
||||
// var corner1 = L.latLng(55.35715491537772, 140.7821677051657);
|
||||
// var corner2 = L.latLng(0.975580441812298, 67.56008229483018);
|
||||
// var bounds = L.latLngBounds(corner2, corner1);
|
||||
// const map = L.map(container as any, {
|
||||
// preferCanvas: true,
|
||||
// zoom: 4.5,
|
||||
// minZoom: 4.23,
|
||||
// maxZoom: 22, // 【修改点1】增大最大缩放级别,允许用户继续滚轮放大
|
||||
// maxNativeZoom: 12,
|
||||
// zoomSnap: 0.1, // 【修改点2】让缩放更平滑,不强制对齐整数
|
||||
// zoomDelta: 0.5,
|
||||
// center: CENTER_positionCN,
|
||||
// attributionControl: false,
|
||||
// zoomControl: false,
|
||||
// trackResize: true,
|
||||
// maxBounds: bounds,
|
||||
// maxBoundsViscosity: 1.0,
|
||||
// wheelPxPerZoomLevel: 180,
|
||||
// });
|
||||
// // ✅ 新增:可视化显示 maxBounds 范围
|
||||
// // L.rectangle(bounds, {color: "#ff7800", weight: 3, opacity: 0.8, fillOpacity: 0.1})
|
||||
// // .addTo(map)
|
||||
// // .bindPopup("这是 maxBounds 的范围");
|
||||
// this.map = map;
|
||||
// this.map.on('zoomend', (e: any) => {
|
||||
// console.log('当前缩放级别', e.target.getZoom());
|
||||
// });
|
||||
// return Promise.resolve(map);
|
||||
// } catch (e) {
|
||||
// console.log('测试', e);
|
||||
// return Promise.reject({});
|
||||
// }
|
||||
// }
|
||||
// addBaseDataLayer(layer: any): any {
|
||||
// // The WMTS URL
|
||||
// console.log(layer);
|
||||
// switch (layer.type) {
|
||||
// case 'wmts':
|
||||
// if (layer.url) {
|
||||
// console.log('https://211.99.26.225:18085' + layer.url);
|
||||
// if (layer) {
|
||||
// var ignLayer = L.tileLayer
|
||||
// .wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
||||
// tileSize: 256, //切片大小
|
||||
// maxZoom: 13,
|
||||
// noWrap: true,
|
||||
// opacity: 0.99,
|
||||
// minZoom: 4,
|
||||
// styles:{
|
||||
// abc:123
|
||||
// },
|
||||
// layer: 'qgc_qsj_arcgistiles_l13'
|
||||
// })
|
||||
// .addTo(this.map)
|
||||
// .bringToBack();
|
||||
// const registryKey = layer.key || layer.title;
|
||||
// if (registryKey) {
|
||||
// this.layerRegistry.set(registryKey, ignLayer);
|
||||
// }
|
||||
// layer._layer = ignLayer;
|
||||
// layer._layer.layerGroup = overlayGroup;
|
||||
// }
|
||||
// }
|
||||
// return layer;
|
||||
|
||||
// case 'markers':
|
||||
// this.getRain(layer);
|
||||
// break;
|
||||
// default: {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// baseLayerSwitcher(key: string) {
|
||||
// if (!this.map) return;
|
||||
|
||||
// console.log('切换底图 key:', key);
|
||||
|
||||
// // 1. 如果当前已有底图且不是同一个,先移除
|
||||
// if (this.currentBaseLayerKey && this.currentBaseLayerKey !== key) {
|
||||
// const oldLayer = this.layerRegistry.get(this.currentBaseLayerKey);
|
||||
// if (oldLayer && this.map.hasLayer(oldLayer)) {
|
||||
// this.map.removeLayer(oldLayer);
|
||||
// console.log(`已移除旧底图: ${this.currentBaseLayerKey}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 2. 检查新底图是否已存在 registry 中
|
||||
// let newLayer = this.layerRegistry.get(key);
|
||||
|
||||
// // 3. 如果不存在,则创建并添加到地图和 registry
|
||||
// if (!newLayer) {
|
||||
// newLayer = this.createBaseLayerByKey(key);
|
||||
// if (newLayer) {
|
||||
// this.layerRegistry.set(key, newLayer);
|
||||
// newLayer.addTo(this.map);
|
||||
// // 确保底图在最底层
|
||||
// newLayer.bringToBack();
|
||||
// }
|
||||
// } else {
|
||||
// // 4. 如果已存在,直接添加(如果未添加)
|
||||
// if (!this.map.hasLayer(newLayer)) {
|
||||
// newLayer.addTo(this.map);
|
||||
// newLayer.bringToBack();
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 5. 更新当前激活的 Key
|
||||
// if (newLayer) {
|
||||
// this.currentBaseLayerKey = key;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 根据 Key 创建具体的 Leaflet 图层实例
|
||||
// */
|
||||
// createBaseLayerByKey(key: string): L.Layer | null {
|
||||
// console.log(key)
|
||||
// const tdtToken = 'e90d56e5a09d1767899ad45846b0cefd' //企业版密钥,勿换e650f138c4481cca888cd13094bb9026
|
||||
// const mapType = 'w' //c:天地图经纬度底图;w:天地图墨卡托底图
|
||||
// const URL_TerTDT = `https://t0.tianditu.gov.cn/ter_${mapType}/wmts?tk=${tdtToken}` //terMap
|
||||
// switch (key) {
|
||||
// case 's_province_boundaries':
|
||||
// // 假设这是之前的 WMTS 矢量图层
|
||||
// // 注意:原代码中硬编码了 URL 和 layer 名称,这里需要确保与实际服务对应
|
||||
// return L.tileLayer.wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
||||
// tileSize: 256,
|
||||
// // maxZoom: 13,
|
||||
// noWrap: true,
|
||||
// opacity: 1,
|
||||
// minZoom: 4,
|
||||
// zIndex: 1,
|
||||
// layer: 'qgc_qsj_arcgistiles_l13', // 请确认此 layer 名称是否对应“矢量”
|
||||
// format: 'image/png'
|
||||
// });
|
||||
|
||||
// case 'BASEMAP-white':
|
||||
// // 地形图示例 (使用 OpenStreetMap 或其他地形服务作为占位,请替换为实际地址)
|
||||
// return L.tileLayer(`${URL_TerTDT}&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=${mapType}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles`, {
|
||||
// // maxZoom: 17,
|
||||
// format: "image/png",
|
||||
// zIndex: 12,
|
||||
// attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
|
||||
// });
|
||||
|
||||
// case 'BASEMAP-img':
|
||||
// // 影像图示例 (使用 Esri World Imagery 作为占位,请替换为实际地址)
|
||||
// return L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
// attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
// });
|
||||
|
||||
// default:
|
||||
// console.warn(`未知的底图 Key: ${key}`);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// controlBaseLayerTreeShowAndHidden(layerType: String, key: String, checked: boolean) {
|
||||
// // 优先使用 key 查找,如果没有 key 则尝试用 layerType
|
||||
// // console.log(this.getAllLayers())
|
||||
// console.log(key)
|
||||
// console.log(this.layerRegistry)
|
||||
// const registryKey:any = key || layerType;
|
||||
// const layerInstance = this.layerRegistry.get(registryKey);
|
||||
// if (layerInstance) {
|
||||
// if (checked) {
|
||||
// // 显示:如果不在地图上,则添加
|
||||
// if (!this.map.hasLayer(layerInstance)) {
|
||||
// layerInstance.addTo(this.map);
|
||||
// }
|
||||
// } else {
|
||||
// // 隐藏:如果在地图上,则移除
|
||||
// if (this.map.hasLayer(layerInstance)) {
|
||||
// this.map.removeLayer(layerInstance);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// console.warn(`未找到标识为 ${registryKey} 的图层实例`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 添加初始化数据图层
|
||||
// * @param pointLayerObj 图层配置对象 (包含 data, key, checked 等属性)
|
||||
// * @param layerType 图层类型/Key (如果 pointLayerObj 中有 key,则优先使用 obj.key)
|
||||
// * @param mdoptions 描点选项配置
|
||||
// * @param legendArray 图例映射数据 (用于根据 anchoPointState 匹配图标)
|
||||
// */
|
||||
// addInitDataLayer = (pointLayerObj: any, layerType?: any, mdoptions?: MDOptions, legendArray?: any) => {
|
||||
// // 1. 参数校验与数据提取
|
||||
// if (!this.map || !pointLayerObj) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let dataArray: any[] = [];
|
||||
// let targetLayerKey: string = layerType;
|
||||
|
||||
// if (Array.isArray(pointLayerObj)) {
|
||||
// dataArray = pointLayerObj;
|
||||
// } else {
|
||||
// dataArray = pointLayerObj.data || [];
|
||||
// targetLayerKey = pointLayerObj.key || layerType;
|
||||
// }
|
||||
|
||||
// if (!targetLayerKey) {
|
||||
// console.warn('缺少图层 Key,无法加载描点');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (dataArray.length === 0) {
|
||||
// const existingGroup = this.layerRegistry.get(targetLayerKey);
|
||||
// if (existingGroup && existingGroup instanceof L.LayerGroup) {
|
||||
// existingGroup.clearLayers();
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.log(`开始加载图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}`);
|
||||
|
||||
// // 2. 获取或创建该图层的 LayerGroup
|
||||
// let layerGroup = this.layerRegistry.get(targetLayerKey);
|
||||
|
||||
// const shouldClear = mdoptions?.isRemove !== false;
|
||||
|
||||
// if (!layerGroup || !(layerGroup instanceof L.LayerGroup)) {
|
||||
// if (layerGroup && this.map.hasLayer(layerGroup)) {
|
||||
// this.map.removeLayer(layerGroup);
|
||||
// }
|
||||
// layerGroup = L.layerGroup();
|
||||
// this.layerRegistry.set(targetLayerKey, layerGroup);
|
||||
// layerGroup.addTo(this.map);
|
||||
// } else {
|
||||
// if (shouldClear) {
|
||||
// layerGroup.clearLayers();
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 3. 遍历数据生成 Marker
|
||||
// dataArray.forEach((item: any) => {
|
||||
// const { lgtd, lttd, stcd, stnm, iconCode, anchoPointState, _id, titleName, ennm } = item;
|
||||
|
||||
// if (lgtd == null || lttd == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 4. 确定图标样式和文字
|
||||
// let iconUrl = '';
|
||||
// let iconSize = [15, 15];
|
||||
|
||||
// // 获取图标 URL
|
||||
// if (iconCode) {
|
||||
// iconUrl = getIconPath(iconCode);
|
||||
// } else if (anchoPointState && legendArray) {
|
||||
// const legendItem = legendArray[anchoPointState];
|
||||
// if (legendItem && legendItem.icon) {
|
||||
// iconUrl = getIconPath(legendItem.icon);
|
||||
// if (legendItem.width && legendItem.height) {
|
||||
// iconSize = [legendItem.width, legendItem.height];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 如果没有找到图标,使用默认或跳过
|
||||
// if (!iconUrl) {
|
||||
// // 可以选择使用默认图标或者继续
|
||||
// iconUrl = getIconPath('default'); // 假设有一个默认图标
|
||||
// if(!iconUrl) return;
|
||||
// }
|
||||
|
||||
// // 5. 创建带文字的 DivIcon
|
||||
// // 显示的文字优先使用 titleName,其次 ennm,最后 stnm
|
||||
// const labelText = titleName || ennm || stnm || '';
|
||||
|
||||
// // 构建 HTML 结构:一个容器包含图片和文字
|
||||
// // 注意:这里使用内联样式简单演示,建议在实际项目中提取到 CSS 类中
|
||||
// const iconHtml = `
|
||||
// <div style="position: relative; display: flex; flex-direction: column; align-items: center; width: max-content;">
|
||||
// <div style="
|
||||
// font: 10px sans-serif;
|
||||
// font-weight: bold;
|
||||
// color: #fff;
|
||||
// white-space: nowrap;
|
||||
// margin-bottom: 2px;
|
||||
// -webkit-text-stroke: 2px #2e2d2d;
|
||||
// paint-order: stroke fill; /* 确保描边在填充外部,防止文字变细 */
|
||||
|
||||
// ">
|
||||
// ${labelText}
|
||||
// </div>
|
||||
// <img src="${iconUrl}" style="width: ${iconSize[0]}px; height: ${iconSize[1]}px; display: block;" />
|
||||
// </div>`;
|
||||
|
||||
// const customIcon = L.divIcon({
|
||||
// html: iconHtml,
|
||||
// className: '', // 重要:设置为空字符串以避免 Leaflet 默认样式干扰
|
||||
// iconSize: [iconSize[0], iconSize[1] + 20], // 高度增加以容纳文字 (假设文字高约20px)
|
||||
// iconAnchor: [iconSize[0] / 2, iconSize[1] + 10], // 锚点设在图片底部中心
|
||||
// popupAnchor: [0, -(iconSize[1] + 20)] // 弹窗锚点设在整体顶部
|
||||
// });
|
||||
|
||||
// // 6. 创建 Marker
|
||||
// const marker = L.marker([lttd, lgtd], {
|
||||
// icon: customIcon,
|
||||
// // @ts-ignore
|
||||
// options: {
|
||||
// ...item,
|
||||
// layerKey: targetLayerKey,
|
||||
// originalEvent: item
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 7. 绑定点击事件 (如果需要)
|
||||
// marker.on('click', (e: any) => {
|
||||
// // 触发全局事件,例如:
|
||||
// // GlobalEvents.get('map_dataLayer_click').set(item);
|
||||
// console.log('Marker clicked:', item);
|
||||
// });
|
||||
|
||||
// // 8. 将 Marker 添加到 LayerGroup
|
||||
// layerGroup.addLayer(marker);
|
||||
// });
|
||||
|
||||
// console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
||||
// }
|
||||
// getRain(data: any) {
|
||||
// if (data?.geojson) {
|
||||
// this.removeRainLayer();
|
||||
// this.removeLayermarkers();
|
||||
// const renderColor = (item: any, colRules: any) => {
|
||||
// let color = 'rgba(0,0,0,0)';
|
||||
// colRules.map((rule: any, index: number) => {
|
||||
// if (
|
||||
// item.properties.hvalue >= rule?.sv &&
|
||||
// item.properties.hvalue < rule?.ev
|
||||
// ) {
|
||||
// color = rule.colors;
|
||||
// }
|
||||
// if (index === colRules.length - 1) {
|
||||
// if (item.properties.hvalue >= rule?.ev) {
|
||||
// color = rule.colors;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// return color;
|
||||
// };
|
||||
|
||||
// data.geojson.features.map((item: any, index: number) => {
|
||||
// let pushlist = L.geoJSON(item, {
|
||||
// style: {
|
||||
// color: renderColor(item, data.colRules),
|
||||
// weight: 2,
|
||||
// opacity: 0.8,
|
||||
// fillOpacity: 0.7
|
||||
// // fillColor: "#1D91C0",
|
||||
// }
|
||||
// }).addTo(this.map);
|
||||
// this.layermarkers.push(pushlist);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// removeLayermarkers = () => {
|
||||
// // console.log('layermarkers',layermarkers);
|
||||
|
||||
// try {
|
||||
// if (this.layermarkers.length > 0) {
|
||||
// if (this.map.hasLayer(this.layermarkers)) {
|
||||
// this.map.removeLayer(this.layermarkers);
|
||||
// } else {
|
||||
// console.log('移除图层失败,应已经移除');
|
||||
// }
|
||||
// }
|
||||
// // removeAllGeojson(map);
|
||||
// } catch (e) {
|
||||
// // message.info('移除图层失败')
|
||||
// console.log('移除图层失败,是否应移除');
|
||||
// }
|
||||
// };
|
||||
|
||||
// removeRainLayer(): void {
|
||||
// console.log('test!删除Layer');
|
||||
// const _this = this;
|
||||
// if (this.rainlayerslist.length > 0) {
|
||||
// this.rainlayerslist.map((item: any) => {
|
||||
// _this.map.removeLayer(item);
|
||||
// });
|
||||
// this.rainlayerslist = [];
|
||||
// }
|
||||
// } // 缩放
|
||||
// zoomToggle(type: 'out' | 'in') {
|
||||
// if (this.map) {
|
||||
// if (type === 'out') {
|
||||
// this.map && this.map.zoomOut();
|
||||
// } else {
|
||||
// this.map && this.map.zoomIn();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -1,759 +0,0 @@
|
||||
/**
|
||||
* @author: yjw
|
||||
* @date: 2022-11-17
|
||||
* @description: 后期建议本文件内容做到系统配置中
|
||||
*/
|
||||
|
||||
// import { MemoryCache } from '@zebras/qgc-share/cache/MemoryCache'
|
||||
// import { Session } from '@zebras/qgc-share/service/Session'
|
||||
|
||||
export interface MapBaseUrlItem {
|
||||
code: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ServerConfigItem {
|
||||
id?: string;
|
||||
key?: string;
|
||||
urlType: 'gisurl' | 'online';
|
||||
url?: string;
|
||||
url_3d?: string;
|
||||
layers?: string;
|
||||
rasteropacity?: number;
|
||||
_layer?: string;
|
||||
imgUrl?: string;
|
||||
visible?: boolean;
|
||||
visibility?: boolean; //?是否可以删除
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
fillOpacity?: number;
|
||||
lineWidth?: number;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
const tdtToken = 'e90d56e5a09d1767899ad45846b0cefd'; //企业版密钥,勿换e650f138c4481cca888cd13094bb9026
|
||||
const mapType = 'w'; //c:天地图经纬度底图;w:天地图墨卡托底图
|
||||
const URL_TerTDT = `https://t0.tianditu.gov.cn/ter_${mapType}/wmts?tk=${tdtToken}`; //terMap
|
||||
// const URL_TerLabelTDT = `http://t0.tianditu.gov.cn/cta_${mapType}/wmts?tk=${tdtToken}` //terLabel
|
||||
// const URL_VecTDT = `http://t0.tianditu.com/vec_${mapType}/wmts?tk=${tdtToken}` //vecMap
|
||||
// const URL_VecLableTDT = `http://t0.tianditu.com/cva_${mapType}/wmts?tk=${tdtToken}` //vecLabel
|
||||
// const URL_ImgTDT = `http://t0.tianditu.com/img_${mapType}/wmts?tk=${tdtToken}` //imgMap
|
||||
// const URL_ImgLableTDT = `http://t0.tianditu.com/cia_${mapType}/wmts?tk=${tdtToken}`
|
||||
|
||||
// const znyMapUrl = "http://10.219.26.6:8050/" //中南院的测试地图环境
|
||||
// const djjtMapUrl = "http://210.72.227.199:18084/" //中南院的测试地图环境
|
||||
//TODO:以后改成在系统管理里面配置地图可视化范围
|
||||
export const location_map_maxlon = 126.8267;
|
||||
export const location_map_maxlat = 38.5769;
|
||||
export const location_map_minlon = 73.7443;
|
||||
export const location_map_minlat = 16.32;
|
||||
// todo: 以后从接口获取数据
|
||||
export const servers: Record<string, ServerConfigItem> | Record<string, any> = {
|
||||
// 省(自治区)界
|
||||
province_boundaries: {
|
||||
id: 'province_boundaries',
|
||||
key: 'province_boundaries',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:provinceline&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:provinceline&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:provinceline',
|
||||
rasteropacity: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 21,
|
||||
type: 'wms',
|
||||
layerType: 'line',
|
||||
lineWidth: 2,
|
||||
visible: true
|
||||
},
|
||||
//国界
|
||||
chinaBoundary: {
|
||||
id: 'chinaBoundary',
|
||||
key: 'chinaBoundary',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Achina@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:china&maxFeatures=50&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:china',
|
||||
_layer: 'china',
|
||||
layerType: 'line',
|
||||
minZoom: 0,
|
||||
maxZoom: 16,
|
||||
minHeight: 0,
|
||||
maxHeight: 80000000,
|
||||
rasteropacity: 1,
|
||||
type: 'vector_Boundary',
|
||||
paint: {
|
||||
'line-color': '#AAA4C5',
|
||||
'line-width': 3,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//九段线
|
||||
nineSegmentLine: {
|
||||
id: 'nineSegmentLine',
|
||||
key: 'nineSegmentLine',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Azh_island1@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:zh_island1&maxFeatures=50&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
layers: 'qgc:zh_island1',
|
||||
_layer: 'zh_island1',
|
||||
layerType: 'line',
|
||||
minZoom: 0,
|
||||
maxZoom: 16,
|
||||
minHeight: 0,
|
||||
maxHeight: 80000000,
|
||||
rasteropacity: 1,
|
||||
type: 'vector_Boundary',
|
||||
paint: {
|
||||
'line-color': '#AAA4C5',
|
||||
'line-width': 3,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
// 基础图层
|
||||
BaseLayer: {
|
||||
'BASEMAP-img': {
|
||||
key: 'BASEMAP-img',
|
||||
type: 'raster-dem',
|
||||
urlType: 'online',
|
||||
url:
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' +
|
||||
`&token=`,
|
||||
url_3d:
|
||||
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' +
|
||||
`&token=`,
|
||||
_layer: 'img',
|
||||
visible: false,
|
||||
maxzoom: 20
|
||||
},
|
||||
'BASEMAP-white': {
|
||||
key: 'BASEMAP-white',
|
||||
urlType: 'online',
|
||||
type: 'raster-dem',
|
||||
url: `${URL_TerTDT}&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=${mapType}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles&token=
|
||||
`,
|
||||
url_3d: URL_TerTDT + `&token=`,
|
||||
_layer: 'ter',
|
||||
visible: false,
|
||||
maxzoom: 14
|
||||
}
|
||||
},
|
||||
// 区|县
|
||||
county: {
|
||||
id: 'county',
|
||||
key: 'county',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Acounty@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:county&outputFormat=application%2Fjson&token=`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:county',
|
||||
_layer: 'county',
|
||||
rasteropacity: 0.5,
|
||||
imgUrl: '/zfile/qgc/icons/map-xzXian.png',
|
||||
minZoom: 8,
|
||||
maxZoom: 16,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'vector',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
//县注记
|
||||
county_lable: {
|
||||
id: 'county_lable',
|
||||
key: 'county_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:countyAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:countyAno&outputFormat=application%2Fjso&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:countyAno',
|
||||
_layer: 'countyAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 8,
|
||||
maxZoom: 16,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
|
||||
// 市
|
||||
city: {
|
||||
id: 'city',
|
||||
key: 'city',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Acity@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:city&outputFormat=application%2Fjson&token=`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:city',
|
||||
_layer: 'city',
|
||||
rasteropacity: 0.5,
|
||||
imgUrl: '/zfile/qgc/icons/map-xzShi.png',
|
||||
minZoom: 5,
|
||||
maxZoom: 8,
|
||||
minHeight: 800000,
|
||||
maxHeight: 1200000,
|
||||
type: 'vector',
|
||||
layerType: 'symbol',
|
||||
visible: false
|
||||
},
|
||||
//市注记
|
||||
city_lable: {
|
||||
id: 'city_lable',
|
||||
key: 'city_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:cityAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:cityAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:cityAno',
|
||||
_layer: 'cityAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 5,
|
||||
maxZoom: 8,
|
||||
minHeight: 800000,
|
||||
maxHeight: 1200000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
// 省
|
||||
provincial_capital: {
|
||||
id: 'provincial_capital',
|
||||
key: 'provincial_capital',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Aprovincecity@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:provincecity&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:provincecity',
|
||||
_layer: 'provincecity',
|
||||
rasteropacity: 0.5,
|
||||
imgUrl: '/zfile/qgc/icons/map-xzSheng.png',
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 5,
|
||||
minHeight: 1200000,
|
||||
maxHeight: 99999999999,
|
||||
type: 'vector',
|
||||
layerType: 'symbol'
|
||||
},
|
||||
//省注记
|
||||
province_lable: {
|
||||
id: 'province_lable',
|
||||
key: 'province_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:provincecityAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:provincecityAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:provincecityAno',
|
||||
_layer: 'provinceAno',
|
||||
rasteropacity: 0.5,
|
||||
minZoom: 0,
|
||||
maxZoom: 5,
|
||||
minHeight: 1200000,
|
||||
maxHeight: 99999999999,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
baseImageLayerUrl: {
|
||||
id: 'BaseLayer',
|
||||
key: 'BaseLayer',
|
||||
urlType: 'online',
|
||||
url: 'http://t{s}.tianditu.gov.cn/img_w/wmts?tk=88735d6d9bbd5930f1b1c8b8df72e762&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}'
|
||||
},
|
||||
WhiteBaseLayerUrl: {
|
||||
id: 'BaseLayer',
|
||||
key: 'BaseLayer',
|
||||
urlType: 'online',
|
||||
url: `https://t0.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=e6372a5333c4bac9b9ef6097453c3cd6&token=
|
||||
`
|
||||
},
|
||||
|
||||
/**
|
||||
* 电站基地
|
||||
*/
|
||||
hydropBase: {
|
||||
id: 'hydropBase',
|
||||
key: 'hydropBase',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3AstationEra1117@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:stationEra1117&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
_layer: 'stationEra1117',
|
||||
layers: 'qgc:stationEra1117',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
type: 'vector',
|
||||
layerType: 'fill',
|
||||
paint: {
|
||||
'fill-color': '#f0dcda',
|
||||
'fill-opacity': 0.8,
|
||||
'fill-outline-color': '#dd7065'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 电站基地配置
|
||||
*/
|
||||
powerBaseStationLayerWFSUrl: {
|
||||
id: 's_hydropBase_wfs',
|
||||
key: 's_hydropBase_wfs',
|
||||
urlType: 'gisurl',
|
||||
url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:stationEra1117&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
_layer: 'stationEra1117',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:stationEra1117',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* 电站基地配置 LM项目
|
||||
*/
|
||||
powerBaseStationLayerWFSUrlOfLM: {
|
||||
id: 's_hydropBase_wfs',
|
||||
key: 's_hydropBase_wfs',
|
||||
urlType: 'gisurl',
|
||||
url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:stationEraLM&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
_layer: 'stationEraLM',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:stationEraLM',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
},
|
||||
/**
|
||||
* 河流连通性分析中河流水系配置
|
||||
*/
|
||||
YLJ_LCJ_LayerWFSUrl: {
|
||||
id: 'river_riverEvaTemp',
|
||||
key: 'river_riverEvaTemp',
|
||||
urlType: 'gisurl',
|
||||
url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:ylj_lcj&maxFeatures=50&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
_layer: 'ylj_lcj',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:ylj_lcj',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* 水质水温渐变线配置
|
||||
*/
|
||||
LCJ_WaterQuality_line: {
|
||||
id: 'LCJ_WaterQuality_line',
|
||||
key: 'LCJ_WaterQuality_line',
|
||||
urlType: 'gisurl',
|
||||
url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:lcj_waterquality_line&maxFeatures=50&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
_layer: 'lcj_waterquality_line',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:lcj_waterquality_line',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* 全国乡镇名称
|
||||
*/
|
||||
NationalTownships: {
|
||||
id: 'NationalTownships',
|
||||
key: 'NationalTownships',
|
||||
layerType: 'symbol',
|
||||
isShowLabel: true,
|
||||
urlType: 'gisurl',
|
||||
url: `/geoserver/gwc/service/tms/1.0.0/qgc%3ANationalTownships@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?token=`,
|
||||
_layer: 'NationalTownships',
|
||||
layers: 'qgc:NationalTownships',
|
||||
visible: true,
|
||||
minZoom: 10.5,
|
||||
maxZoom: 18,
|
||||
isOnlyAno: true,
|
||||
type: 'vector',
|
||||
layout_text: {
|
||||
'text-field': ['get', 'NAME'],
|
||||
'text-font': ['Open Sans Bold'],
|
||||
'text-size': 16,
|
||||
'text-anchor': 'top',
|
||||
'text-padding': 0,
|
||||
'text-max-width': 100,
|
||||
'text-ignore-placement': false,
|
||||
'text-allow-overlap': false,
|
||||
visibility: 'visible'
|
||||
},
|
||||
paint_text: {
|
||||
'text-color': '#404040',
|
||||
'text-halo-color': '#fff',
|
||||
'text-halo-width': 2
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 一级流域
|
||||
*/
|
||||
heliu1: {
|
||||
id: 'heliu1',
|
||||
key: 'heliu1',
|
||||
urlType: 'gisurl',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river1_line&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Ariver1_line@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
_layer: 'river1_line',
|
||||
layers: 'qgc:river1_line',
|
||||
rasteropacity: 0.5,
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 99999999999,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
paint: {
|
||||
'line-color': '#00A1E9',
|
||||
'line-width': 4,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//一级河流注记
|
||||
heliu1_lable: {
|
||||
id: 'heliu1_lable',
|
||||
key: 'heliu1_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:river1_lineAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river1_lineAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:river1_lineAno',
|
||||
_layer: 'river1_lineAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 99999999999,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 二级流域
|
||||
*/
|
||||
heliu2: {
|
||||
id: 'heliu2',
|
||||
key: 'heliu2',
|
||||
urlType: 'gisurl',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river2_line&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Ariver2_line@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
_layer: 'river2_line',
|
||||
layers: 'qgc:river2_line',
|
||||
lineWidth: 2,
|
||||
rasteropacity: 0.5,
|
||||
visible: false,
|
||||
minZoom: 4,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 1200000,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
paint: {
|
||||
'line-color': '#00A1E9',
|
||||
'line-width': 3,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//二级河流注记
|
||||
heliu2_lable: {
|
||||
id: 'heliu2_lable',
|
||||
key: 'heliu2_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:river2_lineAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river2_lineAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:river2_lineAno',
|
||||
_layer: 'river2_lineAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 4,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 1200000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
/**
|
||||
* 三级流域
|
||||
*/
|
||||
heliu3: {
|
||||
id: 'heliu3',
|
||||
key: 'heliu3',
|
||||
urlType: 'gisurl',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river3_line&maxFeatures=50&outputFormat=application/json&token=
|
||||
`,
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Ariver3_line@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
_layer: 'river3_line',
|
||||
layers: 'qgc:river3_line',
|
||||
lineWidth: 1,
|
||||
rasteropacity: 0.5,
|
||||
visible: false,
|
||||
minZoom: 6,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
paint: {
|
||||
'line-color': '#00A1E9',
|
||||
'line-width': 2,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//三级河流注记
|
||||
heliu3_lable: {
|
||||
id: 'heliu3_lable',
|
||||
key: 'heliu3_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:river3_lineAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river3_lineAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:river3_lineAno',
|
||||
_layer: 'river3_lineAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 6,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
/**
|
||||
* 四级流域
|
||||
*/
|
||||
heliu4: {
|
||||
id: 'heliu4',
|
||||
key: 'heliu4',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Ariver4_line@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river4_line&maxFeatures=8000&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
_layer: 'river4_line',
|
||||
layers: 'qgc:river4_line',
|
||||
visible: false,
|
||||
minZoom: 8,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
rasteropacity: 1,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
paint: {
|
||||
'line-color': '#00A1E9',
|
||||
'line-width': 1,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//四级河流注记
|
||||
heliu4_lable: {
|
||||
id: 'heliu4_lable',
|
||||
key: 'heliu4_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:river4_lineAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river4_lineAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:river4_lineAno',
|
||||
_layer: 'river4_lineAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 8,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
|
||||
visible: true
|
||||
},
|
||||
/**
|
||||
* 五级流域 - 水质等级图层
|
||||
*/
|
||||
heliu5: {
|
||||
id: 'heliu5',
|
||||
key: 'heliu5',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Ariver5_line@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river5_line&maxFeatures=8000&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
_layer: 'river5_line',
|
||||
layers: 'qgc:river5_line',
|
||||
visible: false,
|
||||
minZoom: 8,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
rasteropacity: 1,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
paint: {
|
||||
'line-color': '#00A1E9',
|
||||
'line-width': 1,
|
||||
'line-opacity': 0.9
|
||||
}
|
||||
},
|
||||
//五级河流注记
|
||||
heliu5_lable: {
|
||||
id: 'heliu5_lable',
|
||||
key: 'heliu5_lable',
|
||||
urlType: 'gisurl',
|
||||
// url: this.gisServers.gisurl + '/geoserver/qgc/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=qgc%3Aprovinceline&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A3857&WIDTH=768&HEIGHT=620&bbox={bbox-epsg-3857}',
|
||||
url: '/geoserver/qgc/wms?service=WMS&version=1.1.0&request=GetMap&layers=qgc:river5_lineAno&styles=&bbox={bbox-epsg-3857}&width=768&height=620&srs=EPSG:3857&format=image%2Fpng&TRANSPARENT=true',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:river5_lineAno&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
layers: 'qgc:river5_lineAno',
|
||||
_layer: 'river5_lineAno',
|
||||
rasteropacity: 1,
|
||||
minZoom: 8,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
type: 'wms',
|
||||
layerType: 'symbol',
|
||||
visible: true
|
||||
},
|
||||
//流域梯级图
|
||||
Tertiarybasin: {
|
||||
id: 'Tertiarybasin',
|
||||
key: 'Tertiarybasin',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Anew_LiuYuThrLev@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:new_LiuYuThrLev&outputFormat=application%2Fjson&token=
|
||||
`,
|
||||
_layer: 'new_LiuYuThrLev',
|
||||
layers: 'qgc:new_LiuYuThrLev',
|
||||
visible: true,
|
||||
minZoom: 1,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 800000,
|
||||
rasteropacity: 0.8,
|
||||
type: 'vector',
|
||||
layerType: 'fill'
|
||||
},
|
||||
// //鱼类栖息地
|
||||
// fishQxd: {
|
||||
// id: 'fishQxd',
|
||||
// key: 'fishQxd',
|
||||
// urlType: 'gisurl',
|
||||
// url: '/geoserver/gwc/service/tms/1.0.0/qgc%3Afish_qxd@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
// url_3d: '/geoserver/qgc/wms',
|
||||
// geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:fish_qxd&outputFormat=application%2Fjson&token=
|
||||
// `,
|
||||
// _layer: 'fish_qxd',
|
||||
// layers: 'qgc:fish_qxd',
|
||||
// visible: true,
|
||||
// minZoom: 0,
|
||||
// maxZoom: 18,
|
||||
// minHeight: 0,
|
||||
// maxHeight: 80000000,
|
||||
// rasteropacity: 0.9,
|
||||
// type: 'vector',
|
||||
// layerType: 'line',
|
||||
// lineWidth: 2,
|
||||
// paint: {
|
||||
// 'line-color': '#A21C83',
|
||||
// 'line-width': 4,
|
||||
// 'line-opacity': 1
|
||||
// }
|
||||
// },
|
||||
/**
|
||||
* 水功能分区
|
||||
*/
|
||||
WaterFunctionPartition: {
|
||||
id: 'WaterFunctionPartition',
|
||||
key: 'WaterFunctionPartition',
|
||||
urlType: 'gisurl',
|
||||
url: '/geoserver/gwc/service/tms/1.0.0/qgc%3AGNQ@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf',
|
||||
url_3d: '/geoserver/qgc/wms',
|
||||
geojson_url: `/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:GNQ&outputFormat=application%2Fjson&token=`,
|
||||
_layer: 'GNQ',
|
||||
layers: 'qgc:GNQ',
|
||||
visible: true,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
minHeight: 0,
|
||||
maxHeight: 8000000,
|
||||
rasteropacity: 0.9,
|
||||
type: 'vector',
|
||||
layerType: 'line',
|
||||
lineWidth: 2,
|
||||
paint: {
|
||||
'line-color': '#A21C83',
|
||||
'line-width': 3,
|
||||
'line-opacity': 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取Gis图层配置
|
||||
* @param key gis服务key
|
||||
* @returns
|
||||
*/
|
||||
// export const getConfig = (key: string) => {
|
||||
// const mapBaseUrls: Record<string, MapBaseUrlItem> = MemoryCache.get('mapBaseUrls') || {}
|
||||
// if (Object.prototype.hasOwnProperty.call(servers, key)) {
|
||||
// const r = { ...servers[key] }
|
||||
// const { urlType } = r
|
||||
// const baseUrlObj = mapBaseUrls[urlType as string]
|
||||
// if (baseUrlObj) {
|
||||
// r.url = baseUrlObj.url + r.url
|
||||
// r.url_3d = baseUrlObj.url + r.url_3d
|
||||
// if (r.geojson_url) {
|
||||
// r.geojson_url = baseUrlObj.url + r.geojson_url
|
||||
// }
|
||||
// }
|
||||
// return r
|
||||
// }
|
||||
// console.warn(`The gis config which key is ${key} not exist!`)
|
||||
// return null
|
||||
// }
|
||||
@ -1,391 +0,0 @@
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point';
|
||||
import OlMap from 'ol/Map';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { getIconPath } from '@/utils/index';
|
||||
type PointLayerStyleFactory = (feature: Feature) => any;
|
||||
|
||||
type PointLayerManagerOptions = {
|
||||
map: OlMap | null;
|
||||
createStyle: PointLayerStyleFactory;
|
||||
};
|
||||
|
||||
export class PointLayerManager {
|
||||
private map: OlMap | null;
|
||||
private createStyle: PointLayerStyleFactory;
|
||||
private layerRegistry: Map<string, VectorLayer<VectorSource>> = new Map();
|
||||
private layerFeatureIndexes: Map<
|
||||
string,
|
||||
Map<string, Map<string, Feature[]>>
|
||||
> = new Map();
|
||||
|
||||
constructor(options: PointLayerManagerOptions) {
|
||||
this.map = options.map;
|
||||
this.createStyle = options.createStyle;
|
||||
}
|
||||
|
||||
// 备注:同步更新地图实例,供地图初始化后把真实 map 注入点图层管理器。
|
||||
setMap(map: OlMap | null) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
// 备注:获取当前屏幕可视区域内的所有点要素(仅包含图层可见且图例可见的要素)。
|
||||
getFeaturesInViewport(): Feature[] {
|
||||
if (!this.map) return [];
|
||||
|
||||
const extent = this.map.getView().calculateExtent(this.map.getSize());
|
||||
if (!extent) return [];
|
||||
|
||||
const [minX, minY, maxX, maxY] = extent;
|
||||
const result: Feature[] = [];
|
||||
|
||||
this.forEachFeature((feature, layer) => {
|
||||
// 跳过图层本身不可见的要素
|
||||
if (!layer.getVisible()) return;
|
||||
|
||||
const geom = feature.getGeometry();
|
||||
if (!geom || geom.getType() !== 'Point') return;
|
||||
|
||||
const coords = (geom as Point).getCoordinates();
|
||||
const [x, y] = coords;
|
||||
|
||||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||
result.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 备注:统一返回当前点图层注册表,供外层做遍历或清理。
|
||||
getRegistry() {
|
||||
return this.layerRegistry;
|
||||
}
|
||||
|
||||
// 备注:按图层 key 获取已注册的点图层实例。
|
||||
getLayer(layerKey: string) {
|
||||
return this.layerRegistry.get(layerKey);
|
||||
}
|
||||
|
||||
// 备注:返回当前所有点图层实例,供命中检测和批量刷新使用。
|
||||
getLayers(): VectorLayer<VectorSource>[] {
|
||||
return Array.from(this.layerRegistry.values());
|
||||
}
|
||||
|
||||
// 备注:按当前缩放级别统一刷新近邻点展开坐标,保证抽吸后的点击和 popup 与视觉位置一致。
|
||||
updateNearbyFeatureLayout(currentZoom?: number): void {
|
||||
if (!this.map) return;
|
||||
|
||||
if (currentZoom === undefined || !Number.isFinite(currentZoom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.forEachFeature(feature => {
|
||||
this.resetFeatureToBaseCoordinate(feature);
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:判断指定图层 key 的点图层是否已存在。
|
||||
hasLayer(layerKey: string): boolean {
|
||||
return this.layerRegistry.has(layerKey);
|
||||
}
|
||||
|
||||
// 备注:按需获取或创建点图层实例,并自动完成注册与挂载。
|
||||
ensureLayer(layerKey: string, layerType?: string) {
|
||||
if (!this.map || !layerKey) return null;
|
||||
|
||||
let vectorLayer = this.layerRegistry.get(layerKey);
|
||||
if (vectorLayer) {
|
||||
return vectorLayer;
|
||||
}
|
||||
|
||||
const vectorSource = new VectorSource();
|
||||
vectorLayer = new VectorLayer({
|
||||
source: vectorSource,
|
||||
zIndex: 100,
|
||||
declutter: true,
|
||||
style: feature => this.createStyle(feature as Feature)
|
||||
});
|
||||
(vectorLayer as any).type = layerType || layerKey;
|
||||
this.layerRegistry.set(layerKey, vectorLayer);
|
||||
this.map.addLayer(vectorLayer);
|
||||
return vectorLayer;
|
||||
}
|
||||
|
||||
// 备注:把后端点位数组转换为 OpenLayers Feature,并写入指定点图层。
|
||||
addDataLayer(pointData: any, layerType: string): void {
|
||||
if (!this.map) return;
|
||||
|
||||
let dataArray: any[] = [];
|
||||
let targetLayerKey = layerType;
|
||||
|
||||
if (Array.isArray(pointData)) {
|
||||
dataArray = pointData;
|
||||
} else {
|
||||
dataArray = pointData?.data || [];
|
||||
targetLayerKey = pointData?.key || layerType;
|
||||
}
|
||||
|
||||
if (!targetLayerKey) {
|
||||
console.warn('缺少图层 Key,无法加载描点');
|
||||
return;
|
||||
}
|
||||
|
||||
const vectorLayer = this.ensureLayer(targetLayerKey, layerType);
|
||||
if (!vectorLayer) return;
|
||||
|
||||
const vectorSource = vectorLayer.getSource();
|
||||
if (!vectorSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
vectorSource.clear();
|
||||
this.resetLayerFeatureIndexes(targetLayerKey);
|
||||
|
||||
if (dataArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const features: Feature[] = [];
|
||||
|
||||
dataArray.forEach((item: any) => {
|
||||
const feature = this.createFeature(item, targetLayerKey);
|
||||
if (feature) {
|
||||
this.indexFeature(targetLayerKey, feature);
|
||||
features.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
vectorSource.addFeatures(features);
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:统一控制整个点图层显隐,供图层树勾选联动使用。
|
||||
setLayerVisible(layerKey: string, visible?: boolean): void {
|
||||
const vectorLayer = this.layerRegistry.get(layerKey);
|
||||
if (vectorLayer && vectorLayer.getVisible() !== visible) {
|
||||
vectorLayer.setVisible(visible);
|
||||
vectorLayer.changed();
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:按图层 key 删除点图层并清空其 source 数据。
|
||||
removeLayer(layerKey: string): void {
|
||||
if (!this.map || !layerKey) {
|
||||
console.warn('removePointLayer: 无效的图层 key 或地图未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
const vectorLayer = this.layerRegistry.get(layerKey);
|
||||
if (!vectorLayer) {
|
||||
console.warn(`未找到标识为 [${layerKey}] 的描点图层实例`);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = vectorLayer.getSource();
|
||||
if (source) {
|
||||
source.clear();
|
||||
}
|
||||
this.map.removeLayer(vectorLayer);
|
||||
this.layerRegistry.delete(layerKey);
|
||||
this.layerFeatureIndexes.delete(layerKey);
|
||||
}
|
||||
|
||||
// 备注:按图层遍历所有点图层实例,供 hover 刷新和区域裁切复用。
|
||||
forEachLayer(
|
||||
callback: (layer: VectorLayer<VectorSource>, layerKey: string) => void
|
||||
): void {
|
||||
this.layerRegistry.forEach((layer, layerKey) => {
|
||||
callback(layer, layerKey);
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:遍历所有点要素,供区域裁切或批量状态更新使用。
|
||||
forEachFeature(
|
||||
callback: (
|
||||
feature: Feature,
|
||||
layer: VectorLayer<VectorSource>,
|
||||
layerKey: string
|
||||
) => void
|
||||
): void {
|
||||
this.forEachLayer((layer, layerKey) => {
|
||||
const source = layer.getSource();
|
||||
if (!source) return;
|
||||
source.getFeatures().forEach(feature => {
|
||||
callback(feature as Feature, layer, layerKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:按图例字段值统一控制要素显隐,供图例和容量筛选联动复用。
|
||||
setLegendVisibleByField(
|
||||
layerKey: string,
|
||||
matchValue: string,
|
||||
checked: boolean,
|
||||
fieldKey = 'anchoPointState'
|
||||
): void {
|
||||
const vectorLayer = this.layerRegistry.get(layerKey);
|
||||
if (!vectorLayer) return;
|
||||
|
||||
const source = vectorLayer.getSource();
|
||||
if (!source) return;
|
||||
|
||||
if (!matchValue) {
|
||||
source.getFeatures().forEach(feature => {
|
||||
if (feature.get('_legendVisible') !== false) {
|
||||
feature.set('_legendVisible', false);
|
||||
feature.changed();
|
||||
}
|
||||
});
|
||||
vectorLayer.changed();
|
||||
return;
|
||||
}
|
||||
|
||||
const targetFeatures = this.getIndexedFeatures(
|
||||
layerKey,
|
||||
fieldKey,
|
||||
matchValue
|
||||
);
|
||||
if (targetFeatures.length === 0) return;
|
||||
|
||||
targetFeatures.forEach(feature => {
|
||||
if (feature.get('_legendVisible') !== checked) {
|
||||
feature.set('_legendVisible', checked);
|
||||
feature.changed();
|
||||
}
|
||||
});
|
||||
vectorLayer.changed();
|
||||
}
|
||||
|
||||
// 备注:统一销毁所有点图层并清空注册表,供地图销毁阶段复用。
|
||||
destroy(): void {
|
||||
this.forEachLayer(layer => {
|
||||
const source = layer.getSource();
|
||||
if (source) {
|
||||
source.clear();
|
||||
}
|
||||
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
if ((layer as any).setMap) {
|
||||
(layer as any).setMap(null);
|
||||
}
|
||||
});
|
||||
this.layerRegistry.clear();
|
||||
this.layerFeatureIndexes.clear();
|
||||
}
|
||||
|
||||
// 备注:统一把业务点位数据转换为 Feature,并补齐样式依赖字段。
|
||||
private createFeature(item: any, targetLayerKey: string): Feature | null {
|
||||
const { lgtd, lttd, stnm, iconCode, titleName, ennm } = item;
|
||||
|
||||
if (lgtd == null || lttd == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lon = Number(lgtd);
|
||||
const lat = Number(lttd);
|
||||
if (isNaN(lon) || isNaN(lat) || !isFinite(lon) || !isFinite(lat)) {
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(lon) > 180 || Math.abs(lat) > 90) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const coord = fromLonLat([lon, lat]);
|
||||
if (!isFinite(coord[0]) || !isFinite(coord[1])) {
|
||||
return null;
|
||||
}
|
||||
let iconUrl = iconCode ? getIconPath(iconCode) : '';
|
||||
if (!iconUrl) {
|
||||
iconUrl = getIconPath('default') || '';
|
||||
}
|
||||
|
||||
const feature = new Feature({
|
||||
geometry: new Point(coord),
|
||||
...item,
|
||||
_layerKey: targetLayerKey
|
||||
});
|
||||
const rawFeatureId =
|
||||
item.stcd || item._id || `${lon.toFixed(6)},${lat.toFixed(6)}`;
|
||||
feature.setId(`${targetLayerKey}:${rawFeatureId}`);
|
||||
feature.set('_iconUrl', iconUrl);
|
||||
feature.set(
|
||||
'_labelText',
|
||||
item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || ''
|
||||
);
|
||||
feature.set('_baseCoordinates', coord);
|
||||
feature.set('_legendVisible', true);
|
||||
feature.set('_regionVisible', true);
|
||||
feature.set('_sttpMap', item.sttpMap);
|
||||
if (item.popupHtml) {
|
||||
feature.set('popupHtml', item.popupHtml);
|
||||
}
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
// 备注:重置单个图层的字段索引,供重新灌入数据或删除图层时复用。
|
||||
private resetLayerFeatureIndexes(layerKey: string): void {
|
||||
this.layerFeatureIndexes.set(layerKey, new Map());
|
||||
}
|
||||
|
||||
// 备注:按字段值给要素建立索引,供图例切换时快速命中目标锚点集合。
|
||||
private indexFeature(layerKey: string, feature: Feature): void {
|
||||
const fieldIndexMap =
|
||||
this.layerFeatureIndexes.get(layerKey) ||
|
||||
new Map<string, Map<string, Feature[]>>();
|
||||
|
||||
const legendFieldKey = 'anchoPointState';
|
||||
const legendFieldValue = feature.get(legendFieldKey);
|
||||
if (legendFieldValue != null) {
|
||||
const valueMap =
|
||||
fieldIndexMap.get(legendFieldKey) || new Map<string, Feature[]>();
|
||||
const normalizedValue = String(legendFieldValue);
|
||||
const targetFeatures = valueMap.get(normalizedValue) || [];
|
||||
targetFeatures.push(feature);
|
||||
valueMap.set(normalizedValue, targetFeatures);
|
||||
fieldIndexMap.set(legendFieldKey, valueMap);
|
||||
}
|
||||
|
||||
this.layerFeatureIndexes.set(layerKey, fieldIndexMap);
|
||||
}
|
||||
|
||||
// 备注:根据图层 key、字段名和值快速返回目标要素集合,避免图例切换时整层遍历。
|
||||
private getIndexedFeatures(
|
||||
layerKey: string,
|
||||
fieldKey: string,
|
||||
matchValue: string
|
||||
): Feature[] {
|
||||
const fieldIndexMap = this.layerFeatureIndexes.get(layerKey);
|
||||
if (!fieldIndexMap) return [];
|
||||
|
||||
const valueMap = fieldIndexMap.get(fieldKey);
|
||||
if (!valueMap) return [];
|
||||
return valueMap.get(String(matchValue)) || [];
|
||||
}
|
||||
|
||||
// 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。
|
||||
private resetFeatureToBaseCoordinate(feature: Feature): void {
|
||||
const baseCoordinates = feature.get('_baseCoordinates') as
|
||||
| number[]
|
||||
| undefined;
|
||||
const geometry = feature.getGeometry();
|
||||
if (
|
||||
!baseCoordinates ||
|
||||
baseCoordinates.length < 2 ||
|
||||
!geometry ||
|
||||
geometry.getType() !== 'Point'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
(geometry as Point).setCoordinates([
|
||||
baseCoordinates[0],
|
||||
baseCoordinates[1]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,530 +0,0 @@
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point';
|
||||
import OlMap from 'ol/Map';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import {
|
||||
generatePopupHtml,
|
||||
shouldPreferEng2Popup
|
||||
} from '@/utils/popupHtmlGenerator';
|
||||
|
||||
type PopupManagerOptions = {
|
||||
map: OlMap | null;
|
||||
getPointLayers: () => VectorLayer<VectorSource>[];
|
||||
};
|
||||
|
||||
type PopupHitResult = {
|
||||
detectedFeature: Feature | undefined;
|
||||
isHitIcon: boolean;
|
||||
coordinate?: number[];
|
||||
};
|
||||
|
||||
type PointerMoveChangePayload = {
|
||||
hoveredId: string | number | null;
|
||||
detectedFeature: Feature | undefined;
|
||||
coordinate?: number[];
|
||||
};
|
||||
|
||||
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<VectorSource>[];
|
||||
private popupMouseEnterHandler: (() => void) | null = null;
|
||||
private popupMouseMoveHandler: (() => void) | null = null;
|
||||
private hoverChangeHandler:
|
||||
| ((payload: PointerMoveChangePayload) => void)
|
||||
| undefined;
|
||||
|
||||
constructor(options: PopupManagerOptions) {
|
||||
this.map = options.map;
|
||||
this.getPointLayers = options.getPointLayers;
|
||||
}
|
||||
|
||||
// 备注:同步地图实例,供地图初始化和销毁后更新 Popup 管理器上下文。
|
||||
setMap(map: OlMap | null) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
// 备注:初始化 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();
|
||||
};
|
||||
this.popupMouseMoveHandler = () => {
|
||||
this.forceHidePopup();
|
||||
};
|
||||
this.popupElement.addEventListener(
|
||||
'mouseenter',
|
||||
this.popupMouseEnterHandler
|
||||
);
|
||||
this.popupElement.addEventListener('mousemove', this.popupMouseMoveHandler);
|
||||
this.popupOverlay = new Overlay({
|
||||
element: this.popupElement,
|
||||
positioning: 'bottom-center',
|
||||
offset: [0, -10],
|
||||
stopEvent: false,
|
||||
autoPan: false
|
||||
});
|
||||
|
||||
if (this.map) {
|
||||
this.map.addOverlay(this.popupOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:统一处理地图点击命中检测,命中图标时通过回调把要素抛给外层。
|
||||
handleMapClick(
|
||||
pixel: number[],
|
||||
onHit?: (feature: Feature, coordinate?: number[]) => void
|
||||
) {
|
||||
const { detectedFeature, isHitIcon, coordinate } =
|
||||
this.detectFeatureAtPixel(pixel);
|
||||
if (detectedFeature && isHitIcon) {
|
||||
onHit?.(detectedFeature, coordinate);
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:统一处理鼠标悬停命中检测与节流,只有 hover 目标变化时才通知外层刷新。
|
||||
handlePointerMove(
|
||||
pixel: number[],
|
||||
onHoverChange?: (payload: PointerMoveChangePayload) => void
|
||||
) {
|
||||
if (!this.map) return;
|
||||
this.hoverChangeHandler = onHoverChange;
|
||||
|
||||
const zoom = this.map.getView().getZoom();
|
||||
if (zoom !== undefined && zoom < 4.7) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.animationFrameId) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(() => {
|
||||
const { detectedFeature, isHitIcon, coordinate } =
|
||||
this.detectFeatureAtPixel(pixel);
|
||||
const hoveredId =
|
||||
detectedFeature && isHitIcon ? (detectedFeature.getId() as any) : null;
|
||||
const nextFeature = hoveredId ? detectedFeature : undefined;
|
||||
const nextCoordinate = hoveredId ? coordinate : undefined;
|
||||
|
||||
if (hoveredId !== this.lastHoveredId) {
|
||||
this.emitHoverChange({
|
||||
hoveredId,
|
||||
detectedFeature: nextFeature,
|
||||
coordinate: nextCoordinate
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:在指定像素位置检测点图层要素,并判断是否命中图标区域。
|
||||
detectFeatureAtPixel(pixel: number[]): PopupHitResult {
|
||||
let detectedFeature: Feature | undefined = undefined;
|
||||
let isHitIcon = false;
|
||||
let coordinate: number[] | undefined;
|
||||
const pointLayers = this.getPointLayers();
|
||||
|
||||
if (this.isPixelInsidePopup(pixel)) {
|
||||
return {
|
||||
detectedFeature: undefined,
|
||||
isHitIcon: false,
|
||||
coordinate: undefined
|
||||
};
|
||||
}
|
||||
|
||||
this.map?.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer) => {
|
||||
if (pointLayers.includes(layer as VectorLayer<VectorSource>)) {
|
||||
detectedFeature = feature as Feature;
|
||||
const geom = feature.getGeometry();
|
||||
if (geom && geom.getType() === 'Point') {
|
||||
coordinate = (geom as Point).getCoordinates();
|
||||
const iconPixel = this.map?.getPixelFromCoordinate(coordinate);
|
||||
if (iconPixel) {
|
||||
const dx = pixel[0] - iconPixel[0];
|
||||
const dy = pixel[1] - iconPixel[1];
|
||||
if (this.isPixelInsideIconArea(dx, dy)) {
|
||||
isHitIcon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ hitTolerance: 0 }
|
||||
);
|
||||
|
||||
return { detectedFeature, isHitIcon, coordinate };
|
||||
}
|
||||
|
||||
// 备注:仅把图标本体区域作为 hover 命中范围,避免上方文字标签被误判为图标命中。
|
||||
private isPixelInsideIconArea(dx: number, dy: number): boolean {
|
||||
if (!this.map) return false;
|
||||
|
||||
const zoom = this.map.getView().getZoom() ?? 4.5;
|
||||
let dynamicScale = 0.7 + (zoom - 4.5) * 0.08;
|
||||
dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale));
|
||||
|
||||
const halfWidth = 12 * dynamicScale;
|
||||
const topReach = 8 * dynamicScale;
|
||||
const bottomReach = 12 * dynamicScale;
|
||||
|
||||
return (
|
||||
dx >= -halfWidth &&
|
||||
dx <= halfWidth &&
|
||||
dy >= -topReach &&
|
||||
dy <= bottomReach
|
||||
);
|
||||
}
|
||||
|
||||
// 备注:根据当前悬停要素统一显示或隐藏 Popup 内容。
|
||||
showPopup(feature: Feature | undefined, coordinate: number[] | undefined) {
|
||||
if (!this.popupOverlay || !this.popupElement) return;
|
||||
|
||||
if (feature && coordinate) {
|
||||
const props = feature.getProperties();
|
||||
const popupHtml = this.getPopupHtml(props);
|
||||
|
||||
if (popupHtml) {
|
||||
this.popupElement.innerHTML = popupHtml;
|
||||
this.popupOverlay.setPosition(coordinate);
|
||||
this.popupElement.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.popupOverlay.setPosition(undefined);
|
||||
this.popupElement.style.display = 'none';
|
||||
}
|
||||
|
||||
private getPopupHtml(props: Record<string, any>) {
|
||||
return props.popupHtml || generatePopupHtml(props);
|
||||
}
|
||||
|
||||
// 备注:批量显示多个要素的 Popup,使用绝对定位的 div 而非 Overlay。
|
||||
showPopupsForFeatures(
|
||||
features: Feature[],
|
||||
styleChecker?: (feature: Feature) => boolean,
|
||||
options?: {
|
||||
rebuild?: boolean;
|
||||
}
|
||||
) {
|
||||
if (!this.map || !this.popupElement) return;
|
||||
const shouldRebuild = options?.rebuild !== false;
|
||||
|
||||
// 过滤:只显示图例可见的要素
|
||||
const visibleFeatures = features.filter(
|
||||
f => f.get('_legendVisible') !== false
|
||||
);
|
||||
|
||||
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');
|
||||
batchContainer.className = 'batch-popup-container';
|
||||
batchContainer.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
`;
|
||||
batchContainer.id = 'batch-popup-container';
|
||||
this.batchPopupContainer = batchContainer;
|
||||
|
||||
// 先添加容器到 DOM,以便后续元素能正确测量尺寸
|
||||
mapElement.appendChild(batchContainer);
|
||||
|
||||
// 用于碰撞检测的已放置 popup 列表
|
||||
const placedPopups: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}> = [];
|
||||
visibleFeatures.forEach(feature => {
|
||||
const geom = feature.getGeometry();
|
||||
if (!geom || geom.getType() !== 'Point') return;
|
||||
|
||||
const coords = (geom as Point).getCoordinates();
|
||||
const pixel = this.map?.getPixelFromCoordinate(coords);
|
||||
if (!pixel) return;
|
||||
|
||||
// 检查样式是否隐藏(declutter、距离过滤等)
|
||||
if (styleChecker && !styleChecker(feature)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const props = feature.getProperties();
|
||||
const popupHtml = shouldPreferEng2Popup(props)
|
||||
? generatePopupHtml(props, { forceEng2: true }) ||
|
||||
props.popupHtml ||
|
||||
generatePopupHtml(props)
|
||||
: this.getPopupHtml(props);
|
||||
if (!popupHtml) return;
|
||||
|
||||
// 创建与原始 popupElement 完全相同的样式
|
||||
const popupEl = document.createElement('div');
|
||||
popupEl.className = this.popupElement.className;
|
||||
popupEl.innerHTML = popupHtml;
|
||||
// 不设置自定义 cssText,只设置定位(对应原始 popup 的 positioning: 'bottom-center', offset: [0, -10])
|
||||
popupEl.style.position = 'absolute';
|
||||
popupEl.style.display = 'block';
|
||||
popupEl.style.transform = 'translate(-50%, -100%)'; // bottom-center 对齐
|
||||
popupEl.style.pointerEvents = 'none';
|
||||
|
||||
// 临时设置 visibility: hidden 来测量尺寸
|
||||
popupEl.style.visibility = 'hidden';
|
||||
popupEl.style.left = `${pixel[0]}px`;
|
||||
popupEl.style.top = `${pixel[1] - 10}px`;
|
||||
batchContainer.appendChild(popupEl);
|
||||
|
||||
// 获取 popup 尺寸
|
||||
const rect = popupEl.getBoundingClientRect();
|
||||
const popupWidth = rect.width;
|
||||
const popupHeight = rect.height;
|
||||
|
||||
// 计算 popup 的位置(对应原始 positioning: 'bottom-center', offset: [0, -10])
|
||||
const popupX = pixel[0];
|
||||
const popupY = pixel[1] - 10; // bottom-center 对齐 + 10px 偏移
|
||||
const popupRect = {
|
||||
left: popupX - popupWidth / 2,
|
||||
right: popupX + popupWidth / 2,
|
||||
top: popupY - popupHeight,
|
||||
bottom: popupY
|
||||
};
|
||||
|
||||
// 碰撞检测
|
||||
let hasCollision = false;
|
||||
for (const placed of placedPopups) {
|
||||
if (this.checkCollision(popupRect, placed)) {
|
||||
hasCollision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCollision) {
|
||||
// 无碰撞,显示 popup
|
||||
popupEl.style.visibility = 'visible';
|
||||
popupEl.style.left = `${popupX}px`;
|
||||
popupEl.style.top = `${popupY}px`;
|
||||
this.batchPopupItems.push({
|
||||
feature,
|
||||
element: popupEl
|
||||
});
|
||||
|
||||
placedPopups.push({
|
||||
left: popupRect.left,
|
||||
right: popupRect.right,
|
||||
top: popupRect.top,
|
||||
bottom: popupRect.bottom
|
||||
});
|
||||
} else {
|
||||
// 有碰撞,移除该 popup
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:检测两个 popup 矩形是否碰撞。
|
||||
private checkCollision(
|
||||
rect1: { left: number; right: number; top: number; bottom: number },
|
||||
rect2: { left: number; right: number; top: number; bottom: number }
|
||||
): boolean {
|
||||
const padding = 4;
|
||||
return !(
|
||||
rect1.right + padding < rect2.left ||
|
||||
rect1.left - padding > rect2.right ||
|
||||
rect1.bottom + padding < rect2.top ||
|
||||
rect1.top - padding > rect2.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// 备注:统一重置 hover 和 Popup 状态,供地图销毁和切换时复用。
|
||||
reset() {
|
||||
if (this.animationFrameId) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
this.emitHoverChange({
|
||||
hoveredId: null,
|
||||
detectedFeature: undefined,
|
||||
coordinate: undefined
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:在弹窗层意外接管鼠标时,通过统一 hover 回调链路立即清空当前悬停状态。
|
||||
private forceHidePopup() {
|
||||
this.emitHoverChange({
|
||||
hoveredId: null,
|
||||
detectedFeature: undefined,
|
||||
coordinate: undefined
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:统一派发 hover 变化,确保地图侧的 cursor、Popup 和图层刷新走同一条处理链路。
|
||||
private emitHoverChange(payload: PointerMoveChangePayload) {
|
||||
this.lastHoveredId = payload.hoveredId;
|
||||
this.hoverChangeHandler?.(payload);
|
||||
if (!this.hoverChangeHandler) {
|
||||
this.showPopup(payload.detectedFeature, payload.coordinate);
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:当鼠标进入当前 Popup 可见区域时,禁止再把该区域重新识别为图标 hover。
|
||||
private isPixelInsidePopup(pixel: number[]): boolean {
|
||||
if (!this.map || !this.popupElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.popupElement.style.display === 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const popupRect = this.popupElement.getBoundingClientRect();
|
||||
if (popupRect.width <= 0 || popupRect.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mapRect = this.map.getTargetElement().getBoundingClientRect();
|
||||
const clientX = mapRect.left + pixel[0];
|
||||
const clientY = mapRect.top + pixel[1];
|
||||
|
||||
return (
|
||||
clientX >= popupRect.left &&
|
||||
clientX <= popupRect.right &&
|
||||
clientY >= popupRect.top &&
|
||||
clientY <= popupRect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// 备注:销毁 Popup 管理器内部状态和 Overlay 引用。
|
||||
destroy() {
|
||||
this.clearBatchPopups();
|
||||
this.reset();
|
||||
if (this.popupElement && this.popupMouseEnterHandler) {
|
||||
this.popupElement.removeEventListener(
|
||||
'mouseenter',
|
||||
this.popupMouseEnterHandler
|
||||
);
|
||||
}
|
||||
if (this.popupElement && this.popupMouseMoveHandler) {
|
||||
this.popupElement.removeEventListener(
|
||||
'mousemove',
|
||||
this.popupMouseMoveHandler
|
||||
);
|
||||
}
|
||||
if (this.map && this.popupOverlay) {
|
||||
this.map.removeOverlay(this.popupOverlay);
|
||||
}
|
||||
this.popupMouseEnterHandler = null;
|
||||
this.popupMouseMoveHandler = null;
|
||||
this.hoverChangeHandler = undefined;
|
||||
this.popupOverlay = null;
|
||||
this.popupElement = null;
|
||||
this.map = null;
|
||||
}
|
||||
}
|
||||
@ -1,341 +0,0 @@
|
||||
import OlMap from 'ol/Map';
|
||||
import View from 'ol/View';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { PointLayerManager } from './point-layer-manager';
|
||||
|
||||
type RegionMaskManagerOptions = {
|
||||
map: OlMap | null;
|
||||
view: View | null;
|
||||
pointLayerManager: PointLayerManager;
|
||||
defaultCenter: [number, number];
|
||||
defaultZoom: number;
|
||||
};
|
||||
|
||||
export class RegionMaskManager {
|
||||
private map: OlMap | null;
|
||||
private view: View | null;
|
||||
private pointLayerManager: PointLayerManager;
|
||||
private defaultCenter: [number, number];
|
||||
private defaultZoom: number;
|
||||
private maskBindings: Array<{
|
||||
layer: TileLayer;
|
||||
prerender: (event: any) => void;
|
||||
postrender: (event: any) => void;
|
||||
}> = [];
|
||||
|
||||
constructor(options: RegionMaskManagerOptions) {
|
||||
this.map = options.map;
|
||||
this.view = options.view;
|
||||
this.pointLayerManager = options.pointLayerManager;
|
||||
this.defaultCenter = options.defaultCenter;
|
||||
this.defaultZoom = options.defaultZoom;
|
||||
}
|
||||
|
||||
// 备注:同步地图和视图上下文,供地图初始化、销毁和重建后复用区域过滤能力。
|
||||
setContext(map: OlMap | null, view: View | null) {
|
||||
this.map = map;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
// 备注:统一隐藏所有点位的区域显示状态,供基地裁切前的预处理复用。
|
||||
hideAllPoints(): void {
|
||||
this.pointLayerManager.forEachFeature(feature => {
|
||||
feature.set('_regionVisible', false);
|
||||
feature.changed();
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:统一恢复所有点位的区域显示状态,供取消裁切或异常兜底时复用。
|
||||
showAllPoints(): void {
|
||||
this.pointLayerManager.forEachFeature(feature => {
|
||||
feature.set('_regionVisible', true);
|
||||
feature.changed();
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:根据 GeoJSON 边界批量更新点位的区域显隐状态。
|
||||
filterPointsByRegion(geoJson: any): void {
|
||||
const regionCoords = this.extractPolygonCoords(geoJson);
|
||||
if (!regionCoords.length) {
|
||||
console.warn('无法解析区域边界,显示所有锚点');
|
||||
this.showAllPoints();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pointLayerManager.forEachFeature(feature => {
|
||||
const props = feature.getProperties();
|
||||
const lon = Number(props.lgtd);
|
||||
const lat = Number(props.lttd);
|
||||
|
||||
if (!isFinite(lon) || !isFinite(lat)) {
|
||||
feature.set('_regionVisible', false);
|
||||
feature.changed();
|
||||
return;
|
||||
}
|
||||
|
||||
feature.set('_regionVisible', this.isPointInPolygon(lon, lat, regionCoords));
|
||||
feature.changed();
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:根据 GeoJSON 范围拟合地图视野,供基地切换后快速聚焦到目标区域。
|
||||
fitViewToGeoJson(geoJson: any): void {
|
||||
if (!this.map || !this.view) return;
|
||||
|
||||
try {
|
||||
const features = new GeoJSON().readFeatures(geoJson, {
|
||||
dataProjection: 'EPSG:4326',
|
||||
featureProjection: 'EPSG:3857'
|
||||
});
|
||||
|
||||
if (!features.length) return;
|
||||
|
||||
const source = new VectorSource({
|
||||
features
|
||||
});
|
||||
const extent = source.getExtent();
|
||||
|
||||
if (extent[0] === Infinity || extent[0] === -Infinity) {
|
||||
console.warn('无法计算有效的地图包围盒');
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.fit(extent, {
|
||||
padding: [100, 200, 50, 50],
|
||||
duration: 1000,
|
||||
maxZoom: 13
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('调整地图视野失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:把 GeoJSON 边界绑定到底图 prerender/postrender 事件,实现区域裁切遮罩。
|
||||
applyMapMask(rasterLayer: TileLayer, clipGeoJson: any): void {
|
||||
this.applyMapMaskToLayers(rasterLayer ? [rasterLayer] : [], clipGeoJson);
|
||||
}
|
||||
|
||||
// 备注:把 GeoJSON 边界同时绑定到多个底图图层,保证多底图叠加场景下裁切边界保持一致。
|
||||
applyMapMaskToLayers(rasterLayers: TileLayer[] = [], clipGeoJson: any): void {
|
||||
if (!clipGeoJson) return;
|
||||
|
||||
const targetLayers = rasterLayers.filter(Boolean);
|
||||
if (targetLayers.length === 0) return;
|
||||
|
||||
const features = new GeoJSON().readFeatures(clipGeoJson, {
|
||||
dataProjection: 'EPSG:4326',
|
||||
featureProjection: 'EPSG:3857'
|
||||
});
|
||||
|
||||
if (!features.length) {
|
||||
console.warn('裁切数据为空,无法应用遮罩');
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearMaskEvents();
|
||||
|
||||
targetLayers.forEach(rasterLayer => {
|
||||
const maskPrerender = (event: any) => {
|
||||
const context = event.context;
|
||||
const frameState = event.frameState;
|
||||
|
||||
if (!context || !frameState || !this.map) return;
|
||||
|
||||
context.save();
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.beginPath();
|
||||
|
||||
let hasValidPath = false;
|
||||
|
||||
for (const feature of features) {
|
||||
const geometry = feature.getGeometry();
|
||||
if (!geometry) continue;
|
||||
|
||||
if (geometry.getType() === 'Polygon') {
|
||||
const rings = (geometry as any).getCoordinates() as number[][][];
|
||||
for (const ring of rings) {
|
||||
if (this.drawRingToContext(context, ring, frameState)) {
|
||||
hasValidPath = true;
|
||||
}
|
||||
}
|
||||
} else if (geometry.getType() === 'MultiPolygon') {
|
||||
const polygons = (geometry as any).getCoordinates() as number[][][][];
|
||||
for (const polygonRings of polygons) {
|
||||
for (const ring of polygonRings) {
|
||||
if (this.drawRingToContext(context, ring, frameState)) {
|
||||
hasValidPath = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidPath) {
|
||||
console.warn('未能生成有效的裁切路径');
|
||||
return;
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.lineWidth = 6;
|
||||
context.strokeStyle = '#6D64DF';
|
||||
context.stroke();
|
||||
context.clip();
|
||||
context.strokeStyle = '#CCC9F4';
|
||||
context.lineWidth = 5;
|
||||
context.stroke();
|
||||
context.clip();
|
||||
};
|
||||
|
||||
const maskPostrender = (event: any) => {
|
||||
if (event.context) {
|
||||
event.context.restore();
|
||||
event.context.restore();
|
||||
}
|
||||
};
|
||||
|
||||
this.maskBindings.push({
|
||||
layer: rasterLayer,
|
||||
prerender: maskPrerender,
|
||||
postrender: maskPostrender
|
||||
});
|
||||
|
||||
rasterLayer.on('prerender', maskPrerender);
|
||||
rasterLayer.on('postrender', maskPostrender);
|
||||
rasterLayer.changed();
|
||||
});
|
||||
}
|
||||
|
||||
// 备注:清除当前底图遮罩并按需恢复默认视野,供切换基地和异常兜底复用。
|
||||
clearMapMask(resetView = true): void {
|
||||
const maskedLayers = this.maskBindings.map(binding => binding.layer);
|
||||
this.clearMaskEvents();
|
||||
|
||||
maskedLayers.forEach(layer => layer.changed());
|
||||
|
||||
if (resetView && this.view) {
|
||||
this.view.animate({
|
||||
center: fromLonLat(this.defaultCenter),
|
||||
zoom: this.defaultZoom,
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 备注:统一销毁区域过滤相关引用和事件,供地图销毁阶段复用。
|
||||
destroy(): void {
|
||||
this.clearMapMask(false);
|
||||
this.map = null;
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
// 备注:从 GeoJSON 中提取可用于点在面判断的多边形环坐标。
|
||||
private extractPolygonCoords(geoJson: any): number[][][] {
|
||||
if (!geoJson?.features?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const coords: number[][][] = [];
|
||||
geoJson.features.forEach((feature: any) => {
|
||||
const geometry = feature.geometry;
|
||||
if (!geometry) return;
|
||||
|
||||
if (geometry.type === 'Polygon') {
|
||||
coords.push(...geometry.coordinates);
|
||||
} else if (geometry.type === 'MultiPolygon') {
|
||||
geometry.coordinates.forEach((polygon: number[][][]) => {
|
||||
coords.push(...polygon);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
// 备注:使用射线法判断点是否落在任意一个区域环内。
|
||||
private isPointInPolygon(
|
||||
lon: number,
|
||||
lat: number,
|
||||
polygons: number[][][]
|
||||
): boolean {
|
||||
for (const polygon of polygons) {
|
||||
if (this.isPointInRing(lon, lat, polygon)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 备注:使用射线法判断点是否落在单个闭合环内。
|
||||
private isPointInRing(lon: number, lat: number, ring: number[][]): boolean {
|
||||
let inside = false;
|
||||
const ringLength = ring.length;
|
||||
|
||||
for (let i = 0, j = ringLength - 1; i < ringLength; j = i++) {
|
||||
const xi = ring[i][0];
|
||||
const yi = ring[i][1];
|
||||
const xj = ring[j][0];
|
||||
const yj = ring[j][1];
|
||||
|
||||
const intersect =
|
||||
yi > lat !== yj > lat &&
|
||||
lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
||||
|
||||
if (intersect) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
// 备注:把单个面环转换为当前帧 Canvas 可裁切的路径。
|
||||
private drawRingToContext(
|
||||
context: CanvasRenderingContext2D,
|
||||
ring: number[][],
|
||||
frameState: any
|
||||
): boolean {
|
||||
if (!ring || ring.length < 3) return false;
|
||||
|
||||
const pixelRatio = frameState.pixelRatio || window.devicePixelRatio || 1;
|
||||
let moved = false;
|
||||
|
||||
for (let i = 0; i < ring.length; i++) {
|
||||
const coord = ring[i];
|
||||
if (!coord || coord.length < 2 || typeof coord[0] !== 'number') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cssPixel = this.map?.getPixelFromCoordinate(coord as [number, number]);
|
||||
if (!cssPixel) continue;
|
||||
|
||||
const canvasX = cssPixel[0] * pixelRatio;
|
||||
const canvasY = cssPixel[1] * pixelRatio;
|
||||
|
||||
if (!moved) {
|
||||
context.moveTo(canvasX, canvasY);
|
||||
moved = true;
|
||||
} else {
|
||||
context.lineTo(canvasX, canvasY);
|
||||
}
|
||||
}
|
||||
|
||||
if (moved) {
|
||||
context.closePath();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 备注:解除旧底图上的遮罩事件,避免重复绑定或残留旧裁切效果。
|
||||
private clearMaskEvents(): void {
|
||||
this.maskBindings.forEach(binding => {
|
||||
binding.layer.un('prerender', binding.prerender);
|
||||
binding.layer.un('postrender', binding.postrender);
|
||||
});
|
||||
this.maskBindings = [];
|
||||
}
|
||||
}
|
||||
@ -1,747 +0,0 @@
|
||||
/**
|
||||
* 倾斜摄影(OSGB)工具函数
|
||||
* 适配自旧项目 osgbTool.ts,核心逻辑保持一致
|
||||
*
|
||||
* 使用字段: url, boundary, location, height, accuracy
|
||||
*/
|
||||
import * as turf from '@turf/turf';
|
||||
import * as Cesium from 'cesium';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export interface OSGBLocation {
|
||||
destination: number[]; // [x, y, z] 或 [lng, lat, height]
|
||||
orientation: {
|
||||
heading: number;
|
||||
pitch: number;
|
||||
roll: number;
|
||||
};
|
||||
center: number[];
|
||||
}
|
||||
|
||||
export interface OSGBItem {
|
||||
/** 3D Tileset 服务地址 */
|
||||
url: string;
|
||||
/** GeoJSON Polygon 边界(用于裁切) */
|
||||
boundary?: any;
|
||||
/** 定位信息 */
|
||||
location?: OSGBLocation | string;
|
||||
/** 模型高度偏移 */
|
||||
height?: number;
|
||||
/** 屏幕空间误差 (默认 16) */
|
||||
accuracy?: number;
|
||||
/** 内部状态 - 不要手动设置 */
|
||||
osgbtitles?: any;
|
||||
cartesian3?: any;
|
||||
clippingPlanes?: any;
|
||||
eventListener?: any;
|
||||
_loading?: boolean;
|
||||
_loadId?: number;
|
||||
/** 用户手动关闭开关标志:为 true 时不自动加载/显示,只做显隐切换 */
|
||||
_switchOff?: boolean;
|
||||
}
|
||||
|
||||
// ==================== 内部状态(每个 OSGB 实例独立) ====================
|
||||
|
||||
interface OSGBInstanceState {
|
||||
destination: Cesium.Cartesian3 | null;
|
||||
orientation: any;
|
||||
timelookat: any;
|
||||
time: number;
|
||||
num: number;
|
||||
witetime: number;
|
||||
timeout: any;
|
||||
}
|
||||
|
||||
const instanceStates = new WeakMap<OSGBItem, OSGBInstanceState>();
|
||||
|
||||
function getState(obj: OSGBItem): OSGBInstanceState {
|
||||
let state = instanceStates.get(obj);
|
||||
if (!state) {
|
||||
state = {
|
||||
destination: null,
|
||||
orientation: null,
|
||||
timelookat: null,
|
||||
time: 0,
|
||||
num: 0,
|
||||
witetime: 0,
|
||||
timeout: undefined
|
||||
};
|
||||
instanceStates.set(obj, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function clearState(obj: OSGBItem) {
|
||||
const state = instanceStates.get(obj);
|
||||
if (state) {
|
||||
clearTimeout(state.timeout);
|
||||
clearInterval(state.timelookat);
|
||||
}
|
||||
instanceStates.delete(obj);
|
||||
}
|
||||
|
||||
// ==================== 3D Tileset 场景管理 ====================
|
||||
|
||||
/**
|
||||
* 将 tileset 添加到场景
|
||||
*/
|
||||
const addOSGB = (viewer: Cesium.Viewer, tileset: Cesium.Cesium3DTileset) => {
|
||||
if (!viewer || viewer.isDestroyed()) return;
|
||||
viewer.scene.primitives.add(tileset);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从场景移除 tileset(PrimitiveCollection.remove 默认 destroy=true,会销毁 tileset)
|
||||
*/
|
||||
const removeOSGB = (viewer: Cesium.Viewer, tileset: Cesium.Cesium3DTileset) => {
|
||||
if (!viewer || viewer.isDestroyed()) return;
|
||||
viewer.scene.primitives.remove(tileset);
|
||||
};
|
||||
|
||||
// ==================== 裁切相关 ====================
|
||||
|
||||
/**
|
||||
* 规范化 GeoJSON 输入,提取纯 Geometry
|
||||
* 兼容 Feature / FeatureCollection / Geometry
|
||||
*/
|
||||
const normalizeGeometry = (geo: any): any => {
|
||||
if (!geo) return null;
|
||||
// 已经是 Geometry 类型(有 type 且 coordinates)
|
||||
if (geo.type && geo.coordinates) return geo;
|
||||
// Feature → 提取 geometry
|
||||
if (geo.type === 'Feature' && geo.geometry) return geo.geometry;
|
||||
// FeatureCollection → 取第一个 feature 的 geometry
|
||||
if (geo.type === 'FeatureCollection' && geo.features?.length) {
|
||||
return normalizeGeometry(geo.features[0]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const get_verts_core = (poly: any): Cesium.Cartesian3[] => {
|
||||
const verts_carte3: Cesium.Cartesian3[] = [];
|
||||
if (!poly) return verts_carte3;
|
||||
|
||||
try {
|
||||
// 规范化输入:兼容 Feature / FeatureCollection / Geometry
|
||||
const geom = normalizeGeometry(poly);
|
||||
if (!geom) return verts_carte3;
|
||||
|
||||
let verts = turf.coordAll(geom);
|
||||
const clockwiseRing = turf.booleanClockwise(turf.lineString(verts));
|
||||
if (clockwiseRing) {
|
||||
verts = verts.reverse();
|
||||
}
|
||||
for (let i = 0; i < verts.length - 1; i++) {
|
||||
const vert = verts[i];
|
||||
verts_carte3.push(Cesium.Cartesian3.fromDegrees(vert[0], vert[1]));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('get_verts_core 解析 boundary 失败:', e);
|
||||
}
|
||||
return verts_carte3;
|
||||
};
|
||||
|
||||
const get_clipping_planes = (
|
||||
points: Cesium.Cartesian3[]
|
||||
): Cesium.ClippingPlane[] => {
|
||||
const pointsLength = points.length;
|
||||
const clippingPlanes: Cesium.ClippingPlane[] = [];
|
||||
|
||||
for (let i = 0; i < pointsLength; ++i) {
|
||||
const nextIndex = (i + 1) % pointsLength;
|
||||
let midpoint = Cesium.Cartesian3.add(
|
||||
points[i],
|
||||
points[nextIndex],
|
||||
new Cesium.Cartesian3()
|
||||
);
|
||||
midpoint = Cesium.Cartesian3.multiplyByScalar(midpoint, 0.5, midpoint);
|
||||
|
||||
const up = Cesium.Cartesian3.normalize(midpoint, new Cesium.Cartesian3());
|
||||
let right = Cesium.Cartesian3.subtract(
|
||||
points[nextIndex],
|
||||
midpoint,
|
||||
new Cesium.Cartesian3()
|
||||
);
|
||||
right = Cesium.Cartesian3.normalize(right, right);
|
||||
|
||||
let normal = Cesium.Cartesian3.cross(right, up, new Cesium.Cartesian3());
|
||||
normal = Cesium.Cartesian3.normalize(normal, normal);
|
||||
|
||||
const originCenteredPlane = new Cesium.Plane(normal, 0.0);
|
||||
const distance = Cesium.Plane.getPointDistance(
|
||||
originCenteredPlane,
|
||||
midpoint
|
||||
);
|
||||
|
||||
clippingPlanes.push(new Cesium.ClippingPlane(normal, distance));
|
||||
}
|
||||
|
||||
return clippingPlanes;
|
||||
};
|
||||
|
||||
const clipping_terrain = (
|
||||
viewer: Cesium.Viewer,
|
||||
clippingPlanes: Cesium.ClippingPlane[]
|
||||
) => {
|
||||
const globe = viewer.scene.globe;
|
||||
globe.clippingPlanes = new Cesium.ClippingPlaneCollection({
|
||||
planes: clippingPlanes,
|
||||
edgeWidth: 0,
|
||||
edgeColor: Cesium.Color.GREEN,
|
||||
enabled: true
|
||||
});
|
||||
globe.backFaceCulling = true;
|
||||
globe.showSkirts = true;
|
||||
};
|
||||
|
||||
const clipping_3dtiles = (
|
||||
clippingPlanes: Cesium.ClippingPlane[],
|
||||
tileset: Cesium.Cesium3DTileset
|
||||
) => {
|
||||
// Cesium 1.141: root.transform 是内部属性,安全访问
|
||||
const rootTransform = (tileset as any)?.root?.transform;
|
||||
if (!rootTransform) return;
|
||||
const invtrans = Cesium.Matrix4.inverse(rootTransform, new Cesium.Matrix4());
|
||||
for (let i = 0; i < clippingPlanes.length; i++) {
|
||||
clippingPlanes[i] = Cesium.Plane.transform(clippingPlanes[i], invtrans);
|
||||
}
|
||||
tileset.clippingPlanes = new Cesium.ClippingPlaneCollection({
|
||||
planes: clippingPlanes,
|
||||
unionClippingRegions: true,
|
||||
edgeWidth: 0,
|
||||
edgeColor: Cesium.Color.RED,
|
||||
enabled: false
|
||||
});
|
||||
};
|
||||
|
||||
const Clip3DTile = (boundary: any, tileset: Cesium.Cesium3DTileset) => {
|
||||
try {
|
||||
const geom = normalizeGeometry(boundary);
|
||||
if (!geom) return;
|
||||
const buffered = turf.buffer(geom, 10, { units: 'meters' });
|
||||
const verts_core = get_verts_core(buffered).reverse();
|
||||
if (!verts_core.length) return;
|
||||
const planes = get_clipping_planes(verts_core);
|
||||
clipping_3dtiles(planes, tileset);
|
||||
} catch (e) {
|
||||
console.warn('Clip3DTile 裁切失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const ClipTerrain = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
// Cesium 1.141: 安全访问 clippingPlanes 内部属性 _planes
|
||||
const existingPlanes = (viewer.scene.globe.clippingPlanes as any)?._planes;
|
||||
if (
|
||||
!osgbObj.clippingPlanes ||
|
||||
(existingPlanes &&
|
||||
!compareClippingPlanes(osgbObj.clippingPlanes, existingPlanes))
|
||||
) {
|
||||
const verts_core = get_verts_core(osgbObj.boundary);
|
||||
if (!verts_core.length) return;
|
||||
const planes = get_clipping_planes(verts_core);
|
||||
clipping_terrain(viewer, planes);
|
||||
osgbObj.clippingPlanes = planes;
|
||||
}
|
||||
};
|
||||
|
||||
const removeClip = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
if (osgbObj.clippingPlanes) {
|
||||
removeClippingPlanes(viewer);
|
||||
osgbObj.clippingPlanes = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const removeClippingPlanes = (viewer: Cesium.Viewer) => {
|
||||
if (!Cesium.defined(viewer) || viewer.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
if (Cesium.defined(viewer.scene.globe.clippingPlanes)) {
|
||||
viewer.scene.globe.clippingPlanes.removeAll();
|
||||
}
|
||||
};
|
||||
|
||||
const compareClippingPlanes = (planes1: any, planes2: any): boolean => {
|
||||
let result = true;
|
||||
if (planes1.length === planes2?.length) {
|
||||
for (let index = 0; index < planes1.length; index++) {
|
||||
if (planes1[index]._distance !== planes2[index]._distance) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getMinDistanceOfOSGB = (viewer: Cesium.Viewer): number => {
|
||||
let result = Infinity;
|
||||
const length = viewer.scene.primitives.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const p = viewer.scene.primitives.get(i);
|
||||
// Cesium 1.141: _url 和 _distanceToCamera 是内部属性,安全访问
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((p as any)?._url) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = (p as any)?.root?._distanceToCamera;
|
||||
if (d != null && d < result) result = d;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// ==================== 动态显隐 ====================
|
||||
|
||||
const dynamicSetVisible = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
if (!Cesium.defined(viewer) || viewer.isDestroyed()) return;
|
||||
if (!Cesium.defined(osgbObj?.cartesian3)) return;
|
||||
|
||||
let cameraPosition: Cesium.Cartesian3 | undefined;
|
||||
if (viewer.trackedEntity) {
|
||||
cameraPosition = viewer.trackedEntity.position?.getValue(
|
||||
viewer.clock.currentTime
|
||||
);
|
||||
}
|
||||
if (!cameraPosition) {
|
||||
cameraPosition = viewer.camera.position.clone();
|
||||
}
|
||||
|
||||
const distance = Cesium.Cartesian3.distance(
|
||||
osgbObj.cartesian3,
|
||||
cameraPosition
|
||||
);
|
||||
|
||||
if (distance < 12000) {
|
||||
// 用户手动关闭了开关 → 不自动加载
|
||||
if (osgbObj._switchOff) return;
|
||||
|
||||
// 匹配旧代码逻辑:osgbObj.osgbtitles 同步设置为 nonClassificationTileset 防重入
|
||||
// 新代码用 _loading + _loadId 实现同样的效果(因为 fromUrl 是异步的)
|
||||
if (!osgbObj.osgbtitles && !osgbObj._loading) {
|
||||
osgbObj._loading = true;
|
||||
osgbObj._loadId = (osgbObj._loadId || 0) + 1;
|
||||
const loadId = osgbObj._loadId;
|
||||
|
||||
const url = osgbObj.url.includes('?')
|
||||
? osgbObj.url + `&token=${getToken()}`
|
||||
: osgbObj.url + `?token=${getToken()}`;
|
||||
Cesium.Cesium3DTileset.fromUrl(url, {
|
||||
maximumScreenSpaceError: Number(osgbObj?.accuracy) || 16,
|
||||
show: true,
|
||||
maximumMemoryUsage: 128
|
||||
})
|
||||
.then((tileset: Cesium.Cesium3DTileset) => {
|
||||
// 竞态保护:版本号变了(removeQxsy 被调用或新一轮加载开始)→ 销毁退出
|
||||
if (
|
||||
osgbObj._loadId !== loadId ||
|
||||
!Cesium.defined(viewer) ||
|
||||
viewer.isDestroyed()
|
||||
) {
|
||||
tileset.destroy();
|
||||
return;
|
||||
}
|
||||
osgbObj._loading = false;
|
||||
if (!Cesium.defined(tileset)) return;
|
||||
tileset.maximumScreenSpaceError = 32;
|
||||
|
||||
// height 向上偏移:模型几何自带高程,height 只是微调(如 44m)
|
||||
if (osgbObj.height != 0) {
|
||||
const bs = Cesium.Cartographic.fromCartesian(
|
||||
tileset.boundingSphere.center
|
||||
);
|
||||
const surface = Cesium.Cartesian3.fromRadians(
|
||||
bs.longitude,
|
||||
bs.latitude,
|
||||
0.0
|
||||
);
|
||||
const offset = Cesium.Cartesian3.fromRadians(
|
||||
bs.longitude,
|
||||
bs.latitude,
|
||||
osgbObj.height
|
||||
);
|
||||
const translation = Cesium.Cartesian3.subtract(
|
||||
offset,
|
||||
surface,
|
||||
new Cesium.Cartesian3()
|
||||
);
|
||||
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
|
||||
}
|
||||
|
||||
addOSGB(viewer, tileset);
|
||||
osgbObj.osgbtitles = tileset;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
osgbObj._loading = false;
|
||||
console.log('dynamicSetVisible 加载倾斜摄影报错', e);
|
||||
});
|
||||
}
|
||||
} else if (distance > 13000) {
|
||||
// 匹配旧代码逻辑:镜头拉远 → 销毁 tileset 释放内存
|
||||
if (osgbObj.osgbtitles) {
|
||||
removeOSGB(viewer, osgbObj.osgbtitles);
|
||||
osgbObj.osgbtitles = undefined;
|
||||
}
|
||||
// 不重置 _switchOff,开关状态由用户手动控制,不受距离影响
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 地形动态裁切 ====================
|
||||
|
||||
const dynamicClipTerrain = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
if (!osgbObj?.osgbtitles) {
|
||||
removeClip(viewer, osgbObj);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cesium 1.141: root 和 _distanceToCamera 是内部属性,安全访问
|
||||
const root = (osgbObj.osgbtitles as any)?.root;
|
||||
if (!Cesium.defined(root)) {
|
||||
removeClip(viewer, osgbObj);
|
||||
if (osgbObj.osgbtitles?.clippingPlanes?.enabled) {
|
||||
osgbObj.osgbtitles.clippingPlanes.enabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!osgbObj?.osgbtitles?.show) {
|
||||
removeClip(viewer, osgbObj);
|
||||
if (osgbObj.osgbtitles?.clippingPlanes?.enabled) {
|
||||
osgbObj.osgbtitles.clippingPlanes.enabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const distance = root._distanceToCamera;
|
||||
const tilesLoaded = osgbObj.osgbtitles.tilesLoaded;
|
||||
if (osgbObj.boundary && tilesLoaded) {
|
||||
if (distance != null && getMinDistanceOfOSGB(viewer) === distance) {
|
||||
ClipTerrain(viewer, osgbObj);
|
||||
if (
|
||||
osgbObj.osgbtitles?.clippingPlanes &&
|
||||
!osgbObj.osgbtitles.clippingPlanes.enabled
|
||||
) {
|
||||
osgbObj.osgbtitles.clippingPlanes.enabled = true;
|
||||
}
|
||||
} else {
|
||||
removeClip(viewer, osgbObj);
|
||||
if (osgbObj.osgbtitles?.clippingPlanes?.enabled) {
|
||||
osgbObj.osgbtitles.clippingPlanes.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 公开 API ====================
|
||||
|
||||
/**
|
||||
* 加载倾斜摄影 3D Tileset 模型
|
||||
*
|
||||
* Cesium 1.141 中必须使用 Cesium3DTileset.fromUrl() 静态方法,
|
||||
* new Cesium3DTileset({url}) 不会触发网络请求。
|
||||
*/
|
||||
export const LoadOSGB = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
if (!osgbObj.url) return;
|
||||
|
||||
// 已加载则跳过
|
||||
if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) return;
|
||||
|
||||
// 正在加载中也跳过(防止重复调用 fromUrl)
|
||||
if (osgbObj._loading) return;
|
||||
osgbObj._loading = true;
|
||||
|
||||
// 自增版本号:每次 LoadOSGB 都 +1,.then() 里靠版本号判断是否已失效
|
||||
osgbObj._loadId = (osgbObj._loadId || 0) + 1;
|
||||
const loadId = osgbObj._loadId;
|
||||
|
||||
const url = osgbObj.url.includes('?')
|
||||
? osgbObj.url + `&token=${getToken()}`
|
||||
: osgbObj.url + `?token=${getToken()}`;
|
||||
|
||||
Cesium.Cesium3DTileset.fromUrl(url, {
|
||||
maximumScreenSpaceError: Number(osgbObj?.accuracy) || 16,
|
||||
show: true,
|
||||
maximumMemoryUsage: 128
|
||||
})
|
||||
.then((tileset: Cesium.Cesium3DTileset) => {
|
||||
// 竞态保护:版本号变了 → 销毁退出
|
||||
if (
|
||||
osgbObj._loadId !== loadId ||
|
||||
!Cesium.defined(viewer) ||
|
||||
viewer.isDestroyed()
|
||||
) {
|
||||
tileset.destroy();
|
||||
return;
|
||||
}
|
||||
if (!Cesium.defined(tileset)) return;
|
||||
|
||||
osgbObj._loading = false;
|
||||
tileset.maximumScreenSpaceError = 32;
|
||||
|
||||
// height 向上偏移:模型几何自带高程,height 只是微调(如 44m)
|
||||
if (osgbObj.height != 0) {
|
||||
const bs = Cesium.Cartographic.fromCartesian(
|
||||
tileset.boundingSphere.center
|
||||
);
|
||||
const surface = Cesium.Cartesian3.fromRadians(
|
||||
bs.longitude,
|
||||
bs.latitude,
|
||||
0.0
|
||||
);
|
||||
const offset = Cesium.Cartesian3.fromRadians(
|
||||
bs.longitude,
|
||||
bs.latitude,
|
||||
osgbObj.height
|
||||
);
|
||||
const translation = Cesium.Cartesian3.subtract(
|
||||
offset,
|
||||
surface,
|
||||
new Cesium.Cartesian3()
|
||||
);
|
||||
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
|
||||
}
|
||||
|
||||
// 加入场景(匹配旧代码 readyPromise 中 addOSGB)
|
||||
addOSGB(viewer, tileset);
|
||||
osgbObj.osgbtitles = tileset;
|
||||
|
||||
// 始终设置 cartesian3,保证 dynamicSetVisible 能计算距离
|
||||
osgbObj.cartesian3 = tileset.boundingSphere.center.clone();
|
||||
|
||||
// boundary 裁切(匹配旧代码 readyPromise 中处理)
|
||||
if (osgbObj.boundary) {
|
||||
if (typeof osgbObj.boundary === 'string') {
|
||||
osgbObj.boundary = JSON.parse(osgbObj.boundary);
|
||||
}
|
||||
Clip3DTile(osgbObj.boundary, tileset);
|
||||
}
|
||||
|
||||
// 注册 postRender 动态显隐(300ms 节流,匹配旧代码 throttle)
|
||||
let lastTime = 0;
|
||||
osgbObj.eventListener = viewer.scene.postRender.addEventListener(() => {
|
||||
if (!Cesium.defined(viewer) || viewer.isDestroyed()) return;
|
||||
const now = performance.now();
|
||||
if (now - lastTime < 300) return;
|
||||
lastTime = now;
|
||||
|
||||
if (!getState(osgbObj).timelookat) {
|
||||
dynamicSetVisible(viewer, osgbObj);
|
||||
}
|
||||
dynamicClipTerrain(viewer, osgbObj);
|
||||
});
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
osgbObj._loading = false;
|
||||
console.log('加载倾斜摄影报错', e);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 卸载倾斜摄影模型(完全移除)
|
||||
* 仅在切换 2D 等场景调用,平时开关节用 osgbChangeClick 做显隐
|
||||
* 匹配旧代码 removeqxsy(osgbTool.ts:233-244):不清理 cartesian3
|
||||
*/
|
||||
export const removeQxsy = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
// 递增版本号:使所有进行中的 fromUrl().then() 失效
|
||||
osgbObj._loadId = (osgbObj._loadId || 0) + 1;
|
||||
removeClippingPlanes(viewer);
|
||||
osgbObj.clippingPlanes = undefined;
|
||||
osgbObj._loading = false;
|
||||
osgbObj._switchOff = undefined;
|
||||
|
||||
if (typeof osgbObj.eventListener === 'function') {
|
||||
osgbObj.eventListener();
|
||||
osgbObj.eventListener = undefined;
|
||||
}
|
||||
|
||||
if (osgbObj.osgbtitles) {
|
||||
removeOSGB(viewer, osgbObj.osgbtitles);
|
||||
osgbObj.osgbtitles = undefined;
|
||||
}
|
||||
// 注意:不清理 cartesian3,匹配旧代码行为
|
||||
};
|
||||
|
||||
/**
|
||||
* 倾斜摄影显隐切换
|
||||
* - 开关状态全局记录,不受距离影响
|
||||
* - 12km 以内:执行实际的显隐/加载操作
|
||||
* - 12km 以外:仅记录状态,不做 tileset 操作(tileset 不存在或已销毁)
|
||||
* - 仅切换 2D 时才通过 removeQxsy 真正销毁
|
||||
*/
|
||||
export const osgbChangeClick = (
|
||||
viewer: Cesium.Viewer,
|
||||
osgbObj: OSGBItem,
|
||||
checked: boolean
|
||||
) => {
|
||||
// 先记录开关状态(无论距离远近)
|
||||
osgbObj._switchOff = !checked;
|
||||
|
||||
// 12km 以外:只记状态不操作 tileset(此时 tileset 可能不存在或已销毁)
|
||||
if (osgbObj.cartesian3) {
|
||||
const cameraPosition = viewer.camera.position.clone();
|
||||
const distance = Cesium.Cartesian3.distance(
|
||||
osgbObj.cartesian3,
|
||||
cameraPosition
|
||||
);
|
||||
if (distance >= 12000) return;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// 开关 ON:已有未销毁的 tileset → 直接显示;否则加载
|
||||
if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) {
|
||||
osgbObj.osgbtitles.show = true;
|
||||
} else {
|
||||
LoadOSGB(viewer, osgbObj);
|
||||
}
|
||||
} else {
|
||||
// 开关 OFF:隐藏 tileset,移除 terrain 裁切
|
||||
if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) {
|
||||
osgbObj.osgbtitles.show = false;
|
||||
}
|
||||
removeClip(viewer, osgbObj);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 location 字段为结构化定位信息
|
||||
*/
|
||||
const parseLocation = (location: any): OSGBLocation | null => {
|
||||
if (!location) return null;
|
||||
|
||||
// 如果已经是对象,直接用
|
||||
if (typeof location === 'object' && location.destination) {
|
||||
return location as OSGBLocation;
|
||||
}
|
||||
|
||||
// 如果是字符串,尝试解析
|
||||
if (typeof location === 'string') {
|
||||
try {
|
||||
const jsonString = location.replace(/\n/g, '');
|
||||
const startIndex = jsonString.indexOf('{');
|
||||
const endIndex = jsonString.lastIndexOf('}');
|
||||
if (startIndex === -1 || endIndex === -1) return null;
|
||||
const extractedData = jsonString.substring(startIndex, endIndex + 1);
|
||||
const parsed = eval('(' + extractedData + ')');
|
||||
return {
|
||||
destination: parsed.destination,
|
||||
orientation: parsed.orientation,
|
||||
center: parsed.center
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 飞行定位到倾斜摄影模型
|
||||
* 包含 flyTo + 到达后旋转动画
|
||||
*/
|
||||
export const osgbLocation = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => {
|
||||
const state = getState(osgbObj);
|
||||
|
||||
clearTimeout(state.timeout);
|
||||
state.num = 0;
|
||||
state.time = 0;
|
||||
state.witetime = 0;
|
||||
|
||||
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
|
||||
let center = Cesium.Cartesian3.fromDegrees(
|
||||
100.271729604817,
|
||||
35.3107902811941,
|
||||
2800
|
||||
);
|
||||
let heading = Cesium.Math.toRadians(0.0);
|
||||
const pitch = Cesium.Math.toRadians(-30.0);
|
||||
const range = 2000.0;
|
||||
|
||||
const locationInfor = parseLocation(osgbObj.location);
|
||||
|
||||
if (!locationInfor) {
|
||||
console.warn('该模型定位坐标未配置!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是经纬度还是笛卡尔坐标
|
||||
const destCoords = locationInfor.destination;
|
||||
if (destCoords[0] <= 180 && destCoords[0] >= -180) {
|
||||
state.destination = Cesium.Cartesian3.fromDegrees(
|
||||
destCoords[0],
|
||||
destCoords[1],
|
||||
destCoords[2]
|
||||
);
|
||||
} else {
|
||||
state.destination = new Cesium.Cartesian3(
|
||||
destCoords[0],
|
||||
destCoords[1],
|
||||
destCoords[2]
|
||||
);
|
||||
}
|
||||
|
||||
state.orientation = locationInfor.orientation;
|
||||
center = Cesium.Cartesian3.fromDegrees(
|
||||
locationInfor.center[0],
|
||||
locationInfor.center[1],
|
||||
locationInfor.center[2]
|
||||
);
|
||||
|
||||
if (!state.destination || !state.orientation) return;
|
||||
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.flyTo({
|
||||
destination: state.destination,
|
||||
orientation: state.orientation,
|
||||
complete: () => {
|
||||
state.timeout = setTimeout(() => {
|
||||
rotate();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
const rotate = () => {
|
||||
clearInterval(state.timelookat);
|
||||
state.timelookat = setInterval(() => {
|
||||
if (!Cesium.defined(viewer) || viewer.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
state.witetime += 0.7;
|
||||
if (state.witetime > 180 && state.witetime < 360) {
|
||||
state.time += 0.7;
|
||||
heading = Cesium.Math.toRadians(state.time);
|
||||
viewer.camera.lookAt(
|
||||
center,
|
||||
new Cesium.HeadingPitchRange(heading, pitch, range)
|
||||
);
|
||||
} else if (state.witetime > 360) {
|
||||
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
clearInterval(state.timelookat);
|
||||
state.timelookat = null;
|
||||
viewer.camera.flyTo({
|
||||
destination: state.destination!,
|
||||
orientation: state.orientation,
|
||||
duration: 2
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 点击 body 两次退出旋转
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
body.onclick = function () {
|
||||
if (viewer && !viewer.isDestroyed() && viewer.camera) {
|
||||
state.num = state.num + 1;
|
||||
if (state.num > 1) {
|
||||
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
clearInterval(state.timelookat);
|
||||
clearTimeout(state.timeout);
|
||||
state.timelookat = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -2,9 +2,7 @@
|
||||
import { ref, onBeforeMount, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useMapStore } from '@/store/modules/map';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
const mapStore = useMapStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const menus: any = ref([]); // 主菜单列表
|
||||
const subMenus: any = ref([]); // 子菜单列表
|
||||
@ -14,15 +12,6 @@ const subActiveKey = ref(''); // 子菜单选中项
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const buildPageKeyFromPath = (path?: string) => {
|
||||
const normalizedPath = path || '';
|
||||
const parts = normalizedPath.split('/').filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]}_${parts[1]}`;
|
||||
}
|
||||
return normalizedPath || '__route-change__';
|
||||
};
|
||||
|
||||
// 主菜单切换
|
||||
const handleTabChange = (key: string) => {
|
||||
let data = menus.value.find((item: any) => item.id === key);
|
||||
@ -33,9 +22,6 @@ const handleTabChange = (key: string) => {
|
||||
let subData = subMenus.value.find(
|
||||
(item: any) => item.id === subActiveKey.value
|
||||
);
|
||||
mapStore.markPendingPageNavigation(
|
||||
buildPageKeyFromPath(subData?.path || subData?.opturl)
|
||||
);
|
||||
router.push(subData.path || subData.opturl);
|
||||
} else {
|
||||
subActiveKey.value = '';
|
||||
@ -45,9 +31,6 @@ const handleTabChange = (key: string) => {
|
||||
const handleSubTabChange = (key: string) => {
|
||||
subActiveKey.value = key;
|
||||
let data = subMenus.value.find((item: any) => item.id === key);
|
||||
mapStore.markPendingPageNavigation(
|
||||
buildPageKeyFromPath(data?.path || data?.opturl)
|
||||
);
|
||||
router.push(data.path || data.opturl);
|
||||
};
|
||||
|
||||
|
||||
@ -13,9 +13,7 @@ import 'ant-design-vue/dist/reset.css'; // Ant Design 全局样式重置
|
||||
import dayjs from 'dayjs'; // ant 中文语言
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import 'virtual:svg-icons-register';
|
||||
// 3d地图
|
||||
import * as Cesium from 'cesium';
|
||||
import 'cesium/Build/Cesium/Widgets/widgets.css';
|
||||
|
||||
|
||||
// 国际化
|
||||
import i18n from '@/lang/index';
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
const isOpen = ref(true);
|
||||
const uiStore = useUiStore();
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
|
||||
const jidiDataNum = ref(9);
|
||||
const itemClick = (index: any) => {
|
||||
JidiSelectEventStore.updataJidiData(index);
|
||||
};
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="jidiSelectorMod" v-show="!uiStore.isRoaming">
|
||||
<a-spin :spinning="JidiSelectEventStore.loading">
|
||||
<div
|
||||
class="qgc-dropdown-select"
|
||||
@mouseenter="jidiDataNum = JidiSelectEventStore.jidiData.length"
|
||||
@mouseleave="jidiDataNum = 9"
|
||||
>
|
||||
<div class="title" @click="isOpen = !isOpen">
|
||||
<div>水电基地</div>
|
||||
<div
|
||||
style="padding-right: 5px"
|
||||
:style="{ transform: !isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }"
|
||||
>
|
||||
<i class="icon iconfont icon-topOutline"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="item"
|
||||
v-for="(i, index) in JidiSelectEventStore.jidiData.slice(
|
||||
0,
|
||||
jidiDataNum
|
||||
)"
|
||||
:key="index"
|
||||
:class="{ selected: i.selected }"
|
||||
@click="itemClick(index)"
|
||||
>
|
||||
<i class="icon iconfont icon-hydroPower"></i>
|
||||
<span style="margin-left: 10px">{{ i.wbsName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/variables.module.scss' as *;
|
||||
.jidiSelectorMod {
|
||||
width: 175px;
|
||||
max-height: 941px;
|
||||
border: 1px solid #cedce8;
|
||||
border-radius: 1px;
|
||||
background-color: #e5edf3;
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
border-radius: 1px;
|
||||
margin: 16px 0 0 16px;
|
||||
z-index: 99;
|
||||
pointer-events: auto;
|
||||
.qgc-dropdown-select {
|
||||
width: 100%;
|
||||
max-height: 941px;
|
||||
overflow: auto;
|
||||
.title {
|
||||
height: 26.14px;
|
||||
padding: 0 2px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: $primary-title-color;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-topOutline {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
scale: 0.6;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
margin: 4px 0;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #acc4d6;
|
||||
line-height: 30px;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover,
|
||||
&.selected {
|
||||
background-color: #2f6b98;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,806 +0,0 @@
|
||||
import { watch, type WatchStopHandle, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { unByKey } from 'ol/Observable';
|
||||
import dayjs from 'dayjs';
|
||||
import { MapClass } from '@/components/gis/map.class';
|
||||
import { getMapConfig, layerConfig2Flat } from '@/components/gis/gisUtils';
|
||||
import { useMapConfigStore } from '@/modules/map/stores/map-config.store';
|
||||
import { useMapDataStore } from '@/modules/map/stores/map-data.store';
|
||||
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
|
||||
import { useMapStore } from '@/store/modules/map';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
|
||||
type InitializeOptions = {
|
||||
container: HTMLElement;
|
||||
popupContainer?: HTMLDivElement | null;
|
||||
pageKey: string;
|
||||
};
|
||||
|
||||
type LoadPageOptions = {
|
||||
pageKey: string;
|
||||
isInitialLoad?: boolean;
|
||||
};
|
||||
|
||||
type ApplyLayerSelectionOptions = {
|
||||
checkedKeys: string[];
|
||||
triggerKey?: string;
|
||||
checked?: boolean;
|
||||
};
|
||||
const SYSTEM_ID = 'qgc';
|
||||
const HYDRO_DYNAMIC_LAYER_KEYS = [
|
||||
'dw_point',
|
||||
'stinfo_video_point',
|
||||
'stinfo_gjllz_point',
|
||||
'wt_point',
|
||||
'wq_ownWq_point',
|
||||
'wq_countryWq_point',
|
||||
'fp_point',
|
||||
'eq_point',
|
||||
'fb_point',
|
||||
'vp_point',
|
||||
'va_point',
|
||||
'sg_point'
|
||||
];
|
||||
const ENG_POINT_LAYER_KEY = 'eng_point';
|
||||
const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5;
|
||||
const CESIUM_ZOOM_SYNC_DEBOUNCE_MS = 80;
|
||||
const LARGE_ENG_LEGEND_PREFIX = 'large_eng_';
|
||||
const MID_ENG_LEGEND_PREFIX = 'mid_eng_';
|
||||
|
||||
export const useMapOrchestrator = () => {
|
||||
const route = useRoute();
|
||||
const mapClass = MapClass.getInstance();
|
||||
const mapStore = useMapStore();
|
||||
const mapConfigStore = useMapConfigStore();
|
||||
const mapDataStore = useMapDataStore();
|
||||
const mapViewStore = useMapViewStore();
|
||||
const jidiSelectEventStore = useJidiSelectEventStore();
|
||||
let activePageLoadRequestId = 0;
|
||||
let removeZoomListener: (() => void) | null = null;
|
||||
let stopBaseSelectionWatch: WatchStopHandle | null = null;
|
||||
const initializedBaseLayerKeys = new Set<string>();
|
||||
const baseSelectionDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const zoomSyncDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
let latestHydroMenuGetter: (() => boolean) | null = null;
|
||||
let currentMapType: '2D' | '3D' = '2D';
|
||||
let hydroMenuDefaultCheckedKeys = new Set<string>();
|
||||
let hydroDynamicLayerSyncTask: {
|
||||
visible: boolean;
|
||||
promise: Promise<string[] | void>;
|
||||
} | null = null;
|
||||
|
||||
// 备注:统一获取当前地图视图对象,避免在多个组件里重复兼容 2D/3D view 访问方式。
|
||||
const getCurrentView = () => {
|
||||
return mapClass.view?.getView ? mapClass.view.getView() : mapClass.view;
|
||||
};
|
||||
|
||||
// 备注:统一获取当前地图缩放级别,供页面切换和缩放监听复用。
|
||||
const getCurrentZoom = (): number | undefined => {
|
||||
return mapClass.getCurrentZoom();
|
||||
};
|
||||
|
||||
const clearZoomSyncDebounceTimer = () => {
|
||||
if (zoomSyncDebounceTimer.value) {
|
||||
clearTimeout(zoomSyncDebounceTimer.value);
|
||||
zoomSyncDebounceTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 备注:统一把当前页面中的 GIS 基础图层初始化到地图实例,避免在图层树组件里做配置解析和加层编排。
|
||||
const ensureBaseLayersInitialized = (configs: any[] = []) => {
|
||||
const flatLayerConfigs = layerConfig2Flat(configs);
|
||||
|
||||
flatLayerConfigs.forEach((item: any) => {
|
||||
if (item.type !== 'GISMap' || !item.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.config && item.paramJson) {
|
||||
try {
|
||||
const jsonObj = JSON.parse(item.paramJson);
|
||||
item.config = getMapConfig(jsonObj);
|
||||
} catch {
|
||||
item.config = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.key === 'customBaseLayer') {
|
||||
const serializableBaseLayerConfig = {
|
||||
key: item.config?.key,
|
||||
id: item.config?.id,
|
||||
name: item.config?.name,
|
||||
type: item.config?.type,
|
||||
url: item.config?.url,
|
||||
url_3d: item.config?.url_3d,
|
||||
layers: item.config?.layers,
|
||||
rasteropacity: item.config?.rasteropacity
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
'customBaseLayer',
|
||||
JSON.stringify(serializableBaseLayerConfig)
|
||||
);
|
||||
}
|
||||
|
||||
if (!initializedBaseLayerKeys.has(item.key)) {
|
||||
mapClass.addBaseDataLayer(item.config, item.checked === 1);
|
||||
initializedBaseLayerKeys.add(item.key);
|
||||
}
|
||||
|
||||
mapClass.controlBaseLayerTreeShowAndHidden(
|
||||
item.key,
|
||||
item.config?.id || item.key,
|
||||
item.checked === 1
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:统一加载当前页面所需的图层配置、图例配置以及首批锚点数据。
|
||||
const loadPage = async ({
|
||||
pageKey,
|
||||
isInitialLoad = false
|
||||
}: LoadPageOptions) => {
|
||||
let backgroundLoadStarted = false;
|
||||
const pageLoadRequestId = ++activePageLoadRequestId;
|
||||
|
||||
try {
|
||||
mapDataStore.setLoading(true);
|
||||
|
||||
const hasGlobalLegendConfig =
|
||||
Array.isArray(mapConfigStore.legendConfigOriginal) &&
|
||||
mapConfigStore.legendConfigOriginal.length > 0;
|
||||
const shouldLoadGlobalLegend = isInitialLoad && !hasGlobalLegendConfig;
|
||||
const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || '';
|
||||
const previousCheckedKeys = mapViewStore.getCheckedLayerKeys();
|
||||
const moduleId = (route.meta?.moduleId as string) || '';
|
||||
const loadOptions = {
|
||||
systemId: SYSTEM_ID,
|
||||
moduleId,
|
||||
pageKey,
|
||||
description: 'true'
|
||||
};
|
||||
const layerConfigPromise =
|
||||
mapConfigStore.loadPageLayerConfig(loadOptions);
|
||||
const legendConfigPromise = mapConfigStore.loadPageLegendConfig(
|
||||
moduleId,
|
||||
{
|
||||
includeGlobal: shouldLoadGlobalLegend
|
||||
}
|
||||
);
|
||||
|
||||
// 同时等待两个 promise
|
||||
const [{ layerConfig }, { legendOriginal, pageLegend }] =
|
||||
await Promise.all([layerConfigPromise, legendConfigPromise]);
|
||||
|
||||
if (pageLoadRequestId !== activePageLoadRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 先设置图层数据(更新 checkedLayerKeys),再设置图例数据
|
||||
if (layerConfig.length > 0) {
|
||||
hydroMenuDefaultCheckedKeys = new Set(
|
||||
mapConfigStore.extractCheckedLayerKeys(layerConfig)
|
||||
);
|
||||
mapStore.setLayerData(layerConfig);
|
||||
ensureBaseLayersInitialized(layerConfig);
|
||||
|
||||
let checkedKeys = mapViewStore.getCheckedLayerKeys();
|
||||
if (previousPageKey === pageKey && previousCheckedKeys.length > 0) {
|
||||
const getAllLayerKeys = (items: any[]): string[] => {
|
||||
const keys: string[] = [];
|
||||
const walk = (nodes: any[]) => {
|
||||
nodes.forEach(item => {
|
||||
if (item?.key) {
|
||||
keys.push(item.key);
|
||||
}
|
||||
if (item?.children?.length > 0) {
|
||||
walk(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(items);
|
||||
return keys;
|
||||
};
|
||||
const currentLayerKeys = new Set(getAllLayerKeys(layerConfig));
|
||||
const runtimeCheckedKeys = previousCheckedKeys.filter(key =>
|
||||
currentLayerKeys.has(key)
|
||||
);
|
||||
if (runtimeCheckedKeys.length > 0) {
|
||||
mapViewStore.setCheckedLayerKeys(runtimeCheckedKeys);
|
||||
checkedKeys = runtimeCheckedKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置图例数据(此时 checkedLayerKeys 已正确设置)
|
||||
if (legendOriginal.length > 0) {
|
||||
mapStore.setLegendData(legendOriginal, pageLegend);
|
||||
}
|
||||
|
||||
const activePageToken = mapStore.activatePageContext(
|
||||
pageKey,
|
||||
layerConfig
|
||||
);
|
||||
await mapStore.updateLayerData(checkedKeys, true);
|
||||
mapStore.setSelectedLegendData();
|
||||
const backgroundLoadPromise = isInitialLoad
|
||||
? mapStore.loadAllLayerData(layerConfig, checkedKeys, {
|
||||
pageToken: activePageToken,
|
||||
skipSessionCheck: true,
|
||||
pageKey
|
||||
})
|
||||
: mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, {
|
||||
pageToken: activePageToken,
|
||||
skipSessionCheck: true
|
||||
});
|
||||
|
||||
void backgroundLoadPromise.catch(error => {
|
||||
console.error(`页面锚点后台加载失败 [${pageKey}]`, error);
|
||||
});
|
||||
backgroundLoadStarted = true;
|
||||
}
|
||||
} finally {
|
||||
if (
|
||||
!backgroundLoadStarted &&
|
||||
pageLoadRequestId === activePageLoadRequestId
|
||||
) {
|
||||
mapDataStore.setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 备注:统一完成地图实例初始化和弹窗挂载,确保后续事件监听可以在图层数据加载前生效。
|
||||
const initializeMapShell = async ({
|
||||
container,
|
||||
popupContainer
|
||||
}: InitializeOptions) => {
|
||||
await mapClass.init(container);
|
||||
|
||||
if (popupContainer) {
|
||||
const popupHost = container.parentElement;
|
||||
if (popupHost && popupContainer.parentElement !== popupHost) {
|
||||
popupHost.appendChild(popupContainer);
|
||||
}
|
||||
popupContainer.style.display = 'none';
|
||||
mapClass.initPopupOverlay(popupContainer);
|
||||
}
|
||||
};
|
||||
|
||||
const replayCurrentMapState = async (
|
||||
options: Pick<InitializeOptions, 'popupContainer'> & {
|
||||
getIsHydroMenu: () => boolean;
|
||||
}
|
||||
) => {
|
||||
initializedBaseLayerKeys.clear();
|
||||
|
||||
const currentZoom = getCurrentZoom();
|
||||
if (currentZoom !== undefined) {
|
||||
mapViewStore.setCurrentZoomLevel(currentZoom);
|
||||
}
|
||||
|
||||
if (options.popupContainer) {
|
||||
const container = document.getElementById('mapContainer');
|
||||
const popupHost = container?.parentElement || null;
|
||||
if (popupHost && options.popupContainer.parentElement !== popupHost) {
|
||||
popupHost.appendChild(options.popupContainer);
|
||||
}
|
||||
options.popupContainer.style.display = 'none';
|
||||
mapClass.initPopupOverlay(options.popupContainer);
|
||||
}
|
||||
|
||||
ensureBaseLayersInitialized(mapStore.layerData || []);
|
||||
|
||||
const checkedKeys = mapViewStore.getCheckedLayerKeys();
|
||||
await mapStore.updateLayerData(checkedKeys, true);
|
||||
mapStore.setSelectedLegendData();
|
||||
await syncZoomSensitiveState({
|
||||
isHydroMenu: options.getIsHydroMenu(),
|
||||
refreshEngPoint: true
|
||||
});
|
||||
|
||||
const selectedBaseId = mapViewStore.selectedBaseId;
|
||||
mapClass.jdPanelControlShowAndHidden(
|
||||
selectedBaseId || 'all',
|
||||
!!selectedBaseId
|
||||
);
|
||||
|
||||
bindZoomListener(options.getIsHydroMenu);
|
||||
};
|
||||
|
||||
// 备注:统一完成首屏页面配置加载,供挂载阶段和后续页面重载复用。
|
||||
const initialize = async ({
|
||||
container,
|
||||
popupContainer,
|
||||
pageKey
|
||||
}: InitializeOptions) => {
|
||||
await initializeMapShell({
|
||||
container,
|
||||
popupContainer,
|
||||
pageKey
|
||||
});
|
||||
await loadPage({ pageKey, isInitialLoad: true });
|
||||
};
|
||||
|
||||
// 备注:统一绑定地图缩放监听,让页面组件只负责触发初始化,不再直接处理缩放联动细节。
|
||||
const bindZoomListener = (getIsHydroMenu: () => boolean) => {
|
||||
if (removeZoomListener) {
|
||||
removeZoomListener();
|
||||
removeZoomListener = null;
|
||||
}
|
||||
clearZoomSyncDebounceTimer();
|
||||
|
||||
const currentZoom = getCurrentZoom();
|
||||
if (currentZoom !== undefined) {
|
||||
mapViewStore.setCurrentZoomLevel(currentZoom);
|
||||
}
|
||||
|
||||
const emitZoomChange = async () => {
|
||||
const zoom = getCurrentZoom();
|
||||
if (zoom === undefined) return;
|
||||
|
||||
await handleZoomLevelChange(
|
||||
mapViewStore.currentZoomLevel,
|
||||
zoom,
|
||||
getIsHydroMenu()
|
||||
);
|
||||
};
|
||||
|
||||
if (currentMapType === '3D') {
|
||||
const removeCesiumListener =
|
||||
mapClass.view?.camera?.changed?.addEventListener(() => {
|
||||
clearZoomSyncDebounceTimer();
|
||||
zoomSyncDebounceTimer.value = setTimeout(() => {
|
||||
zoomSyncDebounceTimer.value = null;
|
||||
void emitZoomChange();
|
||||
}, CESIUM_ZOOM_SYNC_DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
if (typeof removeCesiumListener === 'function') {
|
||||
removeZoomListener = () => {
|
||||
clearZoomSyncDebounceTimer();
|
||||
removeCesiumListener();
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const view = getCurrentView();
|
||||
if (!view?.on) return;
|
||||
|
||||
const zoomListenerKey = view.on('change:resolution', () => {
|
||||
void emitZoomChange();
|
||||
});
|
||||
removeZoomListener = () => {
|
||||
unByKey(zoomListenerKey);
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:统一绑定基地切换事件,让页面组件不再直接 watch 外部基地选择源。
|
||||
const bindBaseSelection = () => {
|
||||
if (stopBaseSelectionWatch) {
|
||||
stopBaseSelectionWatch();
|
||||
stopBaseSelectionWatch = null;
|
||||
}
|
||||
|
||||
stopBaseSelectionWatch = watch(
|
||||
() => jidiSelectEventStore.selectedItem?.wbsCode,
|
||||
newWbsCode => {
|
||||
if (!newWbsCode) {
|
||||
changeBaseId('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseSelectionDebounceTimer.value) {
|
||||
clearTimeout(baseSelectionDebounceTimer.value);
|
||||
}
|
||||
|
||||
baseSelectionDebounceTimer.value = setTimeout(() => {
|
||||
changeBaseId(newWbsCode);
|
||||
baseSelectionDebounceTimer.value = null;
|
||||
}, 100);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
};
|
||||
|
||||
// 备注:统一挂载地图页面生命周期,初始化后自动接管缩放监听和基地切换监听。
|
||||
const mountView = async (
|
||||
options: InitializeOptions & {
|
||||
getIsHydroMenu: () => boolean;
|
||||
}
|
||||
) => {
|
||||
currentMapType = '2D';
|
||||
latestHydroMenuGetter = options.getIsHydroMenu;
|
||||
await initializeMapShell(options);
|
||||
bindBaseSelection();
|
||||
bindZoomListener(options.getIsHydroMenu);
|
||||
await loadPage({ pageKey: options.pageKey, isInitialLoad: true });
|
||||
await syncZoomSensitiveState({
|
||||
isHydroMenu: options.getIsHydroMenu(),
|
||||
refreshEngPoint: true
|
||||
});
|
||||
};
|
||||
|
||||
const switchMapType = async (
|
||||
type: '2D' | '3D',
|
||||
options: Pick<InitializeOptions, 'popupContainer'> & {
|
||||
getIsHydroMenu?: () => boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const hydroMenuGetter =
|
||||
options.getIsHydroMenu || latestHydroMenuGetter || (() => false);
|
||||
latestHydroMenuGetter = hydroMenuGetter;
|
||||
currentMapType = type;
|
||||
|
||||
await mapClass.switchView(type);
|
||||
await replayCurrentMapState({
|
||||
popupContainer: options.popupContainer,
|
||||
getIsHydroMenu: hydroMenuGetter
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:统一处理菜单切换后的页面配置重载,供页面 watch 直接复用。
|
||||
const reloadPage = async (pageKey: string) => {
|
||||
if (!pageKey) return;
|
||||
await loadPage({ pageKey, isInitialLoad: false });
|
||||
};
|
||||
|
||||
// 备注:菜单切换前只隐藏旧页面当前可见点图层,底图保持连续显示,避免切换时闪烁。
|
||||
const hideCurrentVisibleLayers = () => {
|
||||
const currentCheckedKeys = mapViewStore.getCheckedLayerKeys();
|
||||
|
||||
currentCheckedKeys.forEach(layerKey => {
|
||||
const layerItem = mapStore.findLayerByKey(mapStore.layerData, layerKey);
|
||||
if (!layerItem?.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layerItem.type === 'pointMap') {
|
||||
if (mapClass.hasLayer(layerItem.key)) {
|
||||
mapClass.mdLayerTreeShowOrHidden(layerItem.key, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:统一处理图层树勾选结果归一化和地图联动,供图层树组件复用。
|
||||
const applyLayerSelection = async ({
|
||||
checkedKeys,
|
||||
triggerKey,
|
||||
checked
|
||||
}: ApplyLayerSelectionOptions) => {
|
||||
const normalizedKeys = mapStore.normalizeCheckedLayerKeys(
|
||||
checkedKeys,
|
||||
triggerKey,
|
||||
checked
|
||||
);
|
||||
await mapStore.updateLayerData(normalizedKeys);
|
||||
return normalizedKeys;
|
||||
};
|
||||
|
||||
// 备注:统一确保指定动态图层完成数据预加载,避免缩放切换时重复在页面组件里拼装加载逻辑。
|
||||
const ensureDynamicLayersLoaded = async (layerKeys: string[] = []) => {
|
||||
const layersToLoad = layerKeys
|
||||
.map(layerKey => mapStore.findLayerByKey(mapStore.layerData, layerKey))
|
||||
.filter(
|
||||
(layer: any) => layer?.url && !mapDataStore.hasPointLayerData(layer.key)
|
||||
);
|
||||
|
||||
if (layersToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(layersToLoad.map(layer => mapStore.loadLayerData(layer)));
|
||||
};
|
||||
|
||||
// 备注:统一控制水电开发菜单下的动态图层增删,避免页面组件直接操作加载和勾选细节。
|
||||
const syncHydroDynamicLayers = async (visible: boolean) => {
|
||||
if (
|
||||
hydroDynamicLayerSyncTask &&
|
||||
hydroDynamicLayerSyncTask.visible === visible
|
||||
) {
|
||||
return hydroDynamicLayerSyncTask.promise;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
const currentCheckedKeys = mapViewStore.getCheckedLayerKeys();
|
||||
|
||||
if (visible) {
|
||||
const layersToAdd = HYDRO_DYNAMIC_LAYER_KEYS.filter(
|
||||
key => !currentCheckedKeys.includes(key)
|
||||
);
|
||||
|
||||
if (layersToAdd.length === 0) {
|
||||
return currentCheckedKeys;
|
||||
}
|
||||
|
||||
await ensureDynamicLayersLoaded(layersToAdd);
|
||||
return applyLayerSelection({
|
||||
checkedKeys: [...currentCheckedKeys, ...layersToAdd]
|
||||
});
|
||||
}
|
||||
|
||||
const nextCheckedKeys = currentCheckedKeys.filter(key => {
|
||||
if (!HYDRO_DYNAMIC_LAYER_KEYS.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
return hydroMenuDefaultCheckedKeys.has(key);
|
||||
});
|
||||
|
||||
if (nextCheckedKeys.length === currentCheckedKeys.length) {
|
||||
return currentCheckedKeys;
|
||||
}
|
||||
|
||||
return applyLayerSelection({
|
||||
checkedKeys: nextCheckedKeys
|
||||
});
|
||||
})().finally(() => {
|
||||
if (hydroDynamicLayerSyncTask?.promise === task) {
|
||||
hydroDynamicLayerSyncTask = null;
|
||||
}
|
||||
});
|
||||
|
||||
hydroDynamicLayerSyncTask = {
|
||||
visible,
|
||||
promise: task
|
||||
};
|
||||
return task;
|
||||
};
|
||||
|
||||
// 备注:统一按当前缩放对齐首页菜单的中型站过滤和 12 级动态图层状态。
|
||||
const syncZoomSensitiveState = async ({
|
||||
isHydroMenu,
|
||||
refreshEngPoint = false
|
||||
}: {
|
||||
isHydroMenu: boolean;
|
||||
refreshEngPoint?: boolean;
|
||||
}) => {
|
||||
const currentZoom = getCurrentZoom();
|
||||
if (currentZoom !== undefined) {
|
||||
mapViewStore.setCurrentZoomLevel(currentZoom);
|
||||
}
|
||||
|
||||
if (
|
||||
isHydroMenu &&
|
||||
refreshEngPoint &&
|
||||
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
|
||||
) {
|
||||
mapStore.refreshPointLayerDisplayData(ENG_POINT_LAYER_KEY);
|
||||
}
|
||||
|
||||
await syncHydroDynamicLayers(
|
||||
!!isHydroMenu && currentZoom !== undefined && currentZoom >= 12
|
||||
);
|
||||
};
|
||||
|
||||
// 备注:统一处理地图缩放联动,收口水电开发菜单下的动态图层阈值切换逻辑。
|
||||
const handleZoomLevelChange = async (
|
||||
previousZoom: number,
|
||||
currentZoom: number,
|
||||
isHydroMenu: boolean
|
||||
) => {
|
||||
mapViewStore.setCurrentZoomLevel(currentZoom);
|
||||
|
||||
const crossedEngPointMediumThreshold =
|
||||
(previousZoom <= ENG_POINT_MEDIUM_VISIBLE_ZOOM &&
|
||||
currentZoom > ENG_POINT_MEDIUM_VISIBLE_ZOOM) ||
|
||||
(previousZoom > ENG_POINT_MEDIUM_VISIBLE_ZOOM &&
|
||||
currentZoom <= ENG_POINT_MEDIUM_VISIBLE_ZOOM);
|
||||
|
||||
if (
|
||||
isHydroMenu &&
|
||||
crossedEngPointMediumThreshold &&
|
||||
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
|
||||
) {
|
||||
mapStore.refreshPointLayerDisplayData(ENG_POINT_LAYER_KEY);
|
||||
}
|
||||
|
||||
if (!isHydroMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldZoomLevel = Math.floor(previousZoom);
|
||||
const newZoomLevel = Math.floor(currentZoom);
|
||||
|
||||
if (oldZoomLevel < 12 && newZoomLevel >= 12) {
|
||||
await syncHydroDynamicLayers(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldZoomLevel >= 12 && newZoomLevel < 12) {
|
||||
await syncHydroDynamicLayers(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 备注:统一处理菜单切换后的页面重载和动态图层收口,减少页面组件里的分支编排。
|
||||
const handlePageChange = (pageKey: string, isHydroMenu: boolean) => {
|
||||
if (!pageKey) return Promise.resolve();
|
||||
|
||||
const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || '';
|
||||
if (previousPageKey && previousPageKey !== pageKey) {
|
||||
hideCurrentVisibleLayers();
|
||||
mapViewStore.setCheckedLayerKeys([]);
|
||||
mapStore.setSelectedLegendData();
|
||||
}
|
||||
|
||||
return reloadPage(pageKey).then(async () => {
|
||||
await syncZoomSensitiveState({
|
||||
isHydroMenu,
|
||||
refreshEngPoint: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:统一处理单个图层切换命令,供后续其他入口复用。
|
||||
const toggleLayer = async (layerKey: string, checked?: boolean) => {
|
||||
const currentKeys = mapViewStore.getCheckedLayerKeys();
|
||||
const currentSet = new Set(currentKeys);
|
||||
const nextChecked =
|
||||
checked === undefined ? !currentSet.has(layerKey) : checked;
|
||||
|
||||
if (nextChecked) {
|
||||
currentSet.add(layerKey);
|
||||
} else {
|
||||
currentSet.delete(layerKey);
|
||||
}
|
||||
|
||||
return applyLayerSelection({
|
||||
checkedKeys: Array.from(currentSet),
|
||||
triggerKey: layerKey,
|
||||
checked: nextChecked
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:统一处理单个图例切换命令,供图例组件复用。
|
||||
const toggleLegend = (nameEn: string, checked?: number) => {
|
||||
if (!nameEn) return;
|
||||
console.log('toggleLegend', nameEn, checked);
|
||||
console.log(mapViewStore.getLegendChecked(nameEn));
|
||||
const nextChecked =
|
||||
checked === undefined
|
||||
? mapViewStore.getLegendChecked(nameEn) === 1
|
||||
? 0
|
||||
: 1
|
||||
: checked;
|
||||
console.log(nextChecked);
|
||||
mapStore.updateLegendChecked(nameEn, nextChecked);
|
||||
};
|
||||
|
||||
// 备注:统一处理批量图例切换命令,供容量筛选和分组图例复用。
|
||||
const toggleLegendBatch = (nameEns: string[] = [], checked: number) => {
|
||||
const targetNameEns = nameEns.filter(Boolean);
|
||||
if (targetNameEns.length === 0) return;
|
||||
mapStore.updateLegendCheckedBatch(targetNameEns, checked);
|
||||
};
|
||||
|
||||
// 备注:统一处理基地切换命令,同时驱动地图基地裁切联动。
|
||||
const changeBaseId = (baseId: string) => {
|
||||
const nextBaseId = !baseId || baseId === 'all' ? '' : baseId;
|
||||
mapViewStore.setSelectedBaseId(nextBaseId);
|
||||
mapClass.jdPanelControlShowAndHidden(
|
||||
baseId || 'all',
|
||||
baseId !== 'all' && !!baseId
|
||||
);
|
||||
};
|
||||
|
||||
// 备注:统一处理时间范围变更,并触发相关图层数据重载。
|
||||
const changeTimeRange = async (range: [any, any]) => {
|
||||
mapViewStore.setSearchTimeRange(range);
|
||||
await mapStore.reloadBySearchTimeRange();
|
||||
};
|
||||
|
||||
// 备注:统一处理装机容量筛选,让筛选器切换时同步更新 eng_point 图例运行态和点位显示。
|
||||
const changeEngPointCapacity = (capacityType?: string | null) => {
|
||||
const legendItems = mapStore.getLegendItemsByLayerCode(ENG_POINT_LAYER_KEY);
|
||||
if (!legendItems.length) return;
|
||||
|
||||
const allEngLegendNames = legendItems
|
||||
.map((item: any) => item?.nameEn)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!allEngLegendNames.length) return;
|
||||
|
||||
const normalizedCapacityType = capacityType || 'all';
|
||||
let activePrefixes = [LARGE_ENG_LEGEND_PREFIX, MID_ENG_LEGEND_PREFIX];
|
||||
|
||||
if (normalizedCapacityType === 'large_eng_built') {
|
||||
activePrefixes = [LARGE_ENG_LEGEND_PREFIX];
|
||||
} else if (normalizedCapacityType === 'mid_eng_built') {
|
||||
activePrefixes = [MID_ENG_LEGEND_PREFIX];
|
||||
}
|
||||
|
||||
const visibleLegendNames = allEngLegendNames.filter((nameEn: string) =>
|
||||
activePrefixes.some(prefix => nameEn.startsWith(prefix))
|
||||
);
|
||||
const hiddenLegendNames = allEngLegendNames.filter(
|
||||
(nameEn: string) => !visibleLegendNames.includes(nameEn)
|
||||
);
|
||||
|
||||
mapStore.updateLegendCheckedBatch(hiddenLegendNames, 0);
|
||||
mapStore.updateLegendCheckedBatch(visibleLegendNames, 1);
|
||||
mapStore.refreshPointLayerDisplayData(ENG_POINT_LAYER_KEY);
|
||||
};
|
||||
|
||||
// 备注:统一重置地图筛选表单依赖的时间范围,供筛选组件在切页时复位默认输入。
|
||||
const resetFilterState = () => {
|
||||
mapViewStore.setSearchTimeRange([dayjs().subtract(1, 'M'), dayjs()]);
|
||||
};
|
||||
|
||||
// 备注:统一处理搜索定位,按点位编码从缓存数据中查找并飞行到目标位置。
|
||||
const focusPoint = (
|
||||
pointId: string,
|
||||
zoom = 15,
|
||||
fallbackPoints: any[] = []
|
||||
) => {
|
||||
if (!pointId) return;
|
||||
const pointMatcher = (item: any) => {
|
||||
return item.stcd === pointId || item._id === pointId;
|
||||
};
|
||||
const targetPoint =
|
||||
mapDataStore.pointData.find(pointMatcher) ||
|
||||
fallbackPoints.find(pointMatcher);
|
||||
if (!targetPoint?.lgtd || !targetPoint?.lttd) return;
|
||||
|
||||
const legendState = String(targetPoint?.anchoPointState || '');
|
||||
if (legendState && mapViewStore.getLegendChecked(legendState) !== 1) {
|
||||
mapStore.updateLegendChecked(legendState, 1);
|
||||
}
|
||||
|
||||
const layerKey = String(targetPoint?.layerKey || '');
|
||||
if (layerKey && mapClass.hasLayer(layerKey)) {
|
||||
mapClass.mdLayerTreeShowOrHidden(layerKey, true);
|
||||
}
|
||||
|
||||
mapClass.flyTopanto([targetPoint.lgtd, targetPoint.lttd], zoom);
|
||||
};
|
||||
|
||||
// 备注:统一释放地图页面生命周期里注册的监听,避免页面卸载后残留联动。
|
||||
const unmountView = () => {
|
||||
if (removeZoomListener) {
|
||||
removeZoomListener();
|
||||
removeZoomListener = null;
|
||||
}
|
||||
clearZoomSyncDebounceTimer();
|
||||
|
||||
if (stopBaseSelectionWatch) {
|
||||
stopBaseSelectionWatch();
|
||||
}
|
||||
|
||||
if (baseSelectionDebounceTimer.value) {
|
||||
clearTimeout(baseSelectionDebounceTimer.value);
|
||||
baseSelectionDebounceTimer.value = null;
|
||||
}
|
||||
|
||||
latestHydroMenuGetter = null;
|
||||
currentMapType = '2D';
|
||||
initializedBaseLayerKeys.clear();
|
||||
hydroMenuDefaultCheckedKeys.clear();
|
||||
};
|
||||
|
||||
return {
|
||||
initialize,
|
||||
mountView,
|
||||
switchMapType,
|
||||
unmountView,
|
||||
loadPage,
|
||||
reloadPage,
|
||||
applyLayerSelection,
|
||||
ensureDynamicLayersLoaded,
|
||||
syncHydroDynamicLayers,
|
||||
getCurrentZoom,
|
||||
handleZoomLevelChange,
|
||||
handlePageChange,
|
||||
toggleLayer,
|
||||
toggleLegend,
|
||||
toggleLegendBatch,
|
||||
changeBaseId,
|
||||
changeTimeRange,
|
||||
changeEngPointCapacity,
|
||||
resetFilterState,
|
||||
focusPoint
|
||||
};
|
||||
};
|
||||
@ -1,147 +0,0 @@
|
||||
type BuildLegendTreeOptions = {
|
||||
items?: any[];
|
||||
selectedLayerCodes?: Set<string>;
|
||||
getLegendChecked: (nameEn?: string) => number;
|
||||
normalizeLegendNameEn: (nameEn?: string) => string;
|
||||
};
|
||||
|
||||
// 备注:统一把 checked 值规范为 0/1,避免运行态和原始配置态混用不同类型。
|
||||
export const normalizeLegendCheckedValue = (
|
||||
checked: any
|
||||
): number | undefined => {
|
||||
if (checked === null || checked === undefined || checked === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const numericChecked = Number(checked);
|
||||
if (Number.isNaN(numericChecked)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return numericChecked === 1 ? 1 : 0;
|
||||
};
|
||||
|
||||
// 备注:收集图例树的叶子节点,供初始化图例运行态 checked 索引使用。
|
||||
export const collectLegendLeaves = (items: any[] = []): any[] => {
|
||||
const leaves: any[] = [];
|
||||
items.forEach(item => {
|
||||
if (item?.childrenList?.length > 0) {
|
||||
leaves.push(...collectLegendLeaves(item.childrenList));
|
||||
} else {
|
||||
leaves.push(item);
|
||||
}
|
||||
});
|
||||
return leaves;
|
||||
};
|
||||
|
||||
// 备注:根据原始图例配置生成图例运行态 checked 映射,避免后续重复递归取默认值。
|
||||
export const buildLegendCheckedState = (
|
||||
sourceData: any[] = [],
|
||||
normalizeLegendNameEn: (nameEn?: string) => string
|
||||
): Record<string, number> => {
|
||||
const nextState: Record<string, number> = {};
|
||||
|
||||
collectLegendLeaves(sourceData).forEach(item => {
|
||||
const normalizedNameEn = normalizeLegendNameEn(item?.nameEn);
|
||||
if (!normalizedNameEn) return;
|
||||
|
||||
const fallbackChecked = normalizeLegendCheckedValue(item?.checked) ?? 0;
|
||||
nextState[normalizedNameEn] = fallbackChecked;
|
||||
});
|
||||
|
||||
return nextState;
|
||||
};
|
||||
|
||||
// 备注:按当前选中图层和图例运行态生成可展示的图例树结构。
|
||||
export const buildLegendTree = ({
|
||||
items = [],
|
||||
selectedLayerCodes,
|
||||
getLegendChecked,
|
||||
normalizeLegendNameEn
|
||||
}: BuildLegendTreeOptions): any[] => {
|
||||
const buildLegendNode = (item: any): any | null => {
|
||||
if (item?.name === '地图') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item?.childrenList?.length > 0) {
|
||||
const children = item.childrenList
|
||||
.map((child: any) => buildLegendNode(child))
|
||||
.filter(Boolean);
|
||||
|
||||
if (selectedLayerCodes && children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
checked: children.some((child: any) => child.checked === 1) ? 1 : 0,
|
||||
childrenList: children
|
||||
};
|
||||
}
|
||||
|
||||
if (item?.ifShow === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLayerCodes &&
|
||||
(!item?.layerCode || !selectedLayerCodes.has(item.layerCode))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedNameEn = normalizeLegendNameEn(item?.nameEn);
|
||||
return {
|
||||
...item,
|
||||
nameEn: normalizedNameEn,
|
||||
checked: getLegendChecked(normalizedNameEn)
|
||||
};
|
||||
};
|
||||
|
||||
return items.map(item => buildLegendNode(item)).filter(Boolean);
|
||||
};
|
||||
|
||||
// 备注:按名称从图例树中提取指定分组,供环保设施补充规则复用。
|
||||
export const pickLegendGroupByName = (
|
||||
items: any[] = [],
|
||||
groupName: string
|
||||
): any[] => {
|
||||
return items.filter(item => item?.name === groupName);
|
||||
};
|
||||
|
||||
// 备注:统一应用环保设施图例补充规则,避免在多个位置重复拼装同一组图例。
|
||||
export const applyEnvFacilityLegendRule = (
|
||||
legendItems: any[] = [],
|
||||
layerKeys: string[] = [],
|
||||
legendSource: any[] = [],
|
||||
buildLegendItems: (items: any[]) => any[]
|
||||
): any[] => {
|
||||
const triggerKeys = [
|
||||
'fp_point',
|
||||
'eq_point',
|
||||
'fb_point',
|
||||
'vp_point',
|
||||
'va_point',
|
||||
'sg_point',
|
||||
'dw_point'
|
||||
];
|
||||
const hasTriggerLayer = triggerKeys.some(key => layerKeys.includes(key));
|
||||
const withoutEnvFacility = legendItems.filter(
|
||||
(item: any) => item.name !== '环保设施'
|
||||
);
|
||||
|
||||
if (!hasTriggerLayer) {
|
||||
return withoutEnvFacility;
|
||||
}
|
||||
|
||||
const envFacilityLegend = buildLegendItems(
|
||||
pickLegendGroupByName(legendSource, '环保设施')
|
||||
)[0];
|
||||
|
||||
if (!envFacilityLegend) {
|
||||
return withoutEnvFacility;
|
||||
}
|
||||
|
||||
return [...withoutEnvFacility, envFacilityLegend];
|
||||
};
|
||||
@ -1,80 +0,0 @@
|
||||
type ApplyLayerMutualExclusionRulesOptions = {
|
||||
rawKeys?: string[];
|
||||
previousKeys?: string[];
|
||||
triggerKey?: string;
|
||||
checked?: boolean;
|
||||
getLayerBranchKeys?: (rootKey: string) => string[];
|
||||
};
|
||||
|
||||
// 备注:统一按当前业务规则收口图层互斥关系,返回归一化后的勾选结果。
|
||||
export const applyLayerMutualExclusionRules = ({
|
||||
rawKeys = [],
|
||||
previousKeys = [],
|
||||
triggerKey,
|
||||
checked,
|
||||
getLayerBranchKeys
|
||||
}: ApplyLayerMutualExclusionRulesOptions): string[] => {
|
||||
let normalizedKeys = Array.from(new Set(rawKeys.filter(Boolean)));
|
||||
|
||||
const hasAny = (keys: string[]) =>
|
||||
keys.length > 0 && keys.some(key => normalizedKeys.includes(key));
|
||||
const hadAny = (keys: string[]) =>
|
||||
keys.length > 0 && keys.some(key => previousKeys.includes(key));
|
||||
const removeKeys = (keys: string[]) => {
|
||||
if (keys.length === 0) return;
|
||||
normalizedKeys = normalizedKeys.filter(key => !keys.includes(key));
|
||||
};
|
||||
|
||||
// 备注:统一处理两组图层的互斥关系,并优先保留当前触发的一组。
|
||||
const resolveConflict = (
|
||||
primaryKeys: string[],
|
||||
secondaryKeys: string[],
|
||||
preferredGroup: 'primary' | 'secondary' = 'primary'
|
||||
) => {
|
||||
if (!hasAny(primaryKeys) || !hasAny(secondaryKeys)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerKey && checked !== false) {
|
||||
if (primaryKeys.includes(triggerKey)) {
|
||||
removeKeys(secondaryKeys);
|
||||
return;
|
||||
}
|
||||
if (secondaryKeys.includes(triggerKey)) {
|
||||
removeKeys(primaryKeys);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const primaryWasChecked = hadAny(primaryKeys);
|
||||
const secondaryWasChecked = hadAny(secondaryKeys);
|
||||
|
||||
if (primaryWasChecked && !secondaryWasChecked) {
|
||||
removeKeys(secondaryKeys);
|
||||
return;
|
||||
}
|
||||
if (!primaryWasChecked && secondaryWasChecked) {
|
||||
removeKeys(primaryKeys);
|
||||
return;
|
||||
}
|
||||
|
||||
if (preferredGroup === 'primary') {
|
||||
removeKeys(secondaryKeys);
|
||||
} else {
|
||||
removeKeys(primaryKeys);
|
||||
}
|
||||
};
|
||||
|
||||
resolveConflict(['rare_fish_point'], ['fish_along_point'], 'primary');
|
||||
resolveConflict(
|
||||
['stinfo', 'stinfo_video_point'],
|
||||
['stinfo_ai_video_point'],
|
||||
'secondary'
|
||||
);
|
||||
|
||||
const facilityKeys = getLayerBranchKeys?.('facilities') || [];
|
||||
const facilityBuiltKeys = getLayerBranchKeys?.('facilities_built') || [];
|
||||
resolveConflict(facilityKeys, facilityBuiltKeys, 'primary');
|
||||
|
||||
return normalizedKeys;
|
||||
};
|
||||
@ -1,418 +0,0 @@
|
||||
export type NearbyPointDisplayMode = 'default' | 'replace' | 'expand';
|
||||
|
||||
export type NearbyPointAutoRule = {
|
||||
distanceThresholdMeters: number;
|
||||
replaceAtZoom: number;
|
||||
expandAtZoom: number;
|
||||
expandOffsetPx: number;
|
||||
angleStepDeg: number;
|
||||
minGroupSize: number;
|
||||
};
|
||||
|
||||
export type NearbyPointLayoutRule = {
|
||||
ringSize: number;
|
||||
fanAngleForTwoDeg: number;
|
||||
fanAngleForFourDeg: number;
|
||||
fanAngleForManyDeg: number;
|
||||
ringGapPx: number;
|
||||
ringGapFactor: number;
|
||||
};
|
||||
|
||||
export type NearbyPointDensityDisplayRule = {
|
||||
minDensityValue: number;
|
||||
minZoom: number;
|
||||
};
|
||||
|
||||
export type NearbyPointConfig = {
|
||||
autoRule: NearbyPointAutoRule;
|
||||
layoutRule: NearbyPointLayoutRule;
|
||||
densityDisplayRules: NearbyPointDensityDisplayRule[];
|
||||
};
|
||||
|
||||
export type NearbyPointRuntimeMeta = {
|
||||
groupId: string;
|
||||
displayName: string;
|
||||
groupSize: number;
|
||||
priority: number;
|
||||
showZoomMin: number | null;
|
||||
showZoomMax: number | null;
|
||||
replaceAtZoom: number;
|
||||
expandAtZoom: number;
|
||||
expandOffsetPx: number;
|
||||
expandAngleDeg: number;
|
||||
displayMode: NearbyPointDisplayMode;
|
||||
};
|
||||
|
||||
// 备注:近邻点统一调参入口。
|
||||
// 当前参数偏向“更容易成组、稍早进入替换与展开、展开位移更明显”,
|
||||
// 方便抽吸效果在常用浏览层级更早被感知。
|
||||
export const DEFAULT_NEARBY_POINT_AUTO_RULE: NearbyPointAutoRule = {
|
||||
distanceThresholdMeters: 9000,
|
||||
replaceAtZoom: 10.5,
|
||||
expandAtZoom: 12.2,
|
||||
expandOffsetPx: 48,
|
||||
angleStepDeg: 55,
|
||||
minGroupSize: 2
|
||||
};
|
||||
|
||||
// 备注:展开布局统一调参入口。这里控制单圈数量、扇形角度和外圈扩散节奏。
|
||||
export const DEFAULT_NEARBY_POINT_LAYOUT_RULE: NearbyPointLayoutRule = {
|
||||
ringSize: 5,
|
||||
fanAngleForTwoDeg: 110,
|
||||
fanAngleForFourDeg: 150,
|
||||
fanAngleForManyDeg: 180,
|
||||
ringGapPx: 22,
|
||||
ringGapFactor: 0.85
|
||||
};
|
||||
|
||||
// 备注:密度分档显示规则。distance 越大表示越稀疏,越早参与显示候选。
|
||||
export const DEFAULT_NEARBY_POINT_DENSITY_DISPLAY_RULES: NearbyPointDensityDisplayRule[] =
|
||||
[
|
||||
{
|
||||
minDensityValue: 1500000,
|
||||
minZoom: 0
|
||||
},
|
||||
{
|
||||
minDensityValue: 800000,
|
||||
minZoom: 0
|
||||
},
|
||||
{
|
||||
minDensityValue: 400000,
|
||||
minZoom: 0
|
||||
},
|
||||
{
|
||||
minDensityValue: 50000,
|
||||
minZoom: 2.0
|
||||
},
|
||||
{
|
||||
minDensityValue: 0,
|
||||
minZoom: 4.0
|
||||
}
|
||||
];
|
||||
|
||||
export const DEFAULT_NEARBY_POINT_CONFIG: NearbyPointConfig = {
|
||||
autoRule: DEFAULT_NEARBY_POINT_AUTO_RULE,
|
||||
layoutRule: DEFAULT_NEARBY_POINT_LAYOUT_RULE,
|
||||
densityDisplayRules: DEFAULT_NEARBY_POINT_DENSITY_DISPLAY_RULES
|
||||
};
|
||||
|
||||
export const getNearbyPointConfig = (): NearbyPointConfig => {
|
||||
return DEFAULT_NEARBY_POINT_CONFIG;
|
||||
};
|
||||
|
||||
export const getNearbyPointAutoRule = (): NearbyPointAutoRule => {
|
||||
return getNearbyPointConfig().autoRule;
|
||||
};
|
||||
|
||||
export const getNearbyPointLayoutRule = (): NearbyPointLayoutRule => {
|
||||
return getNearbyPointConfig().layoutRule;
|
||||
};
|
||||
|
||||
export const getNearbyPointDensityDisplayRules =
|
||||
(): NearbyPointDensityDisplayRule[] => {
|
||||
return getNearbyPointConfig().densityDisplayRules;
|
||||
};
|
||||
|
||||
type GroupCandidate = {
|
||||
index: number;
|
||||
point: Record<string, any>;
|
||||
lon: number;
|
||||
lat: number;
|
||||
layerKey: string;
|
||||
};
|
||||
|
||||
const isAlarmRangeLegendState = (legendState: string): boolean => {
|
||||
return (
|
||||
legendState.startsWith('alarm_range_') ||
|
||||
legendState.startsWith('large_eng_built_alarm_range_') ||
|
||||
legendState.startsWith('mid_eng_built_alarm_range_')
|
||||
);
|
||||
};
|
||||
|
||||
const getPointRenderPriority = (point: Record<string, any>): number => {
|
||||
const layerKey = String(point.layerKey || point.type || '').toLowerCase();
|
||||
const legendState = String(point.anchoPointState || '').trim().toLowerCase();
|
||||
|
||||
if (
|
||||
layerKey.includes('eng_alarm_point') ||
|
||||
layerKey.includes('alarm_range') ||
|
||||
isAlarmRangeLegendState(legendState)
|
||||
) {
|
||||
return 300;
|
||||
}
|
||||
|
||||
if (
|
||||
layerKey.includes('eng_point') ||
|
||||
legendState.startsWith('large_eng_') ||
|
||||
legendState.startsWith('mid_eng_')
|
||||
) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
return 100;
|
||||
};
|
||||
|
||||
const normalizeText = (value: unknown): string => {
|
||||
return String(value ?? '')
|
||||
.trim()
|
||||
.replace(/[()()]/g, '')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : null;
|
||||
};
|
||||
|
||||
const haversineDistanceMeters = (
|
||||
lon1: number,
|
||||
lat1: number,
|
||||
lon2: number,
|
||||
lat2: number
|
||||
): number => {
|
||||
const toRad = (degree: number) => (degree * Math.PI) / 180;
|
||||
const earthRadius = 6371000;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const lat1Rad = toRad(lat1);
|
||||
const lat2Rad = toRad(lat2);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1Rad) *
|
||||
Math.cos(lat2Rad) *
|
||||
Math.sin(dLon / 2) *
|
||||
Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return earthRadius * c;
|
||||
};
|
||||
|
||||
const clearNearbyPointRuntimeMeta = (point: Record<string, any>) => {
|
||||
delete point._nearbyRule;
|
||||
delete point._nearbyGroupId;
|
||||
delete point._nearbyDisplayName;
|
||||
delete point._nearbyGroupSize;
|
||||
delete point._nearbyPriority;
|
||||
delete point._nearbyShowZoomMin;
|
||||
delete point._nearbyShowZoomMax;
|
||||
delete point._nearbyReplaceAtZoom;
|
||||
delete point._nearbyExpandAtZoom;
|
||||
delete point._nearbyExpandOffsetPx;
|
||||
delete point._nearbyExpandAngleDeg;
|
||||
delete point._nearbyDisplayMode;
|
||||
delete point._nearbyIsPrimary;
|
||||
};
|
||||
|
||||
const getPointDisplayName = (point: Record<string, any>): string => {
|
||||
return (
|
||||
point.titleName || point.stnm || point.ennm || point.stcd || point._id || ''
|
||||
);
|
||||
};
|
||||
|
||||
const getPointStableIdentity = (
|
||||
point: Record<string, any>,
|
||||
index: number
|
||||
): string => {
|
||||
return normalizeText(
|
||||
point.stcd ||
|
||||
point._id ||
|
||||
point.titleName ||
|
||||
point.stnm ||
|
||||
point.ennm ||
|
||||
`point_${index}`
|
||||
);
|
||||
};
|
||||
|
||||
const getGroupSortKey = (candidate: GroupCandidate): string => {
|
||||
return [
|
||||
getPointStableIdentity(candidate.point, candidate.index),
|
||||
normalizeText(candidate.point.layerKey),
|
||||
String(candidate.index).padStart(6, '0')
|
||||
].join('|');
|
||||
};
|
||||
|
||||
const compareGroupCandidates = (
|
||||
left: GroupCandidate,
|
||||
right: GroupCandidate
|
||||
): number => {
|
||||
const leftRenderPriority = getPointRenderPriority(left.point);
|
||||
const rightRenderPriority = getPointRenderPriority(right.point);
|
||||
if (leftRenderPriority !== rightRenderPriority) {
|
||||
return rightRenderPriority - leftRenderPriority;
|
||||
}
|
||||
|
||||
const leftDensityValue = toFiniteNumber(left.point.distance);
|
||||
const rightDensityValue = toFiniteNumber(right.point.distance);
|
||||
|
||||
if (
|
||||
leftDensityValue !== null &&
|
||||
rightDensityValue !== null &&
|
||||
leftDensityValue !== rightDensityValue
|
||||
) {
|
||||
return rightDensityValue - leftDensityValue;
|
||||
}
|
||||
|
||||
if (leftDensityValue !== null || rightDensityValue !== null) {
|
||||
return leftDensityValue !== null ? -1 : 1;
|
||||
}
|
||||
|
||||
return getGroupSortKey(left).localeCompare(getGroupSortKey(right));
|
||||
};
|
||||
|
||||
const buildRuntimeMeta = (
|
||||
groupId: string,
|
||||
groupSize: number,
|
||||
priority: number,
|
||||
displayName: string,
|
||||
rule: NearbyPointAutoRule
|
||||
): NearbyPointRuntimeMeta => {
|
||||
return {
|
||||
groupId,
|
||||
displayName,
|
||||
groupSize,
|
||||
priority,
|
||||
showZoomMin: null,
|
||||
showZoomMax: null,
|
||||
replaceAtZoom: rule.replaceAtZoom,
|
||||
expandAtZoom: rule.expandAtZoom,
|
||||
expandOffsetPx: rule.expandOffsetPx,
|
||||
expandAngleDeg: 0,
|
||||
displayMode: 'default'
|
||||
};
|
||||
};
|
||||
|
||||
const buildGroupCandidates = (
|
||||
points: Record<string, any>[] = []
|
||||
): GroupCandidate[] => {
|
||||
const candidates: GroupCandidate[] = [];
|
||||
|
||||
points.forEach((point, index) => {
|
||||
const lon = toFiniteNumber(point.lgtd);
|
||||
const lat = toFiniteNumber(point.lttd);
|
||||
if (lon === null || lat === null) {
|
||||
clearNearbyPointRuntimeMeta(point);
|
||||
return;
|
||||
}
|
||||
if (Math.abs(lon) > 180 || Math.abs(lat) > 90) {
|
||||
clearNearbyPointRuntimeMeta(point);
|
||||
return;
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
index,
|
||||
point,
|
||||
lon,
|
||||
lat,
|
||||
layerKey: String(point.layerKey || '')
|
||||
});
|
||||
});
|
||||
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const buildNearbyGroups = (
|
||||
candidates: GroupCandidate[],
|
||||
rule: NearbyPointAutoRule
|
||||
): GroupCandidate[][] => {
|
||||
const result: GroupCandidate[][] = [];
|
||||
const sortedCandidates = [...candidates].sort(compareGroupCandidates);
|
||||
const consumedIndexes = new Set<number>();
|
||||
|
||||
sortedCandidates.forEach((seedCandidate, seedIndex) => {
|
||||
if (consumedIndexes.has(seedIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupItems: GroupCandidate[] = [seedCandidate];
|
||||
const groupIndexes = [seedIndex];
|
||||
|
||||
for (
|
||||
let candidateIndex = seedIndex + 1;
|
||||
candidateIndex < sortedCandidates.length;
|
||||
candidateIndex += 1
|
||||
) {
|
||||
if (consumedIndexes.has(candidateIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentCandidate = sortedCandidates[candidateIndex];
|
||||
const distanceMeters = haversineDistanceMeters(
|
||||
seedCandidate.lon,
|
||||
seedCandidate.lat,
|
||||
currentCandidate.lon,
|
||||
currentCandidate.lat
|
||||
);
|
||||
|
||||
if (distanceMeters <= rule.distanceThresholdMeters) {
|
||||
groupItems.push(currentCandidate);
|
||||
groupIndexes.push(candidateIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupItems.length >= rule.minGroupSize) {
|
||||
groupIndexes.forEach(index => consumedIndexes.add(index));
|
||||
result.push(groupItems);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 备注:统一按空间距离自动识别近邻点组,不按名称或点类型分规则。
|
||||
export const attachNearbyPointRuntimeMeta = (
|
||||
points: Record<string, any>[] = [],
|
||||
rule: NearbyPointAutoRule = getNearbyPointAutoRule()
|
||||
): Record<string, any>[] => {
|
||||
if (!Array.isArray(points) || points.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
points.forEach(point => clearNearbyPointRuntimeMeta(point));
|
||||
|
||||
const candidates = buildGroupCandidates(points);
|
||||
if (candidates.length < rule.minGroupSize) {
|
||||
return points;
|
||||
}
|
||||
const groupedCandidates = buildNearbyGroups(candidates, rule);
|
||||
|
||||
let groupSeed = 0;
|
||||
groupedCandidates.forEach(groupItems => {
|
||||
if (groupItems.length < rule.minGroupSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedItems = [...groupItems].sort(compareGroupCandidates);
|
||||
groupSeed += 1;
|
||||
const groupId = `nearby-group-${groupSeed}`;
|
||||
|
||||
sortedItems.forEach((candidate, orderIndex) => {
|
||||
const priority = orderIndex + 1;
|
||||
const runtimeMeta = buildRuntimeMeta(
|
||||
groupId,
|
||||
sortedItems.length,
|
||||
priority,
|
||||
getPointDisplayName(candidate.point),
|
||||
rule
|
||||
);
|
||||
|
||||
candidate.point._nearbyRule = runtimeMeta;
|
||||
candidate.point._nearbyGroupId = runtimeMeta.groupId;
|
||||
candidate.point._nearbyDisplayName = runtimeMeta.displayName;
|
||||
candidate.point._nearbyGroupSize = runtimeMeta.groupSize;
|
||||
candidate.point._nearbyPriority = runtimeMeta.priority;
|
||||
candidate.point._nearbyShowZoomMin = runtimeMeta.showZoomMin;
|
||||
candidate.point._nearbyShowZoomMax = runtimeMeta.showZoomMax;
|
||||
candidate.point._nearbyReplaceAtZoom = runtimeMeta.replaceAtZoom;
|
||||
candidate.point._nearbyExpandAtZoom = runtimeMeta.expandAtZoom;
|
||||
candidate.point._nearbyExpandOffsetPx = runtimeMeta.expandOffsetPx;
|
||||
candidate.point._nearbyExpandAngleDeg = runtimeMeta.expandAngleDeg;
|
||||
candidate.point._nearbyDisplayMode = runtimeMeta.displayMode;
|
||||
candidate.point._nearbyIsPrimary = runtimeMeta.priority === 1;
|
||||
});
|
||||
});
|
||||
|
||||
return points;
|
||||
};
|
||||
@ -1,251 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { getMapList, getModuleMapLegendList } from '@/api/map';
|
||||
|
||||
type MapConfigLoadOptions = {
|
||||
systemId: string;
|
||||
moduleId: string;
|
||||
pageKey?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type LegendConfigLoadOptions = {
|
||||
includeGlobal?: boolean;
|
||||
};
|
||||
|
||||
const cloneConfigData = <T>(data: T): T => {
|
||||
return JSON.parse(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const useMapConfigStore = defineStore('map-config', () => {
|
||||
const layerConfigTree = ref<any[]>([]);
|
||||
const layerConfigByKey = ref<Record<string, any>>({});
|
||||
const legendConfigOriginal = ref<any[]>([]);
|
||||
const legendConfigByNameEn = ref<Record<string, any>>({});
|
||||
const legendConfigByLayerCode = ref<Record<string, any[]>>({});
|
||||
const pageLegendConfig = ref<any[]>([]);
|
||||
const configLoading = ref(false);
|
||||
const legendLoading = ref(false);
|
||||
const lastLoadOptions = ref<MapConfigLoadOptions | null>(null);
|
||||
|
||||
const normalizeLegendNameEn = (nameEn?: string): string => {
|
||||
if (!nameEn) return '';
|
||||
return nameEn;
|
||||
};
|
||||
|
||||
// 备注:重建图层配置索引,后续按 key 读取图层配置时不再全量递归。
|
||||
const rebuildLayerConfigIndex = (items: any[] = []) => {
|
||||
const nextIndex: Record<string, any> = {};
|
||||
|
||||
const walk = (nodes: any[] = []) => {
|
||||
nodes.forEach(item => {
|
||||
if (item?.key) {
|
||||
nextIndex[item.key] = item;
|
||||
}
|
||||
if (item?.children?.length > 0) {
|
||||
walk(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(items);
|
||||
layerConfigByKey.value = nextIndex;
|
||||
};
|
||||
|
||||
// 备注:从图层配置中提取默认勾选项,作为页面初始化时的基础勾选状态。
|
||||
const extractCheckedLayerKeys = (items: any[] = []): string[] => {
|
||||
const keys: string[] = [];
|
||||
|
||||
const walk = (nodes: any[] = []) => {
|
||||
nodes.forEach(item => {
|
||||
if (Number(item?.checked) === 1 && item?.key) {
|
||||
keys.push(item.key);
|
||||
}
|
||||
if (item?.children?.length > 0) {
|
||||
walk(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(items);
|
||||
return keys;
|
||||
};
|
||||
|
||||
// 备注:重建图例配置索引,分别支持按 nameEn 和 layerCode 查询图例配置。
|
||||
const rebuildLegendConfigIndexes = (items: any[] = []) => {
|
||||
const nextNameEnMap: Record<string, any> = {};
|
||||
const nextLayerCodeMap: Record<string, any[]> = {};
|
||||
|
||||
const walk = (nodes: any[] = []) => {
|
||||
nodes.forEach(item => {
|
||||
if (item?.childrenList?.length > 0) {
|
||||
walk(item.childrenList);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedNameEn = normalizeLegendNameEn(item?.nameEn);
|
||||
if (normalizedNameEn) {
|
||||
nextNameEnMap[normalizedNameEn] = item;
|
||||
}
|
||||
|
||||
if (item?.layerCode) {
|
||||
if (!nextLayerCodeMap[item.layerCode]) {
|
||||
nextLayerCodeMap[item.layerCode] = [];
|
||||
}
|
||||
nextLayerCodeMap[item.layerCode].push(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(items);
|
||||
legendConfigByNameEn.value = nextNameEnMap;
|
||||
legendConfigByLayerCode.value = nextLayerCodeMap;
|
||||
};
|
||||
|
||||
// 备注:设置图层树配置,并同步初始化图层索引与默认勾选状态。
|
||||
const setLayerConfigTree = (data: any[] = []) => {
|
||||
const nextData = cloneConfigData(data);
|
||||
layerConfigTree.value = nextData;
|
||||
rebuildLayerConfigIndex(nextData);
|
||||
};
|
||||
|
||||
// 备注:设置全量原始图例配置,并同步构建图例索引。
|
||||
const setLegendConfigOriginal = (data: any[] = []) => {
|
||||
const nextData = cloneConfigData(data);
|
||||
legendConfigOriginal.value = nextData;
|
||||
rebuildLegendConfigIndexes(nextData);
|
||||
};
|
||||
|
||||
// 备注:设置页面级图例配置,仅保留后端返回的页面图例结构,不在这里做运行态加工。
|
||||
const setPageLegendConfig = (data: any[] = []) => {
|
||||
pageLegendConfig.value = cloneConfigData(data);
|
||||
};
|
||||
|
||||
// 备注:按图层 key 获取图层配置,供后续编排层和运行态 store 复用。
|
||||
const getLayerConfigByKey = (layerKey: string) => {
|
||||
return layerConfigByKey.value[layerKey];
|
||||
};
|
||||
|
||||
// 备注:按图层 layerCode 获取原始图例项列表,供图层勾选后派生图例使用。
|
||||
const getLegendConfigByLayerCode = (layerCode: string): any[] => {
|
||||
return legendConfigByLayerCode.value[layerCode] || [];
|
||||
};
|
||||
|
||||
// 备注:按图例 nameEn 获取原始图例项,供运行态读取默认 checked 配置。
|
||||
const getLegendConfigByNameEn = (nameEn: string) => {
|
||||
const normalizedNameEn = normalizeLegendNameEn(nameEn);
|
||||
return legendConfigByNameEn.value[normalizedNameEn];
|
||||
};
|
||||
|
||||
// 备注:统一清理配置 store,便于菜单切换或地图销毁后重置配置状态。
|
||||
const clearConfigState = () => {
|
||||
layerConfigTree.value = [];
|
||||
layerConfigByKey.value = {};
|
||||
legendConfigOriginal.value = [];
|
||||
legendConfigByNameEn.value = {};
|
||||
legendConfigByLayerCode.value = {};
|
||||
pageLegendConfig.value = [];
|
||||
configLoading.value = false;
|
||||
legendLoading.value = false;
|
||||
lastLoadOptions.value = null;
|
||||
};
|
||||
|
||||
const loadPageLayerConfig = async (options: MapConfigLoadOptions) => {
|
||||
configLoading.value = true;
|
||||
lastLoadOptions.value = { ...options };
|
||||
|
||||
try {
|
||||
const layerRes = await getMapList({
|
||||
systemId: options.systemId,
|
||||
moduleId: options.moduleId,
|
||||
description: options.description ?? 'true'
|
||||
});
|
||||
|
||||
const layerConfig = layerRes?.data?.mapLayerVos || [];
|
||||
setLayerConfigTree(layerConfig);
|
||||
return {
|
||||
layerConfig
|
||||
};
|
||||
} finally {
|
||||
configLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPageLegendConfig = async (
|
||||
pageKey?: string,
|
||||
options: LegendConfigLoadOptions = {}
|
||||
) => {
|
||||
legendLoading.value = true;
|
||||
|
||||
try {
|
||||
const includeGlobal = options.includeGlobal !== false;
|
||||
|
||||
let legendOriginal = legendConfigOriginal.value || [];
|
||||
let pageLegend: any[] = [];
|
||||
|
||||
if (includeGlobal) {
|
||||
const [legendAllRes, legendPageRes] = await Promise.all([
|
||||
getModuleMapLegendList(),
|
||||
getModuleMapLegendList(pageKey ? { moduleId: pageKey } : undefined)
|
||||
]);
|
||||
|
||||
legendOriginal = legendAllRes?.data || [];
|
||||
pageLegend = legendPageRes?.data || [];
|
||||
} else {
|
||||
const legendPageRes = await getModuleMapLegendList(
|
||||
pageKey ? { moduleId: pageKey } : undefined
|
||||
);
|
||||
pageLegend = legendPageRes?.data || [];
|
||||
}
|
||||
|
||||
setLegendConfigOriginal(legendOriginal);
|
||||
setPageLegendConfig(pageLegend);
|
||||
|
||||
return {
|
||||
legendOriginal,
|
||||
pageLegend
|
||||
};
|
||||
} finally {
|
||||
legendLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 备注:加载页面地图配置,只负责请求和保存配置数据,不处理锚点或地图渲染逻辑。
|
||||
const loadPageMapConfig = async (options: MapConfigLoadOptions) => {
|
||||
const [{ layerConfig }, { legendOriginal, pageLegend }] = await Promise.all(
|
||||
[loadPageLayerConfig(options), loadPageLegendConfig(options.pageKey)]
|
||||
);
|
||||
|
||||
return {
|
||||
layerConfig,
|
||||
legendOriginal,
|
||||
pageLegend
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
layerConfigTree,
|
||||
layerConfigByKey,
|
||||
legendConfigOriginal,
|
||||
legendConfigByNameEn,
|
||||
legendConfigByLayerCode,
|
||||
pageLegendConfig,
|
||||
configLoading,
|
||||
legendLoading,
|
||||
lastLoadOptions,
|
||||
normalizeLegendNameEn,
|
||||
rebuildLayerConfigIndex,
|
||||
extractCheckedLayerKeys,
|
||||
rebuildLegendConfigIndexes,
|
||||
setLayerConfigTree,
|
||||
setLegendConfigOriginal,
|
||||
setPageLegendConfig,
|
||||
getLayerConfigByKey,
|
||||
getLegendConfigByLayerCode,
|
||||
getLegendConfigByNameEn,
|
||||
clearConfigState,
|
||||
loadPageLayerConfig,
|
||||
loadPageLegendConfig,
|
||||
loadPageMapConfig
|
||||
};
|
||||
});
|
||||
@ -1,192 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
type PointLayerCacheItem = {
|
||||
checked: boolean;
|
||||
data: any[];
|
||||
cacheKey?: string;
|
||||
};
|
||||
|
||||
type LayerLoadStatus = {
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export const useMapDataStore = defineStore('map-data', () => {
|
||||
const pointData = ref<any[]>([]);
|
||||
const pointDataCache = ref<Record<string, PointLayerCacheItem>>({});
|
||||
const layerLoadState = ref<Record<string, LayerLoadStatus>>({});
|
||||
const loading = ref(true);
|
||||
|
||||
// 备注:统一设置地图模块整体 loading 状态,供图例和筛选组件复用。
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
|
||||
// 备注:整体清空锚点合并数据,通常用于菜单切换或重新初始化。
|
||||
const clearPointData = () => {
|
||||
pointData.value = [];
|
||||
};
|
||||
|
||||
// 备注:直接覆盖锚点合并数据,供地图加载完成后统一回写。
|
||||
const setPointData = (data: any[] = []) => {
|
||||
pointData.value = data;
|
||||
};
|
||||
|
||||
// 备注:读取指定图层缓存,供地图渲染逻辑优先命中缓存数据。
|
||||
const getPointLayerCache = (layerKey: string) => {
|
||||
return pointDataCache.value[layerKey];
|
||||
};
|
||||
|
||||
// 备注:判断单个图层缓存是否与当前请求条件匹配,避免旧筛选结果直接复用。
|
||||
const hasValidPointLayerCache = (layerKey: string, cacheKey: string) => {
|
||||
const cache = getPointLayerCache(layerKey);
|
||||
if (!cache || !cacheKey) return false;
|
||||
return cache.cacheKey === cacheKey && cache.data.length > 0;
|
||||
};
|
||||
|
||||
// 备注:读取单个图层的缓存数据数组,未命中时返回空数组,简化外层判空逻辑。
|
||||
const getPointLayerData = (layerKey: string): any[] => {
|
||||
return pointDataCache.value[layerKey]?.data || [];
|
||||
};
|
||||
|
||||
// 备注:判断单个图层缓存是否已具备可用数据,供加载和显隐逻辑快速分支。
|
||||
const hasPointLayerData = (layerKey: string): boolean => {
|
||||
return getPointLayerData(layerKey).length > 0;
|
||||
};
|
||||
|
||||
// 备注:写入单个图层的锚点缓存,并保留当前是否勾选的状态。
|
||||
const setPointLayerCache = (layerKey: string, cache: PointLayerCacheItem) => {
|
||||
if (!layerKey) return;
|
||||
pointDataCache.value[layerKey] = cache;
|
||||
};
|
||||
|
||||
// 备注:只更新缓存中的勾选状态,避免重复覆盖整份锚点数据。
|
||||
const setPointLayerCacheChecked = (layerKey: string, checked: boolean) => {
|
||||
const cache = pointDataCache.value[layerKey];
|
||||
if (!cache) return;
|
||||
setPointLayerCache(layerKey, {
|
||||
...cache,
|
||||
checked
|
||||
});
|
||||
};
|
||||
|
||||
// 备注:删除指定图层缓存,供时间筛选重载和图层强制刷新使用。
|
||||
const removePointLayerCache = (layerKey: string) => {
|
||||
if (!pointDataCache.value[layerKey]) return;
|
||||
const nextCache = { ...pointDataCache.value };
|
||||
delete nextCache[layerKey];
|
||||
pointDataCache.value = nextCache;
|
||||
};
|
||||
|
||||
// 备注:清空全部图层缓存,供页面切换或地图销毁时复用。
|
||||
const clearPointLayerCache = () => {
|
||||
pointDataCache.value = {};
|
||||
};
|
||||
|
||||
// 备注:根据当前缓存重建合并锚点数据,避免在多个地方手写拼装逻辑。
|
||||
const rebuildPointDataFromCache = (layerKeys: string[] = []) => {
|
||||
const targetLayerKeys =
|
||||
layerKeys.length > 0 ? layerKeys : Object.keys(pointDataCache.value);
|
||||
const allPointData: any[] = [];
|
||||
|
||||
targetLayerKeys.forEach(layerKey => {
|
||||
const cache = pointDataCache.value[layerKey];
|
||||
if (!cache?.data?.length) return;
|
||||
|
||||
cache.data.forEach((point: any) => {
|
||||
if (point?.layerKey !== layerKey) {
|
||||
point.layerKey = layerKey;
|
||||
}
|
||||
allPointData.push(point);
|
||||
});
|
||||
});
|
||||
|
||||
pointData.value = allPointData;
|
||||
return allPointData;
|
||||
};
|
||||
|
||||
// 备注:统一初始化单个图层的加载状态,避免外层重复判空。
|
||||
const ensureLayerLoadState = (layerKey: string): LayerLoadStatus => {
|
||||
if (!layerLoadState.value[layerKey]) {
|
||||
layerLoadState.value[layerKey] = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return layerLoadState.value[layerKey];
|
||||
};
|
||||
|
||||
// 备注:标记单个图层进入加载中状态。
|
||||
const setLayerLoading = (layerKey: string) => {
|
||||
const current = ensureLayerLoadState(layerKey);
|
||||
layerLoadState.value[layerKey] = {
|
||||
...current,
|
||||
loading: true,
|
||||
error: null
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:标记单个图层加载成功,供后续判断是否已完成首次加载。
|
||||
const setLayerLoaded = (layerKey: string) => {
|
||||
const current = ensureLayerLoadState(layerKey);
|
||||
layerLoadState.value[layerKey] = {
|
||||
...current,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
error: null
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:标记单个图层加载失败,并记录错误信息。
|
||||
const setLayerLoadError = (layerKey: string, error: unknown) => {
|
||||
const current = ensureLayerLoadState(layerKey);
|
||||
layerLoadState.value[layerKey] = {
|
||||
...current,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:清理指定图层的加载状态,供重载场景复用。
|
||||
const clearLayerLoadState = (layerKey: string) => {
|
||||
if (!layerLoadState.value[layerKey]) return;
|
||||
delete layerLoadState.value[layerKey];
|
||||
};
|
||||
|
||||
// 备注:整体重置数据缓存 store,供菜单切换或地图销毁时调用。
|
||||
const resetDataState = () => {
|
||||
pointData.value = [];
|
||||
pointDataCache.value = {};
|
||||
layerLoadState.value = {};
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
return {
|
||||
pointData,
|
||||
pointDataCache,
|
||||
layerLoadState,
|
||||
loading,
|
||||
setLoading,
|
||||
clearPointData,
|
||||
setPointData,
|
||||
getPointLayerCache,
|
||||
hasValidPointLayerCache,
|
||||
getPointLayerData,
|
||||
hasPointLayerData,
|
||||
setPointLayerCache,
|
||||
setPointLayerCacheChecked,
|
||||
removePointLayerCache,
|
||||
clearPointLayerCache,
|
||||
rebuildPointDataFromCache,
|
||||
ensureLayerLoadState,
|
||||
setLayerLoading,
|
||||
setLayerLoaded,
|
||||
setLayerLoadError,
|
||||
clearLayerLoadState,
|
||||
resetDataState
|
||||
};
|
||||
});
|
||||
@ -1,92 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const useMapViewStore = defineStore('map-view', () => {
|
||||
const checkedLayerKeys = ref<string[]>([]);
|
||||
const legendCheckedState = ref<Record<string, number>>({});
|
||||
const searchTimeRange = ref<[any, any]>([dayjs().subtract(1, 'M'), dayjs()]);
|
||||
const selectedBaseId = ref('');
|
||||
const currentZoomLevel = ref(4.5);
|
||||
|
||||
// 备注:统一写入当前选中的图层 key,始终保持去重后的结果。
|
||||
const setCheckedLayerKeys = (keys: string[] = []) => {
|
||||
checkedLayerKeys.value = Array.from(new Set(keys.filter(Boolean)));
|
||||
};
|
||||
|
||||
// 备注:获取当前选中的图层 key,供图层树、动态图层和筛选联动复用。
|
||||
const getCheckedLayerKeys = (): string[] => {
|
||||
return checkedLayerKeys.value;
|
||||
};
|
||||
|
||||
// 备注:覆盖写入完整图例运行态勾选状态,适合图例初始化和整批替换。
|
||||
const setLegendCheckedState = (state: Record<string, number> = {}) => {
|
||||
legendCheckedState.value = { ...state };
|
||||
};
|
||||
|
||||
// 备注:写入单个图例运行态勾选状态,供点击图例项时复用。
|
||||
const setLegendChecked = (nameEn: string, checked: number) => {
|
||||
if (!nameEn) return;
|
||||
legendCheckedState.value = {
|
||||
...legendCheckedState.value,
|
||||
[nameEn]: checked
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:批量写入多个图例运行态勾选状态,供分组图例和筛选联动复用。
|
||||
const setLegendCheckedBatch = (
|
||||
stateMap: Record<string, number> = {}
|
||||
) => {
|
||||
legendCheckedState.value = {
|
||||
...legendCheckedState.value,
|
||||
...stateMap
|
||||
};
|
||||
};
|
||||
|
||||
// 备注:获取单个图例当前运行态勾选状态,未命中时默认返回 0。
|
||||
const getLegendChecked = (nameEn: string): number => {
|
||||
return legendCheckedState.value[nameEn] ?? 0;
|
||||
};
|
||||
|
||||
// 备注:统一更新当前时间筛选范围,供筛选组件和地图数据重载复用。
|
||||
const setSearchTimeRange = (range: [any, any]) => {
|
||||
searchTimeRange.value = range;
|
||||
};
|
||||
|
||||
// 备注:统一更新当前基地 ID,供地图筛选和基地联动复用。
|
||||
const setSelectedBaseId = (baseId: string) => {
|
||||
selectedBaseId.value = baseId || '';
|
||||
};
|
||||
|
||||
// 备注:统一更新当前地图缩放级别,供动态图层联动复用。
|
||||
const setCurrentZoomLevel = (zoom: number) => {
|
||||
currentZoomLevel.value = zoom;
|
||||
};
|
||||
|
||||
// 备注:在菜单切换或地图销毁时重置运行态到默认值。
|
||||
const resetViewState = () => {
|
||||
checkedLayerKeys.value = [];
|
||||
legendCheckedState.value = {};
|
||||
searchTimeRange.value = [dayjs().subtract(1, 'M'), dayjs()];
|
||||
selectedBaseId.value = '';
|
||||
currentZoomLevel.value = 4.5;
|
||||
};
|
||||
|
||||
return {
|
||||
checkedLayerKeys,
|
||||
legendCheckedState,
|
||||
searchTimeRange,
|
||||
selectedBaseId,
|
||||
currentZoomLevel,
|
||||
setCheckedLayerKeys,
|
||||
getCheckedLayerKeys,
|
||||
setLegendCheckedState,
|
||||
setLegendChecked,
|
||||
setLegendCheckedBatch,
|
||||
getLegendChecked,
|
||||
setSearchTimeRange,
|
||||
setSelectedBaseId,
|
||||
setCurrentZoomLevel,
|
||||
resetViewState
|
||||
};
|
||||
});
|
||||
@ -31,6 +31,7 @@ export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
|
||||
const list = [...res.data];
|
||||
// 直接赋值给 ref,触发响应式更新
|
||||
baseOption.value = list;
|
||||
// debugger
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取水电基地列表失败:', error);
|
||||
|
||||
@ -174,7 +174,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<!-- <a-col :span="24">
|
||||
<a-form-item label="简介" name="introduce" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
v-model:value="formData.introduce"
|
||||
@ -184,7 +184,7 @@
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-col> -->
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
|
||||
@ -2,7 +2,6 @@ import { UserConfig, ConfigEnv, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import path from 'path';
|
||||
import cesium from 'vite-plugin-cesium';
|
||||
|
||||
export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
// 获取 .env 环境配置文件
|
||||
@ -11,7 +10,6 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
cesium(),
|
||||
createSvgIconsPlugin({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
|
||||
@ -122,7 +120,6 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
if (id.includes('element-plus')) return 'element-plus';
|
||||
if (id.includes('ant-design-vue')) return 'ant-design-vue';
|
||||
if (id.includes('lodash')) return 'lodash';
|
||||
if (id.includes('cesium')) return 'cesium';
|
||||
// 其他 node_modules 归为 vendor
|
||||
return 'vendor';
|
||||
}
|
||||
|
||||
@ -326,3 +326,156 @@ export function deleteFlowStationInfo(data) {
|
||||
data
|
||||
});
|
||||
}
|
||||
// 水温监测 - 分页列表
|
||||
export function getWtPageList(data) {
|
||||
return request({
|
||||
url: '/base/wt/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 水质监测 - 分页列表
|
||||
export function getWqPageList(data) {
|
||||
return request({
|
||||
url: '/base/wq/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 生态流量 - 分页列表
|
||||
export function getEqPageList(data) {
|
||||
return request({
|
||||
url: '/base/eq/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 低温水减缓设施 - 分页列表
|
||||
export function getDwPageList(data) {
|
||||
return request({
|
||||
url: '/base/dw/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 栖息地 - 分页列表
|
||||
export function getFhPageList(data) {
|
||||
return request({
|
||||
url: '/base/fhbt/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 珍稀植物园 - 分页列表
|
||||
export function getVpPageList(data) {
|
||||
return request({
|
||||
url: '/base/vp/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 动物救助站 - 分页列表
|
||||
export function getVaPageList(data) {
|
||||
return request({
|
||||
url: '/base/va/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 过鱼设施 - 分页列表
|
||||
export function getFpPageList(data) {
|
||||
return request({
|
||||
url: '/env/fpss/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 水生生态 - 分页列表
|
||||
export function getWePageList(data) {
|
||||
return request({
|
||||
url: '/base/we/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 鱼类调查 - 分页列表
|
||||
export function getAiPageList(data) {
|
||||
return request({
|
||||
url: '/base/aimonitor/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 智能告警 - 分页列表
|
||||
export function getWarnPageList(data) {
|
||||
return request({
|
||||
url: '/base/warnRule/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// AI边缘计算盒子 - 分页列表
|
||||
export function getAiboxPageList(data) {
|
||||
return request({
|
||||
url: '/base/aibox/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 人工产卵场 - 分页列表
|
||||
export function getSgPageList(data) {
|
||||
return request({
|
||||
url: '/base/artsg/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 其他水生生态保护 - 分页列表
|
||||
export function getOtwePageList(data) {
|
||||
return request({
|
||||
url: '/base/otwe/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 陆生生态 - 分页列表
|
||||
export function getTePageList(data) {
|
||||
return request({
|
||||
url: '/base/te/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 其他陆生生态保护 - 分页列表
|
||||
export function getOttePageList(data) {
|
||||
return request({
|
||||
url: '/base/otte/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 视频监控 - 分页列表
|
||||
export function getVdPageList(data) {
|
||||
return request({
|
||||
url: '/base/vdinfo/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 鱼类增殖站 - 分页列表
|
||||
export function getFbPageList(data) {
|
||||
return request({
|
||||
url: '/base/fbrd/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 声呐及水下摄像头 - 分页列表
|
||||
export function getSonarPageList(data) {
|
||||
return request({
|
||||
url: '/base/sonar/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
@ -668,7 +668,19 @@ const enhancedColumns = computed(() => {
|
||||
title: String(text)
|
||||
},
|
||||
{
|
||||
default: () => String(text)
|
||||
default: () => h(
|
||||
'span',
|
||||
{
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
},
|
||||
String(text)
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -36,7 +36,6 @@ defineOptions({
|
||||
const emit = defineEmits<{
|
||||
(e: 'row-click', record: any): void;
|
||||
}>();
|
||||
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const baseid = ref('');
|
||||
const dataLoading = ref(false);
|
||||
@ -169,7 +168,6 @@ const getData = async () => {
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const result = { total: 0, buildinstall: 0, ubuildinstall: 0, nbuildinstall: 0 };
|
||||
list.forEach((e: any) => {
|
||||
result.total += Number(e.total) * 100;
|
||||
@ -180,16 +178,15 @@ const getData = async () => {
|
||||
const { total, nbuildinstall, ubuildinstall, buildinstall } = newList[e.key];
|
||||
newList[e.key] = {
|
||||
...e,
|
||||
total: Number(total) + Number(e.total),
|
||||
nbuildinstall: Number(nbuildinstall) + Number(e.nbuildinstall),
|
||||
ubuildinstall: Number(ubuildinstall) + Number(e.ubuildinstall),
|
||||
buildinstall: Number(buildinstall) + Number(e.buildinstall)
|
||||
total: (Number(total) + Number(e.total)).toFixed(2),
|
||||
nbuildinstall: (Number(nbuildinstall) + Number(e.nbuildinstall)).toFixed(2),
|
||||
ubuildinstall: (Number(ubuildinstall) + Number(e.ubuildinstall)).toFixed(2),
|
||||
buildinstall: (Number(buildinstall) + Number(e.buildinstall)).toFixed(2)
|
||||
};
|
||||
} else {
|
||||
newList[e.key] = e;
|
||||
}
|
||||
});
|
||||
|
||||
const { total, buildinstall, ubuildinstall, nbuildinstall } = result;
|
||||
const arr: any[] = keyList.map((key) => newList[key]);
|
||||
tableData.value = [
|
||||
@ -202,6 +199,7 @@ const getData = async () => {
|
||||
nbuildinstall: (nbuildinstall / 100)?.toFixed(2)
|
||||
}
|
||||
];
|
||||
|
||||
} else {
|
||||
tableData.value = [];
|
||||
}
|
||||
|
||||
@ -342,7 +342,7 @@ const openUrl = (url:string)=>{
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -27,25 +27,6 @@
|
||||
}"
|
||||
:list-url="getPowerTableList"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleDetail(record)"
|
||||
>查看详情</a-button
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 自定义数据列 Modal -->
|
||||
@ -281,7 +262,7 @@ const exportBtn = () => {
|
||||
const customFilterBtn = () => {
|
||||
customColumnVisible.value = true;
|
||||
// 打开弹窗前刷新列配置数据
|
||||
fetchColumnConfig();
|
||||
// fetchColumnConfig();
|
||||
};
|
||||
|
||||
// 确认自定义列选择
|
||||
@ -501,24 +482,12 @@ const fetchColumnConfig = () => {
|
||||
});
|
||||
|
||||
// 添加操作列
|
||||
cols.push({
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: isEdit.value ? 180 : 100
|
||||
});
|
||||
|
||||
|
||||
columns.value = cols;
|
||||
}).catch(() => {
|
||||
// API 失败时,至少显示操作列
|
||||
columns.value = [{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: isEdit.value ? 180 : 100
|
||||
}];
|
||||
|
||||
}).finally(() => {
|
||||
columnLoaded.value = true;
|
||||
// 等待表格渲染完成后再请求数据
|
||||
@ -531,7 +500,7 @@ onMounted(() => {
|
||||
init();
|
||||
fetchColumnConfig();
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '站名',
|
||||
placeholder: '请输入站名',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
onMounted(() => {
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<AiBoxSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getAiboxPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import AiBoxSearch from './AiBoxSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getAiboxPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '站名',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '站码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '站类',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ipaddr',
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ipaddr',
|
||||
visible: true,
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'purpose',
|
||||
title: '用途',
|
||||
dataIndex: 'purpose',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '工程名称',
|
||||
placeholder: '请输入工程名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["OTWE"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<OtweSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getOtwePageList"
|
||||
>
|
||||
</BasicTable>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import OtweSearch from './OtweSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getOtwePageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '工程名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '工程编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '位置',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '站名',
|
||||
placeholder: '请输入站名',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["AI_5014", "WVA", "AI_5005", "FPRD", "AI_5012", "AI_5001", "AI_5006", "AI_5004", "AI_5007", "AI_5002", "AI_5008", "AI_5003", "AI_5010"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<FishSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getAiPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FishSearch from './FishSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getAiPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '站名',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '站码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ipaddr',
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ipaddr',
|
||||
visible: true,
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'purpose',
|
||||
title: '用途',
|
||||
dataIndex: 'purpose',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData" :zhujianfujian="'fu'" @reset="handleReset" @finish="onSearchFinish">
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = { baseId: 'all', rstcd: null, stnm: null, sttp: null };
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{ type: 'jidiData', name: 'baseId', label: '水电基地', placeholder: '请输入水电基地名称', fieldProps: { allowClear: true }, options: [] },
|
||||
{ type: 'Input', name: 'stnm', label: '设施名称', placeholder: '请输入设施名称', width: 160, fieldProps: { allowClear: true } },
|
||||
{ col: isHide.value ? 0 : null, type: 'Select', name: 'sttp', label: '设施类型', width: 160, fieldProps: { allowClear: true, placeholder: '请选择设施类型' }, options: sttpOption.value }
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
const res = await sttpGetKendoList({
|
||||
filter: { logic: 'and', filters: [{ field: 'sttpCode', operator: 'in', dataType: 'string', value: ['FP', 'FP_1', 'FP_2', 'FP_3', 'FP_4', 'FP_5'] }] }
|
||||
});
|
||||
sttpOption.value = res.data.data.map(item => ({ label: item.sttpName, value: item.sttpCode }));
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => emit('search-finish', { ...values });
|
||||
const hideBtn = () => { isHide.value = !isHide.value; };
|
||||
const handleReset = () => emit('reset', initSearchData);
|
||||
|
||||
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<FpSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getFpPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FpSearch from './FpSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFpPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const bldsttMap: Record<number, string> = { 0: '规划', 1: '在建', 2: '已建' };
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '设施编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 180,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '设施类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'bldsttCode',
|
||||
title: '建设状态',
|
||||
dataIndex: 'bldsttCode',
|
||||
visible: true,
|
||||
width: 80,
|
||||
customRender: ({ text }) =>
|
||||
text !== null && text !== undefined ? bldsttMap[text] || text : '-'
|
||||
},
|
||||
{
|
||||
key: 'sjgycnt',
|
||||
title: '过鱼规模(尾)',
|
||||
dataIndex: 'sjgycnt',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'swdt',
|
||||
title: '开工日期',
|
||||
dataIndex: 'swdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址/位置',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
tableRef.value.getList(buildSearchParams(values));
|
||||
};
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '断面名称',
|
||||
placeholder: '请输入断面名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["TE"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<TeSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getTePageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import TeSearch from './TeSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getTePageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '断面名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '断面编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,121 @@
|
||||
<!-- <template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
<a-button type="primary" @click="handleAdd">新增</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '站点名称',
|
||||
placeholder: '请输入站点名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["OSGB"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script> -->
|
||||
@ -0,0 +1,275 @@
|
||||
<!-- <template>
|
||||
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
|
||||
<PhotoSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@add="handleAdd"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getOpPageList"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<EditPhotoModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:is-add="isAdd"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteOpFn"
|
||||
:label="'倾斜摄影站点'"
|
||||
title="删除倾斜摄影站点"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import PhotoSearch from './PhotoSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import EditPhotoModal from './EditPhotoModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import { getOpPageList, deleteOpInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '站点名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '站点编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '位置',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
title: '在线地址',
|
||||
dataIndex: 'url',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isAdd.value = true;
|
||||
editRecord.value = null;
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
isAdd.value = false;
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteOpFn = async (record: any, reason: string) => {
|
||||
return deleteOpInfo({
|
||||
ids: [record.stcd],
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style> -->
|
||||
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: { allowClear: true },
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '栖息地名称',
|
||||
placeholder: '请输入栖息地名称',
|
||||
width: 160,
|
||||
fieldProps: { allowClear: true }
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<FhSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getFhPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import FhSearch from './FhSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFhPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{ key: 'stnm', title: '栖息地名称', dataIndex: 'stnm', visible: true, width: 260, ellipsis: true },
|
||||
{ key: 'stcd', title: '栖息地站码', dataIndex: 'stcd', visible: true, width: 320, ellipsis: true },
|
||||
{ key: 'baseName', title: '水电基地', dataIndex: 'baseName', visible: true, width: 120, ellipsis: true },
|
||||
{ key: 'ennm', title: '所属电站', dataIndex: 'ennm', visible: true, width: 150, ellipsis: true },
|
||||
{ key: 'bhdxName', title: '保护对象', dataIndex: 'bhdxName', visible: true, width: 240, ellipsis: true },
|
||||
{ key: 'bhhl', title: '保护河流', dataIndex: 'bhhl', visible: true, width: 120, ellipsis: true },
|
||||
{ key: 'bhhd', title: '保护河段', dataIndex: 'bhhd', visible: true, width: 240, ellipsis: true },
|
||||
{ key: 'bhfw', title: '保护范围', dataIndex: 'bhfw', visible: true, width: 120, ellipsis: true },
|
||||
{ key: 'bhhxcd', title: '保护核心长度(km)', dataIndex: 'bhhxcd', visible: true, width: 140 },
|
||||
|
||||
{ key: 'bhcs', title: '保护措施', dataIndex: 'bhcs', visible: true, width: 120, ellipsis: true },
|
||||
{ key: 'bhfs', title: '保护方式', dataIndex: 'bhfs', visible: true, width: 120, ellipsis: true },
|
||||
{ key: 'inv', title: '投资(亿元)', dataIndex: 'inv', visible: true, width: 120 },
|
||||
{ key: 'stlc', title: '站址', dataIndex: 'stlc', visible: true, ellipsis: true,width: 220 },
|
||||
{ key: 'layerCode', title: '图层编码', dataIndex: 'layerCode', visible: true, ellipsis: true, width: 160 },
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(columns.value.reduce((sum: number, col: any) => sum + (col.width || 180), 0), 600)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd ? { field: 'rstcd', operator: 'eq', dataType: 'string', value: values.rstcd } : null,
|
||||
values.baseId !== 'all' && values.baseId != null ? { field: 'baseId', operator: 'eq', dataType: 'string', value: values.baseId } : null,
|
||||
values.stnm ? { field: 'stnm', operator: 'contains', dataType: 'string', value: values.stnm } : null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => { currentSearchParams.value = values; initTable(values); };
|
||||
const onReset = (values: any) => { currentSearchParams.value = values; initTable(values); };
|
||||
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
|
||||
|
||||
|
||||
|
||||
onMounted(() => { nextTick(() => { tableScrollY.value = calcTableScrollY(tableRef.value); }); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one { position: relative; z-index: 900; pointer-events: all; }
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '救助站名称',
|
||||
placeholder: '请输入救助站名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["VA"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<VaSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getVaPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import VaSearch from './VaSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getVaPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
const tableRef = ref(),
|
||||
tableScrollY = ref<string | number>(0),
|
||||
currentSearchParams = ref<any>({});
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '救助站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '救助站编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'place',
|
||||
title: '地点',
|
||||
dataIndex: 'place',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData" :zhujianfujian="'fu'" @reset="handleReset" @finish="onSearchFinish">
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = { baseId: 'all', rstcd: null, stnm: null, sttp: null };
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{ type: 'jidiData', name: 'baseId', label: '水电基地', placeholder: '请输入水电基地名称', fieldProps: { allowClear: true }, options: [] },
|
||||
{ type: 'Input', name: 'stnm', label: '设施名称', placeholder: '请输入设施名称', width: 160, fieldProps: { allowClear: true } },
|
||||
{ col: isHide.value ? 0 : null, type: 'Select', name: 'sttp', label: '设施类型', width: 160, fieldProps: { allowClear: true, placeholder: '请选择设施类型' }, options: sttpOption.value }
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
const res = await sttpGetKendoList({
|
||||
filter: { logic: 'and', filters: [{ field: 'sttpCode', operator: 'in', dataType: 'string', value: ['EQ', 'EQ_1', 'EQ_2', 'EQ_3', 'EQ_4', 'EQ_5', 'EQ_6'] }] }
|
||||
});
|
||||
sttpOption.value = res.data.data.map(item => ({ label: item.sttpName, value: item.sttpCode }));
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => emit('search-finish', { ...values });
|
||||
const hideBtn = () => { isHide.value = !isHide.value; };
|
||||
const handleReset = () => emit('reset', initSearchData);
|
||||
const handleAdd = () => emit('add');
|
||||
const seeEdit = () => emit('seeEdit');
|
||||
|
||||
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<EqSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getEqPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import EqSearch from './EqSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getEqPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const bldsttMap: Record<number, string> = { 0: '规划', 1: '在建', 2: '已建' };
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 160,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '设施编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '设施类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'bldsttCode',
|
||||
title: '建设状态',
|
||||
dataIndex: 'bldsttCode',
|
||||
visible: true,
|
||||
width: 80,
|
||||
customRender: ({ text }) =>
|
||||
text !== null && text !== undefined ? bldsttMap[text] || text : '-'
|
||||
},
|
||||
{
|
||||
key: 'minFlow',
|
||||
title: '最小下泄流量(m³/s)',
|
||||
dataIndex: 'minFlow',
|
||||
visible: true,
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
key: 'swdt',
|
||||
title: '开工日期',
|
||||
dataIndex: 'swdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址/位置',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
tableRef.value.getList(buildSearchParams(values));
|
||||
};
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData" :zhujianfujian="'fu'" @reset="handleReset" @finish="onSearchFinish">
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = { baseId: 'all', rstcd: null, stnm: null, sttp: null };
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{ type: 'jidiData', name: 'baseId', label: '水电基地', placeholder: '请输入水电基地名称', fieldProps: { allowClear: true }, options: [] },
|
||||
{ type: 'Input', name: 'stnm', label: '监控点名', placeholder: '请输入监控点名', width: 160, fieldProps: { allowClear: true } },
|
||||
{ col: isHide.value ? 0 : null, type: 'Select', name: 'sttp', label: '监控点类型', width: 160, fieldProps: { allowClear: true, placeholder: '请选择监控点类型' }, options: sttpOption.value }
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
const res = await sttpGetKendoList({
|
||||
filter: { logic: 'and', filters: [{ field: 'sttpCode', operator: 'in', dataType: 'string', value: ['VD', 'VD_DW', 'VD_EQ', 'VD_EQS', 'VD_FB', 'VD_FBFM', 'VD_FBI', 'VD_FBP', 'VD_FC', 'VD_FH', 'VD_FP', 'VD_FPB', 'VD_FPC', 'VD_GZFC', 'VD_OTTE', 'VD_OTWE', 'VD_PR', 'VD_SG', 'VD_TE', 'VD_VA', 'VD_VP', 'VD_WE', 'VD_WQ', 'VD_WT'] }] }
|
||||
});
|
||||
sttpOption.value = res.data.data.map(item => ({ label: item.sttpName, value: item.sttpCode }));
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => emit('search-finish', { ...values });
|
||||
const hideBtn = () => { isHide.value = !isHide.value; };
|
||||
const handleReset = () => emit('reset', initSearchData);
|
||||
|
||||
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<VdSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getVdPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import VdSearch from './VdSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getVdPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '监控点名',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '监控点编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 300,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '监控点类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'addvcdName',
|
||||
title: '所属行政区',
|
||||
dataIndex: 'addvcdName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '监控点地址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
tableRef.value.getList(buildSearchParams(values));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '断面名称',
|
||||
placeholder: '请输入断面名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["WE"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<WeSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@add="handleAdd"
|
||||
@seeEdit="handleSeeEdit"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getWePageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import WeSearch from './WeSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getWePageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '断面名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '断面编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 280,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isAdd.value = true;
|
||||
editRecord.value = null;
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '测站名称',
|
||||
placeholder: '请输入测站名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["WT", "WTRV", "WTVT"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<WtSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getWtPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import WtSearch from './WtSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getWtPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '测站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 240,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '水温站站码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width:80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([])
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '测站名称',
|
||||
placeholder: '请输入测站名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options:sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter":
|
||||
{
|
||||
"logic": "and",
|
||||
"filters":
|
||||
[
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["WQ", "WQFB", "WQFP", "WQH"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList()
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<WqSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getWqPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import WqSearch from './WqSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getWqPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '测站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '水质站站码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 180,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'dtinTypeName',
|
||||
title: '类别',
|
||||
dataIndex: 'dtinTypeName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'wwqtgName',
|
||||
title: '水质要求',
|
||||
dataIndex: 'wwqtgName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? { field: 'rstcd', operator: 'eq', dataType: 'string', value: values.rstcd }
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? { field: 'baseId', operator: 'eq', dataType: 'string', value: values.baseId }
|
||||
: null,
|
||||
values.stnm
|
||||
? { field: 'stnm', operator: 'contains', dataType: 'string', value: values.stnm }
|
||||
: null,
|
||||
values.sttp
|
||||
? { field: 'sttp', operator: 'eq', dataType: 'string', value: values.sttp }
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '站名',
|
||||
placeholder: '请输入站名',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '监控类别',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择监控类别'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["VD_SN", "VD_WVD"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<SonarSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getSonarPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import SonarSearch from './SonarSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getSonarPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '站名',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '站码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '监控类别',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'mwayName',
|
||||
title: '监测方式',
|
||||
dataIndex: 'mwayName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="isAdd ? '新增人工产卵场' : '编辑人工产卵场'"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
:rules="formRules"
|
||||
class="max-h-[70vh] overflow-y-auto pr-4"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产卵场编码" name="stcd">
|
||||
<a-input
|
||||
v-model:value="formData.stcd"
|
||||
placeholder="请输入产卵场编码"
|
||||
:disabled="!isAdd"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产卵场名称" name="stnm">
|
||||
<a-input
|
||||
v-model:value="formData.stnm"
|
||||
placeholder="请输入产卵场名称"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="测站类型" name="sttp">
|
||||
<a-select
|
||||
v-model:value="formData.sttp"
|
||||
placeholder="请选择测站类型"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in sttpOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="站址" name="stlc">
|
||||
<a-input
|
||||
v-model:value="formData.stlc"
|
||||
placeholder="请输入站址"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水电基地" name="baseId">
|
||||
<a-select
|
||||
v-model:value="formData.baseId"
|
||||
placeholder="请选择水电基地"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in baseNameOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属电站" name="rstcd">
|
||||
<a-select
|
||||
v-model:value="formData.rstcd"
|
||||
placeholder="请选择所属电站"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in engInfoOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="保护对象" name="prtcts">
|
||||
<a-input
|
||||
v-model:value="formData.prtcts"
|
||||
placeholder="请输入保护对象"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产卵场数量(个)" name="cnt">
|
||||
<a-input-number
|
||||
v-model:value="formData.cnt"
|
||||
placeholder="请输入数量"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="面积(km²)" name="ar">
|
||||
<a-input-number
|
||||
v-model:value="formData.ar"
|
||||
placeholder="请输入面积"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="营造型式" name="tp">
|
||||
<a-input
|
||||
v-model:value="formData.tp"
|
||||
placeholder="请输入营造型式"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="实施时间" name="attm">
|
||||
<a-date-picker
|
||||
v-model:value="formData.attm"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择实施时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="建成日期" name="jcdt">
|
||||
<a-date-picker
|
||||
v-model:value="formData.jcdt"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择建成日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="是否启用" name="usfl">
|
||||
<a-select
|
||||
v-model:value="formData.usfl"
|
||||
placeholder="请选择是否启用"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="投资(亿元)" name="inv">
|
||||
<a-input-number
|
||||
v-model:value="formData.inv"
|
||||
placeholder="请输入投资"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="监测频次(min)" name="dtfrqcy">
|
||||
<a-input-number
|
||||
v-model:value="formData.dtfrqcy"
|
||||
placeholder="请输入监测频次"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="简介" name="introduce" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
v-model:value="formData.introduce"
|
||||
placeholder="请输入简介"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<ConfirmModal
|
||||
v-model:open="confirmModalVisible"
|
||||
:diff-list="diffList"
|
||||
:confirm-loading="confirmLoading"
|
||||
@confirm="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { addSgInfo, updateSgInfo } from '@/api/DataQueryMenuModule';
|
||||
import { getEngInfoDropdown } from '@/api/select';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
import ConfirmModal from '@/components/ConfirmModal/index.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
isAdd?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
const jidiSelectEventStore = useJidiSelectEventStore();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref<any>({});
|
||||
const originalRecord = ref<any>({});
|
||||
const confirmModalVisible = ref(false);
|
||||
|
||||
const formRules = ref<any>({
|
||||
stcd: [{ required: true, message: '请输入产卵场编码', trigger: 'blur' }],
|
||||
stnm: [{ required: true, message: '请输入产卵场名称', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 下拉选项
|
||||
const engInfoOptions = ref<any[]>([]);
|
||||
const sttpOptions = ref<any[]>([]);
|
||||
|
||||
const baseNameOptions = ref<any[]>(
|
||||
jidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all')
|
||||
.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode
|
||||
}))
|
||||
);
|
||||
|
||||
// 字段名 → 中文名映射
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
stcd: '产卵场编码',
|
||||
stnm: '产卵场名称',
|
||||
sttp: '测站类型',
|
||||
stlc: '站址',
|
||||
baseId: '水电基地',
|
||||
rstcd: '所属电站',
|
||||
prtcts: '保护对象',
|
||||
cnt: '产卵场数量',
|
||||
ar: '面积',
|
||||
tp: '营造型式',
|
||||
attm: '实施时间',
|
||||
jcdt: '建成日期',
|
||||
usfl: '是否启用',
|
||||
inv: '投资',
|
||||
dtfrqcy: '监测频次',
|
||||
introduce: '简介',
|
||||
remark: '备注'
|
||||
};
|
||||
|
||||
// select 选项映射
|
||||
const selectOptionsMap: Record<string, () => any[]> = {
|
||||
sttp: () => sttpOptions.value,
|
||||
usfl: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 }
|
||||
],
|
||||
baseId: () => baseNameOptions.value,
|
||||
rstcd: () => engInfoOptions.value
|
||||
};
|
||||
|
||||
// 按修改顺序记录变更
|
||||
const changeOrder = ref<
|
||||
{ field: string; label: string; oldValue: string; newValue: string }[]
|
||||
>([]);
|
||||
const diffList = changeOrder;
|
||||
|
||||
const resolveSelectLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空';
|
||||
const getOptions = selectOptionsMap[field];
|
||||
if (getOptions) {
|
||||
const opt = getOptions().find(
|
||||
(o: any) => String(o.value) === String(value)
|
||||
);
|
||||
if (opt) return opt.label;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
const label = option.label || option.value;
|
||||
const keyword = inputValue || '';
|
||||
return label.includes(keyword);
|
||||
};
|
||||
|
||||
// 监听 formData 变化
|
||||
watch(
|
||||
() => formData.value,
|
||||
newData => {
|
||||
const original = originalRecord.value;
|
||||
for (const key in newData) {
|
||||
const oldVal = original[key];
|
||||
const newVal = newData[key];
|
||||
if (oldVal !== newVal) {
|
||||
const existingIdx = changeOrder.value.findIndex(c => c.field === key);
|
||||
const oldNorm =
|
||||
oldVal === null || oldVal === undefined || oldVal === '-'
|
||||
? null
|
||||
: oldVal;
|
||||
const newNorm = newVal === null || newVal === undefined ? null : newVal;
|
||||
if (oldNorm === newNorm) continue;
|
||||
if (
|
||||
oldNorm !== null &&
|
||||
newNorm !== null &&
|
||||
!isNaN(Number(oldNorm)) &&
|
||||
!isNaN(Number(newNorm)) &&
|
||||
Number(oldNorm) === Number(newNorm)
|
||||
)
|
||||
continue;
|
||||
const isSelect = !!selectOptionsMap[key];
|
||||
const oldDisplay = isSelect
|
||||
? resolveSelectLabel(key, oldNorm)
|
||||
: oldNorm === null
|
||||
? '空'
|
||||
: String(oldNorm);
|
||||
const newDisplay = isSelect
|
||||
? resolveSelectLabel(key, newNorm)
|
||||
: newNorm === null
|
||||
? '空'
|
||||
: String(newNorm);
|
||||
const entry = {
|
||||
field: key,
|
||||
label: fieldLabelMap[key] || key,
|
||||
oldValue: oldDisplay,
|
||||
newValue: newDisplay
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(c => c.field !== key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听 record 变化
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (newRecord) {
|
||||
const converted = { ...newRecord };
|
||||
originalRecord.value = { ...converted };
|
||||
formData.value = { ...converted };
|
||||
changeOrder.value = [];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 加载下拉数据
|
||||
const loadDropdownData = () => {
|
||||
// 加载测站类型
|
||||
sttpGetKendoList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'in',
|
||||
dataType: 'string',
|
||||
value: ['SG']
|
||||
}
|
||||
]
|
||||
}
|
||||
}).then(res => {
|
||||
sttpOptions.value = (res.data?.data || []).map((item: any) => ({
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
}));
|
||||
});
|
||||
baseNameOptions.value = jidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all')
|
||||
.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode
|
||||
}));
|
||||
getEngInfoDropdown({}).then(res => {
|
||||
engInfoOptions.value = (res.data || []).map((item: any) => ({
|
||||
label: item.ennm,
|
||||
value: item.stcd
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
val => {
|
||||
if (val) {
|
||||
loadDropdownData();
|
||||
if (props.isAdd) {
|
||||
formData.value = {};
|
||||
originalRecord.value = {};
|
||||
changeOrder.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validateFields();
|
||||
if (!props.isAdd && changeOrder.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async (source: string) => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const engInfo: Record<string, any> = {};
|
||||
let res: any;
|
||||
if (props.isAdd) {
|
||||
Object.assign(engInfo, formData.value);
|
||||
res = await addSgInfo({
|
||||
engInfo,
|
||||
source: source.trim() || '新增'
|
||||
});
|
||||
} else {
|
||||
engInfo.stcd = formData.value.stcd;
|
||||
changeOrder.value.forEach(item => {
|
||||
engInfo[item.field] = formData.value[item.field];
|
||||
});
|
||||
res = await updateSgInfo({
|
||||
engInfo,
|
||||
source: source.trim() || '编辑'
|
||||
});
|
||||
}
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success(props.isAdd ? '新增成功' : '编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || (props.isAdd ? '新增失败' : '编辑失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
<a-button type="primary" @click="handleAdd">新增</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '产卵场名称',
|
||||
placeholder: '请输入产卵场名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["SG"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<SpawnSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@add="handleAdd"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getSgPageList"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 编辑/新增 Modal -->
|
||||
<EditSpawnModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:is-add="isAdd"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteSgFn"
|
||||
:label="'人工产卵场'"
|
||||
title="删除人工产卵场"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import SpawnSearch from './SpawnSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import EditSpawnModal from './EditSpawnModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import { getSgPageList, deleteSgInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '产卵场名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '产卵场编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'prtcts',
|
||||
title: '保护对象',
|
||||
dataIndex: 'prtcts',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'cnt',
|
||||
title: '数量(个)',
|
||||
dataIndex: 'cnt',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ar',
|
||||
title: '面积(km²)',
|
||||
dataIndex: 'ar',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isAdd.value = true;
|
||||
editRecord.value = null;
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
isAdd.value = false;
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteSgFn = async (record: any, reason: string) => {
|
||||
return deleteSgInfo({
|
||||
ids: [record.stcd],
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '设施名称',
|
||||
placeholder: '请输入设施名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '设施类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择设施类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["DW", "DW_1", "DW_2", "DW_3", "DW_4", "DW_5", "DW_6", "DW_9"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<DwSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getDwPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import DwSearch from './DwSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getDwPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '设施编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '设施类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '工程名称',
|
||||
placeholder: '请输入工程名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择测站类型'
|
||||
},
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "sttpCode",
|
||||
"operator": "in",
|
||||
"dataType": "string",
|
||||
"value": ["OTTE"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await sttpGetKendoList(params);
|
||||
sttpOption.value = res.data.data.map(item => {
|
||||
return {
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<OtteSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getOttePageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import OtteSearch from './OtteSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getOttePageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '工程名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '工程编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'sttpName',
|
||||
title: '测站类型',
|
||||
dataIndex: 'sttpName',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '位置',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'usflName',
|
||||
title: '是否启用',
|
||||
dataIndex: 'usflName',
|
||||
visible: true,
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="isAdd ? '新增智能告警' : '编辑智能告警'"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
:rules="formRules"
|
||||
class="max-h-[70vh] overflow-y-auto pr-4"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预警规则名称" name="ruleName">
|
||||
<a-input
|
||||
v-model:value="formData.ruleName"
|
||||
placeholder="请输入预警规则名称"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预警规则类型" name="ruleType">
|
||||
<a-select
|
||||
v-model:value="formData.ruleType"
|
||||
placeholder="请选择预警规则类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="WQLVL">水质预警</a-select-option>
|
||||
<a-select-option value="EQMN">电站生态流量(环保部)</a-select-option>
|
||||
<a-select-option value="EQMNMWR">电站生态流量(水利部)</a-select-option>
|
||||
<a-select-option value="RSVRFSR">水位预警</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="规则编码" name="ruleCode">
|
||||
<a-select
|
||||
v-model:value="formData.ruleCode"
|
||||
placeholder="请选择规则编码"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="global">全局规则</a-select-option>
|
||||
<a-select-option value="common">通用告警规则</a-select-option>
|
||||
<a-select-option value="custom">自定义告警规则</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属测站" name="stcd">
|
||||
<a-input
|
||||
v-model:value="formData.stcd"
|
||||
placeholder="请输入所属测站"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水电基地" name="baseId">
|
||||
<a-select
|
||||
v-model:value="formData.baseId"
|
||||
placeholder="请选择水电基地"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in baseNameOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属电站" name="rstcd">
|
||||
<a-select
|
||||
v-model:value="formData.rstcd"
|
||||
placeholder="请选择所属电站"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in engInfoOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="是否展示告警等级" name="isShow">
|
||||
<a-select
|
||||
v-model:value="formData.isShow"
|
||||
placeholder="请选择"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option :value="1">展示</a-select-option>
|
||||
<a-select-option :value="0">不展示</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="description" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入备注"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<ConfirmModal
|
||||
v-model:open="confirmModalVisible"
|
||||
:diff-list="diffList"
|
||||
:confirm-loading="confirmLoading"
|
||||
@confirm="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { addWarnInfo, updateWarnInfo } from '@/api/DataQueryMenuModule';
|
||||
import { getEngInfoDropdown } from '@/api/select';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import ConfirmModal from '@/components/ConfirmModal/index.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
isAdd?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
const jidiSelectEventStore = useJidiSelectEventStore();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref<any>({});
|
||||
const originalRecord = ref<any>({});
|
||||
const confirmModalVisible = ref(false);
|
||||
|
||||
const formRules = ref<any>({
|
||||
ruleName: [{ required: true, message: '请输入预警规则名称', trigger: 'blur' }],
|
||||
ruleType: [{ required: true, message: '请选择预警规则类型', trigger: 'change' }]
|
||||
});
|
||||
|
||||
// 下拉选项
|
||||
const engInfoOptions = ref<any[]>([]);
|
||||
|
||||
const baseNameOptions = ref<any[]>(
|
||||
jidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all')
|
||||
.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode
|
||||
}))
|
||||
);
|
||||
|
||||
// 字段名 → 中文名映射
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
ruleName: '预警规则名称',
|
||||
ruleType: '预警规则类型',
|
||||
ruleCode: '规则编码',
|
||||
stcd: '所属测站',
|
||||
baseId: '水电基地',
|
||||
rstcd: '所属电站',
|
||||
isShow: '是否展示告警等级',
|
||||
description: '备注'
|
||||
};
|
||||
|
||||
// select 选项映射
|
||||
const selectOptionsMap: Record<string, () => any[]> = {
|
||||
ruleType: () => [
|
||||
{ label: '水质预警', value: 'WQLVL' },
|
||||
{ label: '电站生态流量(环保部)', value: 'EQMN' },
|
||||
{ label: '电站生态流量(水利部)', value: 'EQMNMWR' },
|
||||
{ label: '水位预警', value: 'RSVRFSR' }
|
||||
],
|
||||
ruleCode: () => [
|
||||
{ label: '全局规则', value: 'global' },
|
||||
{ label: '通用告警规则', value: 'common' },
|
||||
{ label: '自定义告警规则', value: 'custom' }
|
||||
],
|
||||
isShow: () => [
|
||||
{ label: '展示', value: 1 },
|
||||
{ label: '不展示', value: 0 }
|
||||
],
|
||||
baseId: () => baseNameOptions.value,
|
||||
rstcd: () => engInfoOptions.value
|
||||
};
|
||||
|
||||
// 按修改顺序记录变更
|
||||
const changeOrder = ref<
|
||||
{ field: string; label: string; oldValue: string; newValue: string }[]
|
||||
>([]);
|
||||
const diffList = changeOrder;
|
||||
|
||||
const resolveSelectLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空';
|
||||
const getOptions = selectOptionsMap[field];
|
||||
if (getOptions) {
|
||||
const opt = getOptions().find(
|
||||
(o: any) => String(o.value) === String(value)
|
||||
);
|
||||
if (opt) return opt.label;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
const label = option.label || option.value;
|
||||
const keyword = inputValue || '';
|
||||
return label.includes(keyword);
|
||||
};
|
||||
|
||||
// 监听 formData 变化
|
||||
watch(
|
||||
() => formData.value,
|
||||
newData => {
|
||||
const original = originalRecord.value;
|
||||
for (const key in newData) {
|
||||
const oldVal = original[key];
|
||||
const newVal = newData[key];
|
||||
if (oldVal !== newVal) {
|
||||
const existingIdx = changeOrder.value.findIndex(c => c.field === key);
|
||||
const oldNorm =
|
||||
oldVal === null || oldVal === undefined || oldVal === '-'
|
||||
? null
|
||||
: oldVal;
|
||||
const newNorm = newVal === null || newVal === undefined ? null : newVal;
|
||||
if (oldNorm === newNorm) continue;
|
||||
if (
|
||||
oldNorm !== null &&
|
||||
newNorm !== null &&
|
||||
!isNaN(Number(oldNorm)) &&
|
||||
!isNaN(Number(newNorm)) &&
|
||||
Number(oldNorm) === Number(newNorm)
|
||||
)
|
||||
continue;
|
||||
const isSelect = !!selectOptionsMap[key];
|
||||
const oldDisplay = isSelect
|
||||
? resolveSelectLabel(key, oldNorm)
|
||||
: oldNorm === null
|
||||
? '空'
|
||||
: String(oldNorm);
|
||||
const newDisplay = isSelect
|
||||
? resolveSelectLabel(key, newNorm)
|
||||
: newNorm === null
|
||||
? '空'
|
||||
: String(newNorm);
|
||||
const entry = {
|
||||
field: key,
|
||||
label: fieldLabelMap[key] || key,
|
||||
oldValue: oldDisplay,
|
||||
newValue: newDisplay
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(c => c.field !== key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听 record 变化
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (newRecord) {
|
||||
const converted = { ...newRecord };
|
||||
originalRecord.value = { ...converted };
|
||||
formData.value = { ...converted };
|
||||
changeOrder.value = [];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 加载下拉数据
|
||||
const loadDropdownData = () => {
|
||||
baseNameOptions.value = jidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all')
|
||||
.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode
|
||||
}));
|
||||
getEngInfoDropdown({}).then(res => {
|
||||
engInfoOptions.value = (res.data || []).map((item: any) => ({
|
||||
label: item.ennm,
|
||||
value: item.stcd
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
val => {
|
||||
if (val) {
|
||||
loadDropdownData();
|
||||
if (props.isAdd) {
|
||||
formData.value = {};
|
||||
originalRecord.value = {};
|
||||
changeOrder.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validateFields();
|
||||
if (!props.isAdd && changeOrder.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async (source: string) => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const engInfo: Record<string, any> = {};
|
||||
let res: any;
|
||||
if (props.isAdd) {
|
||||
Object.assign(engInfo, formData.value);
|
||||
res = await addWarnInfo({
|
||||
engInfo,
|
||||
source: source.trim() || '新增'
|
||||
});
|
||||
} else {
|
||||
engInfo.id = formData.value.id;
|
||||
changeOrder.value.forEach(item => {
|
||||
engInfo[item.field] = formData.value[item.field];
|
||||
});
|
||||
res = await updateWarnInfo({
|
||||
engInfo,
|
||||
source: source.trim() || '编辑'
|
||||
});
|
||||
}
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success(props.isAdd ? '新增成功' : '编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || (props.isAdd ? '新增失败' : '编辑失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
<a-button type="primary" @click="handleAdd">新增</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
ruleName: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'ruleName',
|
||||
label: '规则名称',
|
||||
placeholder: '请输入规则名称',
|
||||
width: 160,
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', { ...values });
|
||||
};
|
||||
|
||||
const hideBtn = () => {
|
||||
isHide.value = !isHide.value;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
basicSearchRef
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<WarnSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@add="handleAdd"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getWarnPageList"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 编辑/新增 Modal -->
|
||||
<EditWarnModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:is-add="isAdd"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteWarnFn"
|
||||
:label="'智能告警'"
|
||||
title="删除智能告警"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import WarnSearch from './WarnSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import EditWarnModal from './EditWarnModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import { getWarnPageList, deleteWarnInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
// 编辑相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'ruleName',
|
||||
title: '预警规则名称',
|
||||
dataIndex: 'ruleName',
|
||||
visible: true,
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ruleType',
|
||||
title: '预警规则类型',
|
||||
dataIndex: 'ruleType',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ruleCode',
|
||||
title: '规则编码',
|
||||
dataIndex: 'ruleCode',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '所属测站',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'isShow',
|
||||
title: '展示告警等级',
|
||||
dataIndex: 'isShow',
|
||||
visible: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => {
|
||||
if (text === 1) return '展示';
|
||||
if (text === 0) return '不展示';
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
visible: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.ruleName
|
||||
? {
|
||||
field: 'ruleName',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.ruleName
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const params = buildSearchParams(values);
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isAdd.value = true;
|
||||
editRecord.value = null;
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
isAdd.value = false;
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除(使用 id 作为主键)
|
||||
const deleteWarnFn = async (record: any, reason: string) => {
|
||||
return deleteWarnInfo({
|
||||
ids: [record.id],
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref<boolean>(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = {
|
||||
baseId: 'all',
|
||||
rstcd: null,
|
||||
stnm: null,
|
||||
sttp: null
|
||||
};
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'jidiData',
|
||||
name: 'baseId',
|
||||
label: '水电基地',
|
||||
placeholder: '请输入水电基地名称',
|
||||
fieldProps: { allowClear: true },
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'stnm',
|
||||
label: '增殖站名称',
|
||||
placeholder: '请输入增殖站名称',
|
||||
width: 160,
|
||||
fieldProps: { allowClear: true }
|
||||
},
|
||||
{
|
||||
col: isHide.value ? 0 : null,
|
||||
type: 'Select',
|
||||
name: 'sttp',
|
||||
label: '测站类型',
|
||||
width: 160,
|
||||
fieldProps: { allowClear: true, placeholder: '请选择测站类型' },
|
||||
options: sttpOption.value
|
||||
}
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
const res = await sttpGetKendoList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{ field: 'sttpCode', operator: 'in', dataType: 'string', value: ['FB'] }
|
||||
]
|
||||
}
|
||||
});
|
||||
sttpOption.value = res.data.data.map(item => ({
|
||||
label: item.sttpName,
|
||||
value: item.sttpCode
|
||||
}));
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => emit('search-finish', { ...values });
|
||||
const hideBtn = () => { isHide.value = !isHide.value; };
|
||||
const handleReset = () => emit('reset', initSearchData);
|
||||
|
||||
onMounted(() => {
|
||||
getBaseList();
|
||||
emit('search-finish', { ...initSearchData });
|
||||
});
|
||||
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<FbSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getFbPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FbSearch from './FbSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFbPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '增殖站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 160,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '增殖站编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'zzfldx',
|
||||
title: '放流对象',
|
||||
dataIndex: 'zzfldx',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'zzflcnt',
|
||||
title: '放流规模(尾)',
|
||||
dataIndex: 'zzflcnt',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
|
||||
{
|
||||
key: 'zzflbjfs',
|
||||
title: '标记方式',
|
||||
dataIndex: 'zzflbjfs',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'zzflar',
|
||||
title: '总占地面积(km²)',
|
||||
dataIndex: 'zzflar',
|
||||
visible: true,
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'zzflrw',
|
||||
title: '承担放流任务',
|
||||
dataIndex: 'zzflrw',
|
||||
visible: true,
|
||||
width: 130,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'zzflfllc',
|
||||
title: '放流地点',
|
||||
dataIndex: 'zzflfllc',
|
||||
visible: true,
|
||||
width: 260,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'zzflyzms',
|
||||
title: '养殖模式',
|
||||
dataIndex: 'zzflyzms',
|
||||
visible: true,
|
||||
width: 220,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'zzflgy',
|
||||
title: '生产工艺',
|
||||
dataIndex: 'zzflgy',
|
||||
visible: true,
|
||||
width: 180,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'swdt',
|
||||
title: '开工日期',
|
||||
dataIndex: 'swdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'zzfltm',
|
||||
title: '放流时间',
|
||||
dataIndex: 'zzfltm',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'addvcdName',
|
||||
title: '地址',
|
||||
dataIndex: 'addvcdName',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
tableRef.value.getList(buildSearchParams(values));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData" :zhujianfujian="'fu'" @reset="handleReset" @finish="onSearchFinish">
|
||||
<template #actions>
|
||||
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
|
||||
const basicSearchRef = ref();
|
||||
const isHide = ref(false);
|
||||
const sttpOption = ref([]);
|
||||
|
||||
const initSearchData = { baseId: 'all', rstcd: null, stnm: null, sttp: null };
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{ type: 'jidiData', name: 'baseId', label: '水电基地', placeholder: '请输入水电基地名称', fieldProps: { allowClear: true }, options: [] },
|
||||
{ type: 'Input', name: 'stnm', label: '植物园名称', placeholder: '请输入植物园名称', width: 160, fieldProps: { allowClear: true } },
|
||||
{ col: isHide.value ? 0 : null, type: 'Select', name: 'sttp', label: '测站类型', width: 160, fieldProps: { allowClear: true, placeholder: '请选择测站类型' }, options: sttpOption.value }
|
||||
]);
|
||||
|
||||
const getBaseList = async () => {
|
||||
const res = await sttpGetKendoList({
|
||||
filter: { logic: 'and', filters: [{ field: 'sttpCode', operator: 'in', dataType: 'string', value: ['VP', 'AI_VP_GRAZE', 'AI_VP_WATER'] }] }
|
||||
});
|
||||
sttpOption.value = res.data.data.map(item => ({ label: item.sttpName, value: item.sttpCode }));
|
||||
};
|
||||
|
||||
const onSearchFinish = (values: any) => emit('search-finish', { ...values });
|
||||
const hideBtn = () => { isHide.value = !isHide.value; };
|
||||
const handleReset = () => emit('reset', initSearchData);
|
||||
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
|
||||
defineExpose({ basicSearchRef });
|
||||
</script>
|
||||
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col body_one">
|
||||
<VpSearch
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enableEllipsis="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getVpPageList"
|
||||
>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import VpSearch from './VpSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getVpPageList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const tableRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '植物园名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stcd',
|
||||
title: '植物园编码',
|
||||
dataIndex: 'stcd',
|
||||
visible: true,
|
||||
width: 150,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '水电基地',
|
||||
dataIndex: 'baseName',
|
||||
visible: true,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'area',
|
||||
title: '面积(km²)',
|
||||
dataIndex: 'area',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'inv',
|
||||
title: '投资(亿元)',
|
||||
dataIndex: 'inv',
|
||||
visible: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'swdt',
|
||||
title: '开工日期',
|
||||
dataIndex: 'swdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
visible: true,
|
||||
width: 110,
|
||||
customRender: ({ text }) => {
|
||||
if (!text || text === '-') return '-';
|
||||
const date = dayjs(text);
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'bhfs',
|
||||
title: '保护方式',
|
||||
dataIndex: 'bhfs',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'stlc',
|
||||
title: '站址',
|
||||
dataIndex: 'stlc',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
]);
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
const buildSearchParams = (values: any) => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.baseId !== 'all' && values.baseId != null
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.baseId
|
||||
}
|
||||
: null,
|
||||
values.stnm
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.stnm
|
||||
}
|
||||
: null,
|
||||
values.sttp
|
||||
? {
|
||||
field: 'sttp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.sttp
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const onReset = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
initTable(values);
|
||||
};
|
||||
const initTable = (values: any) => {
|
||||
tableRef.value.getList(buildSearchParams(values));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑流量站数据"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 7 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<div class="form-scroll-area">
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">基本信息</div></a-col
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="站点名称">
|
||||
<a-input :value="recordData.stnm || '-'" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="时间">
|
||||
<a-input :value="displayTime" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属基地">
|
||||
<a-input :value="recordData.baseName || '-'" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属电站">
|
||||
<a-input :value="recordData.ennm || '-'" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">监测数据</div></a-col
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水位(m)">
|
||||
<a-input-number
|
||||
v-model:value="formData.z"
|
||||
placeholder="请输入水位"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="流量(m³/s)">
|
||||
<a-input-number
|
||||
v-model:value="formData.q"
|
||||
placeholder="请输入流量"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="流速(m/s)">
|
||||
<a-input-number
|
||||
v-model:value="formData.v"
|
||||
placeholder="请输入流速"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="confirmModalVisible"
|
||||
title="确认修改"
|
||||
width="700px"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
>
|
||||
<div style="max-height: 50vh; overflow-y: auto; margin-bottom: 16px">
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
v-for="item in changedFieldList"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
>
|
||||
<span style="color: #ff4d4f; text-decoration: line-through">{{
|
||||
item.oldValue
|
||||
}}</span>
|
||||
<span style="margin: 0 8px">→</span>
|
||||
<span style="color: #52c41a">{{ item.newValue }}</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-form-item label="修改依据">
|
||||
<a-textarea
|
||||
v-model:value="sourceValue"
|
||||
placeholder="请输入修改依据"
|
||||
:rows="5"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateFlowStationInfo } from '@/api/DataQueryMenuModule';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
timeScale?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const confirmModalVisible = ref(false);
|
||||
const sourceValue = ref('');
|
||||
const recordData = ref<any>({});
|
||||
const formData = ref<{
|
||||
z: number | null;
|
||||
q: number | null;
|
||||
v: number | null;
|
||||
}>({
|
||||
z: null,
|
||||
q: null,
|
||||
v: null
|
||||
});
|
||||
|
||||
const normalizeValue = (value: any) => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value);
|
||||
};
|
||||
|
||||
const formatValue = (value: any) => {
|
||||
const normalized = normalizeValue(value);
|
||||
return normalized === null ? '空' : String(normalized);
|
||||
};
|
||||
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
z: '水位(m)',
|
||||
q: '流量(m³/s)',
|
||||
v: '流速(m/s)'
|
||||
};
|
||||
|
||||
const displayTime = computed(() => {
|
||||
const timeScale = props.timeScale;
|
||||
if (timeScale === 'month') {
|
||||
const year = recordData.value?.year ?? '-';
|
||||
const month = recordData.value?.month ?? '-';
|
||||
return `${year}-${String(month).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const timeValue = recordData.value?.dt ?? recordData.value?.tm;
|
||||
if (!timeValue) return '-';
|
||||
if (timeScale === 'dt') {
|
||||
return dayjs(timeValue).format('YYYY-MM-DD');
|
||||
}
|
||||
return dayjs(timeValue).format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
|
||||
const changedFieldList = computed(() => {
|
||||
return (['z', 'q', 'v'] as const)
|
||||
.map(key => {
|
||||
const oldValue = normalizeValue(recordData.value?.[key]);
|
||||
const newValue = normalizeValue(formData.value[key]);
|
||||
if (oldValue === newValue) return null;
|
||||
return {
|
||||
key,
|
||||
label: fieldLabelMap[key],
|
||||
oldValue: formatValue(oldValue),
|
||||
newValue: formatValue(newValue)
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as {
|
||||
key: string;
|
||||
label: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}[];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (!newRecord) {
|
||||
recordData.value = {};
|
||||
formData.value = {
|
||||
z: null,
|
||||
q: null,
|
||||
v: null
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
recordData.value = JSON.parse(JSON.stringify(newRecord));
|
||||
formData.value = {
|
||||
z: normalizeValue(newRecord.z),
|
||||
q: normalizeValue(newRecord.q),
|
||||
v: normalizeValue(newRecord.v)
|
||||
};
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = false;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
if (changedFieldList.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const updateData = {
|
||||
stcd: recordData.value?.stcd,
|
||||
tm: recordData.value?.tm,
|
||||
stnm: recordData.value?.stnm,
|
||||
z:
|
||||
formData.value.z === null || formData.value.z === undefined
|
||||
? ''
|
||||
: String(formData.value.z),
|
||||
q:
|
||||
formData.value.q === null || formData.value.q === undefined
|
||||
? ''
|
||||
: String(formData.value.q),
|
||||
v:
|
||||
formData.value.v === null || formData.value.v === undefined
|
||||
? ''
|
||||
: String(formData.value.v)
|
||||
};
|
||||
|
||||
const res = await updateFlowStationInfo({
|
||||
updateData,
|
||||
source: sourceValue.value.trim()
|
||||
});
|
||||
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-scroll-area {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 4px 4px 0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-section + .form-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 8px 0 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -19,48 +19,15 @@
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<EditFlowStationModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:time-scale="currentSearchParams.timeScale"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="handleDeleteFn"
|
||||
title="删除流量站数据"
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FlowStationSearch from './FlowStationSearch.vue';
|
||||
import EditFlowStationModal from './EditFlowStationModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import {
|
||||
getFlowStationList,
|
||||
@ -216,15 +183,7 @@ const Columns = computed(() => {
|
||||
// 在基地列之后插入时间列
|
||||
const baseIndex = result.findIndex(c => c.key === 'ennm');
|
||||
result.splice(baseIndex + 1, 0, timeColumn.value);
|
||||
if (currentSearchParams.value.timeScale === 'tm') {
|
||||
result.push({
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@ -257,33 +216,6 @@ const exportBtn = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
return deleteFlowStationInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd,
|
||||
tm: record.tm
|
||||
}
|
||||
],
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const filters = [
|
||||
@ -323,7 +255,7 @@ const initTable = (values: any) => {
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,252 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑表层水温数据"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 7 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<div class="form-scroll-area">
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">基本信息</div></a-col
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="断面名称">
|
||||
<a-input :value="recordData.stnm || '-'" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="时间">
|
||||
<a-input :value="displayTime" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">水温数据</div></a-col
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水温(℃)">
|
||||
<a-input-number
|
||||
v-model:value="formData.wt"
|
||||
placeholder="请输入水温"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="confirmModalVisible"
|
||||
title="确认修改"
|
||||
width="700px"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
>
|
||||
<div style="max-height: 50vh; overflow-y: auto; margin-bottom: 16px">
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item label="水温(℃)">
|
||||
<span style="color: #ff4d4f; text-decoration: line-through">{{
|
||||
originalWtText
|
||||
}}</span>
|
||||
<span style="margin: 0 8px">→</span>
|
||||
<span style="color: #52c41a">{{ currentWtText }}</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-form-item label="修改依据">
|
||||
<a-textarea
|
||||
v-model:value="sourceValue"
|
||||
placeholder="请输入修改依据"
|
||||
:rows="5"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateSurfaceTempInfo } from '@/api/DataQueryMenuModule';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
timeScale?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const confirmModalVisible = ref(false);
|
||||
const sourceValue = ref('');
|
||||
const recordData = ref<any>({});
|
||||
const formData = ref<{
|
||||
wt: number | null;
|
||||
}>({
|
||||
wt: null
|
||||
});
|
||||
|
||||
const normalizeValue = (value: any) => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value);
|
||||
};
|
||||
|
||||
const formatValue = (value: any) => {
|
||||
const normalized = normalizeValue(value);
|
||||
return normalized === null ? '空' : String(normalized);
|
||||
};
|
||||
|
||||
const displayTime = computed(() => {
|
||||
const timeScale = props.timeScale;
|
||||
if (timeScale === 'month') {
|
||||
const year = recordData.value?.year ?? '-';
|
||||
const month = recordData.value?.month ?? '-';
|
||||
return `${year}-${String(month).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const timeValue = recordData.value?.dt ?? recordData.value?.tm;
|
||||
if (!timeValue) return '-';
|
||||
if (timeScale === 'dt') {
|
||||
return dayjs(timeValue).format('YYYY-MM-DD');
|
||||
}
|
||||
return dayjs(timeValue).format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
|
||||
const originalWt = computed(() => normalizeValue(recordData.value?.wt));
|
||||
const originalWtText = computed(() => formatValue(originalWt.value));
|
||||
const currentWtText = computed(() => formatValue(formData.value.wt));
|
||||
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (!newRecord) {
|
||||
recordData.value = {};
|
||||
formData.value = { wt: null };
|
||||
return;
|
||||
}
|
||||
|
||||
recordData.value = JSON.parse(JSON.stringify(newRecord));
|
||||
formData.value = {
|
||||
wt: normalizeValue(newRecord.wt)
|
||||
};
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = false;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
if (originalWt.value === formData.value.wt) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const updateData = {
|
||||
stcd: recordData.value?.stcd,
|
||||
dt: recordData.value?.tm,
|
||||
stnm: recordData.value?.stnm,
|
||||
wt:
|
||||
formData.value.wt === null || formData.value.wt === undefined
|
||||
? ''
|
||||
: String(formData.value.wt)
|
||||
};
|
||||
|
||||
const res = await updateSurfaceTempInfo({
|
||||
updateData,
|
||||
source: sourceValue.value.trim()
|
||||
});
|
||||
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-scroll-area {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 4px 4px 0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-section + .form-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 8px 0 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -19,54 +19,18 @@
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<EditSurfaceTempModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:time-scale="currentSearchParams.timeScale"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="handleDeleteFn"
|
||||
title="删除表层水温数据"
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import SurfaceTempSearch from './SurfaceTempSearch.vue';
|
||||
import EditSurfaceTempModal from './EditSurfaceTempModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import {
|
||||
getSurfaceTempList,
|
||||
getSurfaceTempDayList,
|
||||
getSurfaceTempMonthList,
|
||||
deleteSurfaceTempInfo
|
||||
} from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { buildTimeFilters } from '@/utils/buildTimeFilters';
|
||||
@ -103,9 +67,6 @@ const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
|
||||
// 根据 timeScale 动态切换 API
|
||||
const currentListUrl = computed(() => {
|
||||
@ -180,15 +141,6 @@ const Columns = computed(() => {
|
||||
// 在基地列之后插入时间列
|
||||
const baseIndex = result.findIndex(c => c.key === 'stnm');
|
||||
result.splice(baseIndex + 1, 0, timeColumn.value);
|
||||
if (currentSearchParams.value.timeScale === 'tm') {
|
||||
result.push({
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
@ -220,35 +172,6 @@ const exportBtn = () => {
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
return deleteSurfaceTempInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd,
|
||||
dt: record.tm
|
||||
}
|
||||
],
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
@ -287,7 +210,7 @@ const initTable = (values: any) => {
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,288 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑垂向水温数据"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 7 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<div class="form-scroll-area">
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">基本信息</div></a-col
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="测站名称">
|
||||
<a-input :value="recordData.stnm || '-'" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="时间">
|
||||
<a-input :value="displayTime" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">水深数据</div></a-col
|
||||
>
|
||||
<a-col v-for="item in depthFieldList" :key="item.key" :span="12">
|
||||
<a-form-item :label="`水深${item.key}m(℃)`">
|
||||
<a-input-number
|
||||
v-model:value="formData.dataList[item.key]"
|
||||
placeholder="请输入水温"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="confirmModalVisible"
|
||||
title="确认修改"
|
||||
width="700px"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
>
|
||||
<div style="max-height: 50vh; overflow-y: auto; margin-bottom: 16px">
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
v-for="item in changedDepthList"
|
||||
:key="item.key"
|
||||
:label="`水深${item.key}m(℃)`"
|
||||
>
|
||||
<span style="color: #ff4d4f; text-decoration: line-through">{{
|
||||
item.oldValue
|
||||
}}</span>
|
||||
<span style="margin: 0 8px">→</span>
|
||||
<span style="color: #52c41a">{{ item.newValue }}</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-form-item label="修改依据">
|
||||
<a-textarea
|
||||
v-model:value="sourceValue"
|
||||
placeholder="请输入修改依据"
|
||||
:rows="5"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateVerticalInfo } from '@/api/DataQueryMenuModule';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
timeScale?: string;
|
||||
visibleDepths?: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const confirmModalVisible = ref(false);
|
||||
const sourceValue = ref('');
|
||||
const recordData = ref<any>({});
|
||||
const formData = ref<{
|
||||
dataList: Record<string, number | null>;
|
||||
}>({
|
||||
dataList: {}
|
||||
});
|
||||
|
||||
const depthFieldList = computed(() => {
|
||||
const dataList = recordData.value?.dataList || {};
|
||||
const visibleDepths = props.visibleDepths || [];
|
||||
return Object.keys(dataList)
|
||||
.filter(key => visibleDepths.includes(key))
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map(key => ({
|
||||
key
|
||||
}));
|
||||
});
|
||||
|
||||
const displayTime = computed(() => {
|
||||
const timeValue = recordData.value?.dt ?? recordData.value?.tm;
|
||||
if (!timeValue) return '-';
|
||||
if (props.timeScale === 'month') {
|
||||
return dayjs(timeValue).format('YYYY-MM');
|
||||
}
|
||||
if (props.timeScale === 'dt') {
|
||||
return dayjs(timeValue).format('YYYY-MM-DD');
|
||||
}
|
||||
return dayjs(timeValue).format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
|
||||
const normalizeValue = (value: any) => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value);
|
||||
};
|
||||
|
||||
const formatValue = (value: any) => {
|
||||
const normalized = normalizeValue(value);
|
||||
return normalized === null ? '空' : String(normalized);
|
||||
};
|
||||
|
||||
const changedDepthList = computed(() => {
|
||||
return depthFieldList.value
|
||||
.map(item => {
|
||||
const oldValue = normalizeValue(recordData.value?.dataList?.[item.key]);
|
||||
const newValue = normalizeValue(formData.value.dataList?.[item.key]);
|
||||
if (oldValue === newValue) return null;
|
||||
return {
|
||||
key: item.key,
|
||||
oldValue: formatValue(oldValue),
|
||||
newValue: formatValue(newValue)
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as { key: string; oldValue: string; newValue: string }[];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (!newRecord) {
|
||||
recordData.value = {};
|
||||
formData.value = {
|
||||
dataList: {}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
recordData.value = JSON.parse(JSON.stringify(newRecord));
|
||||
const dataList = recordData.value?.dataList || {};
|
||||
const nextDataList: Record<string, number | null> = {};
|
||||
Object.keys(dataList)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.forEach(key => {
|
||||
nextDataList[key] = normalizeValue(dataList[key]);
|
||||
});
|
||||
formData.value = {
|
||||
dataList: nextDataList
|
||||
};
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = false;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
if (changedDepthList.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const updateData = {
|
||||
stcd: recordData.value?.stcd,
|
||||
dt: recordData.value?.dt,
|
||||
stnm: recordData.value?.stnm,
|
||||
map: depthFieldList.value.reduce((result, item) => {
|
||||
const value = formData.value.dataList?.[item.key];
|
||||
result[item.key] =
|
||||
value === null || value === undefined || value === ''
|
||||
? ''
|
||||
: String(value);
|
||||
return result;
|
||||
}, {} as Record<string, string>)
|
||||
};
|
||||
|
||||
const res = await updateVerticalInfo({
|
||||
updateData,
|
||||
source: sourceValue.value.trim()
|
||||
});
|
||||
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-scroll-area {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 0 4px 0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-section + .form-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 8px 0 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -59,43 +59,9 @@
|
||||
:min-selection-count="1"
|
||||
@selection-change="handleCxswSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditVerticalTempModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:time-scale="currentSearchParams.timeScale"
|
||||
:visible-depths="selectedColumns"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="handleDeleteFn"
|
||||
title="删除垂向水温数据"
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -111,8 +77,6 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import VerticalTempSearch from './VerticalTempSearch.vue';
|
||||
import EditVerticalTempModal from './EditVerticalTempModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getVerticalList, deleteVerticalInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
@ -221,20 +185,7 @@ const tableColumns = computed(() => {
|
||||
: '-';
|
||||
}
|
||||
}));
|
||||
|
||||
const actionCols =
|
||||
currentSearchParams.value.timeScale === 'tm'
|
||||
? [
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
return [...fixedCols, ...dataCols, ...actionCols];
|
||||
return [...fixedCols, ...dataCols ];
|
||||
});
|
||||
|
||||
const tableScrollX = computed(() => {
|
||||
@ -243,13 +194,7 @@ const tableScrollX = computed(() => {
|
||||
return total > 600 ? total : undefined;
|
||||
});
|
||||
|
||||
// 默认选中行
|
||||
const defaultSelectedRowKeys = computed(() => {
|
||||
if (tableData.value && tableData.value.length > 0) {
|
||||
return [tableData.value[0].dt];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
|
||||
// 图表颜色
|
||||
const COLORS = [
|
||||
@ -434,33 +379,7 @@ const exportBtn = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
return deleteVerticalInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd ?? record.id,
|
||||
tm: record.tm ?? record.dt
|
||||
}
|
||||
],
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
const filters = [
|
||||
@ -627,7 +546,7 @@ let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableRef.value);
|
||||
|
||||
// 初始化图表
|
||||
if (chartRef.value && !chartInstance) {
|
||||
|
||||
@ -1,405 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑水质数据"
|
||||
width="60vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
:rules="formRules"
|
||||
class="max-h-[70vh] overflow-y-auto pr-4"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<!-- 基本信息(只读展示) -->
|
||||
<a-col :span="24"><div class="form-group-title">基本信息</div></a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-form-item label="测站/断面名称">
|
||||
<a-input :value="formData.stnm" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="所属电站">
|
||||
<a-input :value="formData.ennm" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="基地">
|
||||
<a-input :value="formData.baseName" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 地表水等级 -->
|
||||
<a-col :span="24"><div class="form-group-title">地表水等级</div></a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-form-item label="水质要求" name="wwqtg">
|
||||
<a-input
|
||||
v-model:value="formData.wwqtgName"
|
||||
disabled
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="水质等级" name="wqgrd">
|
||||
<a-input
|
||||
v-model:value="formData.wqgrdName"
|
||||
disabled
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 污染物监测指标 -->
|
||||
<a-col :span="24"
|
||||
><div class="form-group-title">污染物监测指标</div></a-col
|
||||
>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-form-item label="化学需氧量(mg/L)" name="codcr">
|
||||
<a-input-number
|
||||
v-model:value="formData.codcr"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="氨氮(mg/L)" name="nh3n">
|
||||
<a-input-number
|
||||
v-model:value="formData.nh3n"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="总磷(mg/L)" name="tp">
|
||||
<a-input-number
|
||||
v-model:value="formData.tp"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="总氮(mg/L)" name="tn">
|
||||
<a-input-number
|
||||
v-model:value="formData.tn"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 五参数 -->
|
||||
<a-col :span="24"><div class="form-group-title">五参数</div></a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-form-item label="电导率(μS/cm)" name="cond">
|
||||
<a-input-number
|
||||
v-model:value="formData.cond"
|
||||
placeholder="请输入"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="溶解氧(mg/L)" name="dox">
|
||||
<a-input-number
|
||||
v-model:value="formData.dox"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="pH值" name="ph">
|
||||
<a-input-number
|
||||
v-model:value="formData.ph"
|
||||
placeholder="请输入"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="浊度(NTU)" name="tu">
|
||||
<a-input-number
|
||||
v-model:value="formData.tu"
|
||||
placeholder="请输入"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="水温(℃)" name="wtmp">
|
||||
<a-input-number
|
||||
v-model:value="formData.wtmp"
|
||||
placeholder="请输入"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 确认修改弹框 -->
|
||||
<a-modal
|
||||
v-model:open="confirmModalVisible"
|
||||
title="确认修改"
|
||||
width="800px"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
>
|
||||
<div style="max-height: 50vh; overflow-y: auto; margin-bottom: 16px">
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
v-for="item in diffList"
|
||||
:key="item.field"
|
||||
:label="item.label"
|
||||
>
|
||||
<span style="color: #ff4d4f; text-decoration: line-through">{{
|
||||
item.oldValue
|
||||
}}</span>
|
||||
<span style="margin: 0 8px">→</span>
|
||||
<span style="color: #52c41a">{{ item.newValue }}</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-form-item label="修改依据">
|
||||
<a-textarea
|
||||
v-model:value="sourceValue"
|
||||
placeholder="请输入修改依据"
|
||||
:rows="5"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateWaterDataInfo } from '@/api/DataQueryMenuModule';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref<any>({});
|
||||
const originalRecord = ref<any>({});
|
||||
const formRules = ref<any>({
|
||||
stnm: [{ required: true, message: '请输入测站名称', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 确认弹框状态
|
||||
const confirmModalVisible = ref(false);
|
||||
const sourceValue = ref('');
|
||||
|
||||
// 字段名 → 中文名映射
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
codcr: '化学需氧量(mg/L)',
|
||||
nh3n: '氨氮(mg/L)',
|
||||
tp: '总磷(mg/L)',
|
||||
tn: '总氮(mg/L)',
|
||||
cond: '电导率(μS/cm)',
|
||||
dox: '溶解氧(mg/L)',
|
||||
ph: 'pH值',
|
||||
tu: '浊度(NTU)',
|
||||
wtmp: '水温(℃)'
|
||||
};
|
||||
|
||||
// select 选项映射(用于在 diff 中显示中文名)
|
||||
const selectOptionsMap: Record<string, () => any[]> = {};
|
||||
|
||||
const resolveSelectLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空';
|
||||
const getOptions = selectOptionsMap[field];
|
||||
if (getOptions) {
|
||||
const opt = getOptions().find(
|
||||
(o: any) => String(o.value) === String(value)
|
||||
);
|
||||
if (opt) return opt.label;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 按修改顺序记录变更
|
||||
const changeOrder = ref<
|
||||
{ field: string; label: string; oldValue: string; newValue: string }[]
|
||||
>([]);
|
||||
const diffList = changeOrder;
|
||||
|
||||
// 将值中的"-"转换为null
|
||||
const convertDashToNull = (obj: any) => {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj[key] === '-' || obj[key] === '') {
|
||||
result[key] = null;
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 监听 formData 变化,按顺序记录修改
|
||||
watch(
|
||||
() => formData.value,
|
||||
newData => {
|
||||
const original = originalRecord.value;
|
||||
for (const key in newData) {
|
||||
const oldVal = original[key];
|
||||
const newVal = newData[key];
|
||||
if (oldVal !== newVal) {
|
||||
const existingIdx = changeOrder.value.findIndex(c => c.field === key);
|
||||
const oldNorm =
|
||||
oldVal === null || oldVal === undefined || oldVal === '-'
|
||||
? null
|
||||
: oldVal;
|
||||
const newNorm = newVal === null || newVal === undefined ? null : newVal;
|
||||
if (oldNorm === newNorm) continue;
|
||||
if (
|
||||
oldNorm !== null &&
|
||||
newNorm !== null &&
|
||||
!isNaN(Number(oldNorm)) &&
|
||||
!isNaN(Number(newNorm)) &&
|
||||
Number(oldNorm) === Number(newNorm)
|
||||
)
|
||||
continue;
|
||||
const isSelect = !!selectOptionsMap[key];
|
||||
const oldDisplay = isSelect
|
||||
? resolveSelectLabel(key, oldNorm)
|
||||
: oldNorm === null
|
||||
? '空'
|
||||
: String(oldNorm);
|
||||
const newDisplay = isSelect
|
||||
? resolveSelectLabel(key, newNorm)
|
||||
: newNorm === null
|
||||
? '空'
|
||||
: String(newNorm);
|
||||
const entry = {
|
||||
field: key,
|
||||
label: fieldLabelMap[key] || key,
|
||||
oldValue: oldDisplay,
|
||||
newValue: newDisplay
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(c => c.field !== key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听 record 变化,填充表单
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (newRecord) {
|
||||
const converted = convertDashToNull({ ...newRecord });
|
||||
originalRecord.value = { ...converted };
|
||||
formData.value = { ...converted };
|
||||
changeOrder.value = [];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validateFields();
|
||||
|
||||
if (changeOrder.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认弹框
|
||||
sourceValue.value = '';
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const res = await updateWaterDataInfo({
|
||||
updateData: { ...formData.value },
|
||||
source: sourceValue.value.trim()
|
||||
});
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
sourceValue.value = '';
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-group-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||