数据查询修改

This commit is contained in:
扈兆增 2026-06-30 08:41:54 +08:00
parent 78eb7f9b87
commit 0398be3d83
35 changed files with 3418 additions and 426 deletions

View File

@ -0,0 +1,340 @@
# 地图模块-OpenLayers抽吸与碰撞分析
## 1. 背景
当前地图 2D 主引擎已经切换为 OpenLayers项目依赖版本为 `ol@^10.8.0`
本次关注的问题有两个:
- 点位“抽吸”逻辑不符合业务预期;
- 文字碰撞时把整个点位一起隐藏,而不是仅隐藏文字。
典型场景是两个相邻电站,例如“积石峡”和“公伯峡”:
- 可用代号 `1`、`2` 表示两个相邻点;
- 默认缩放下只展示 `1`
- 放大到某个层级后,`1` 隐藏,`2` 展示;
- 再继续放大后,`1` 和 `2` 都展示,但 `2` 需要“抽吸/拉开”显示,避免与 `1` 重叠;
- 如果只是文字发生碰撞,希望点图标保留,仅文字隐藏或延后显示。
本文件先整理当前代码现状和问题成因,不直接修改实现。
## 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 的碰撞判断。
在这个基础上,再进入代码修改会更稳。

View File

@ -0,0 +1,622 @@
# 地图模块-详细说明
## 1. 文档目的
本文档用于梳理当前前端地图模块的整体结构、运行链路、核心文件职责、图层与点位渲染机制、图例与筛选联动方式,以及后台配置与前台运行时之间的关系。
本文档只描述当前地图模块整体实现,不包含抽吸和碰撞的专项分析。
抽吸与碰撞问题单独整理在:
- `docs/地图模块-OpenLayers抽吸与碰撞分析.md`
## 2. 当前技术栈
当前地图模块涉及的核心技术如下:
- 前端框架Vue 3 + TypeScript + Ant Design Vue
- 状态管理Pinia
- 2D 地图引擎OpenLayers
- 3D 地图引擎Cesium
- 地图服务GeoServer
- 数据来源:
- 后端接口返回图层树配置
- 后端接口返回图例配置
- 后端接口返回各类锚点数据
项目依赖中当前地图引擎版本如下:
- `ol@^10.8.0`
- `cesium@^1.141.0`
- `leaflet@^1.9.4`
说明:
- 当前 2D 主链路已经切换到 OpenLayers
- Leaflet 依赖仍保留在项目中,但不是当前主运行路径。
## 3. 地图整体架构
当前地图模块可以分成 6 层:
1. 页面入口层
2. 应用编排层
3. 地图能力门面层
4. 地图引擎实现层
5. 状态存储层
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/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` 常驻挂载,入口位置在:
- [AppMain.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/layout/components/AppMain.vue)
这意味着地图并不是某一个业务页单独创建,而是作为全局地图容器持续存在,然后随着菜单切换切换图层和页面配置。
### 4.2 `GisView.vue` 职责
文件:
- [GisView.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/GisView.vue)
当前职责主要有:
- 提供地图 DOM 容器 `#mapContainer`
- 挂载地图 popup 容器
- 挂载地图图例、筛选器、控制器、底图切换器
- 根据当前路由计算 `pageKey`
- 在组件挂载时触发地图初始化
- 在页面切换时触发地图页面配置切换
- 分发少量地图控制命令,例如 2D/3D 切换、梯级流域显示
它已经不再承担大部分地图业务拼装逻辑,更多是地图页面入口和组件装配层。
## 5. 应用编排层
文件:
- [map-orchestrator.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/application/map-orchestrator.ts)
`map-orchestrator` 是当前地图运行时的调度中心主要负责把页面、Store、地图实例串起来。
核心职责包括:
- 初始化地图壳和 popup 容器
- 根据 `pageKey` 加载当前页面地图配置
- 拉取图层树和图例配置
- 初始化基础底图
- 触发点位数据加载
- 处理图层树勾选
- 处理图例显隐
- 处理基地切换
- 处理时间筛选和重新加载
- 处理搜索定位
- 处理某些菜单下的缩放联动图层
### 5.1 初始化主链路
大致流程如下:
1. `GisView.vue` 调用 `mapOrchestrator.mountView()`
2. 编排器初始化地图实例
3. 挂载 popup 容器
4. 加载当前页面的图层树、图例和页面图例
5. 初始化基础底图
6. 加载点位图层数据
7. 建立缩放监听、基地监听和图例联动
### 5.2 页面切换
当路由变化后:
- `GisView.vue` 重新计算 `pageKey`
- 调用 `mapOrchestrator.handlePageChange()`
- 编排器重新加载该页面对应的地图配置和显示状态
这使得地图容器本身不销毁,只是页面配置和显示内容发生变化。
## 6. 地图能力门面层
文件:
- [map.class.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.class.ts)
`MapClass` 是地图能力统一门面,对外提供统一方法,屏蔽 2D 和 3D 的差异。
典型能力包括:
- `init()`
- `addBaseDataLayer()`
- `addInitDataLayer()`
- `mdLayerTreeShowOrHidden()`
- `setLegendPointVisible()`
- `baseLayerSwitcher()`
- `flyTopanto()`
- `zoomToggle()`
- `lengthCalculate()`
- `areCalculate()`
- `mapOutPut()`
- `destroy()`
- `switchView()`
当前 2D 主实现是:
- `this.service = new MapOl()`
说明当前前台主要还是 OpenLayers 在承担地图业务。
## 7. 地图引擎实现层
### 7.1 OpenLayers 主实现
文件:
- [map.ol.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.ol.ts)
这是当前地图模块最核心的执行层文件,承担了大量实际渲染与地图交互逻辑,包括:
- OpenLayers 地图初始化
- 视图和底图管理
- 点图层管理
- 点位样式生成
- hover popup 与批量 popup
- 区域裁切
- 基地过滤
- 梯级流域图层
- 测量、截图、定位
### 7.2 Cesium 实现
文件:
- [map.cesium.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.cesium.ts)
当前 3D 能力存在,但相对 2D 来说并没有完全对齐。
目前更像是保留了切换入口和部分基础能力,例如:
- viewer 初始化
- 底图加载
- 定位与飞行
- 部分截图能力
多数业务点位和复杂运行逻辑仍然主要围绕 OpenLayers 设计。
## 8. 点图层管理
文件:
- [point-layer-manager.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/ol/point-layer-manager.ts)
这个文件负责 OpenLayers 点图层的生命周期管理。
核心职责包括:
- 按图层 key 创建 `VectorLayer`
- 给每个点图层维护独立的 `VectorSource`
- 把后端点位数据转为 `Feature<Point>`
- 建立图例字段索引
- 控制整层显隐
- 控制图例项对应点位显隐
- 删除点图层
- 获取当前可视范围内点位
### 8.1 当前图层组织方式
当前点位图层的组织方式是:
- 每个业务点图层对应一个 `VectorLayer`
- 每条后端点位数据被转换为一个 `Feature`
- Feature 上会额外挂载运行时字段,例如:
- `_iconUrl`
- `_labelText`
- `_legendVisible`
- `_regionVisible`
- `_layerKey`
### 8.2 图层显隐
图层显隐主要分三类:
- 图层树控制整层显隐
- 图例项控制某一类点位显隐
- 基地裁切控制某些点位在当前基地范围外隐藏
这些状态最终会体现在 Feature 属性和图层可见性上。
## 9. 点位样式与渲染
点位样式主逻辑在:
- [map.ol.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/map.ol.ts)
当前点位样式包含以下内容:
- 图标
- 文字标签
- 字号随缩放动态变化
- 图标大小随缩放动态变化
- 图例显隐状态
- 基地区域显隐状态
- 按 `distance` 字段做缩放级别抽稀
### 9.1 当前样式生成逻辑
当前渲染时会综合判断:
- 是否有图标 URL
- 图例是否可见
- 区域是否可见
- 是否满足当前缩放级别下的距离阈值
满足后才会返回 `Style`
### 9.2 当前标签处理
当前标签会做以下处理:
- 去掉括号
- 最多显示两行
- 每行长度受限
- 超长文本截断并加省略号
- 根据单行或多行情况设置不同偏移
这部分属于当前地图的统一标签显示策略。
## 10. Popup 机制
文件:
- [popup-manager.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/ol/popup-manager.ts)
Popup 分为两类:
- hover 时显示的单点 popup
- 高缩放下批量显示的 popup
### 10.1 Hover Popup
基础 hover popup 由地图鼠标移动事件驱动:
- 检测当前 hover 的要素
- 更新悬停状态
- 在对应坐标显示 popup
### 10.2 批量 Popup
在较高缩放级别下,会进入批量 popup 模式:
- 获取当前视口内所有可见点位
- 逐个测量 popup 尺寸
- 做边界框碰撞判断
- 只渲染不冲突的 popup
这个机制的主要目的是避免高缩放下 popup 过多时完全覆盖地图。
## 11. 基础底图与 GIS 图层
基础底图和 GIS 叠加图层相关能力主要集中在:
- `map.ol.ts`
- `mapurlManage.ts`
- `gisUtils.ts`
### 11.1 `mapurlManage.ts`
文件:
- [mapurlManage.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/mapurlManage.ts)
主要维护:
- GeoServer 服务地址
- WMTS 配置
- XYZ 配置
- 矢量图层相关配置
- 某些专题图层和图例显示参数
### 11.2 `gisUtils.ts`
文件:
- [gisUtils.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/gis/gisUtils.ts)
主要负责:
- 图层树拍平
- 地图配置补全
- 底图配置转换
- 部分标签/偏移相关工具逻辑
## 12. 图层树与图例
### 12.1 图层树 UI
文件:
- [LayerController.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapController/LayerController.vue)
主要职责:
- 展示右侧图层树
- 响应勾选变化
- 把勾选结果转发给编排器和 Store
当前图层树里还包含若干业务互斥规则,例如:
- 视频站和 AI 视频站互斥
- 环保设施和环保设施在建互斥
- 珍稀鱼类和沿程鱼类互斥
### 12.2 图例 UI
文件:
- [mapLegend/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapLegend/index.vue)
主要职责:
- 展示当前页面和当前已选图层对应的图例
- 维护图例项勾选状态
- 把图例点击行为分发给编排器
图例不是固定写死的,而是由后端返回配置和当前已选图层共同决定。
## 13. 地图筛选与控制器
### 13.1 `MapFilter`
文件:
- [mapFilter/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapFilter/index.vue)
当前承担的筛选功能包括:
- 基地筛选
- 时间筛选
- 装机容量筛选
- 关键字搜索
- 个别专题下的额外筛选
### 13.2 `MapController`
文件:
- [mapController/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/mapController/index.vue)
当前承担的地图操作包括:
- 放大缩小
- 图层树显隐
- 2D / 3D 切换
- 量算
- 截图
- 梯级流域专题显示
### 13.3 `BaseLayerSwitcher`
文件:
- [BaseLayerSwitcher/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/components/BaseLayerSwitcher/index.vue)
用于切换:
- 矢量底图
- 地形底图
- 影像底图
## 14. Store 分层
### 14.1 旧主 Store
文件:
- [map.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/store/modules/map.ts)
当前仍然承担大量地图运行态逻辑,例如:
- 图层选中状态
- 图例选中状态
- 图层数据加载
- 图例联动
- 点位缓存
- 图层更新
### 14.2 新拆分 Store
文件:
- [map-config.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-config.store.ts)
- [map-data.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-data.store.ts)
- [map-view.store.ts](file:///d:/wordpack/WholeProcessPlatform/frontend/src/modules/map/stores/map-view.store.ts)
当前已经开始按职责拆分:
- `map-config.store.ts`
- 维护图层树和图例配置
- `map-data.store.ts`
- 维护图层数据缓存和点位数据
- `map-view.store.ts`
- 维护视图相关运行态,例如选中图层、图例勾选、缩放级别、基地状态
这说明地图模块已经在往“编排层 + Store 分层 + 引擎执行层”方向演进。
## 15. 基地裁切与区域过滤
基地逻辑是当前地图模块里的关键能力之一。
入口主要在:
- `GisView.vue`
- `map-orchestrator.ts`
- `map.ol.ts`
整体过程大致如下:
1. 选择基地
2. 编排器接收基地变化
3. 调用地图实例更新基地范围
4. 请求或读取基地边界 GeoJSON
5. 对点位执行区域内外判断
6. 更新 Feature 的 `_regionVisible`
7. 地图样式函数据此决定是否显示
除了点位控制外,基地切换还会影响底图裁切和专题图层显示。
## 16. 后台配置与前台运行时关系
地图模块不仅依赖前台代码,也强依赖后台配置。
### 16.1 后台管理入口
主要页面位于:
- [views/system/map/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/views/system/map/index.vue)
- [views/system/map/components/LayerManagement/index.vue](file:///d:/wordpack/WholeProcessPlatform/frontend/src/views/system/map/components/LayerManagement/index.vue)
### 16.2 后台管理内容
后台可维护的内容主要包括:
- 图层树结构
- 图层是否启用
- 图层接口地址
- 图层基础参数
- 图例元数据
### 16.3 前后台关系
运行时不是前端把所有图层写死,而是:
- 后台维护元数据
- 前端进入页面后加载配置
- 再根据配置请求对应图层数据
- 最后交给地图引擎渲染
因此地图模块本质上是“配置驱动 + 运行时渲染”的模式。
## 17. 当前调用链总结
### 17.1 页面初始化链路
整体调用链如下:
1. `AppMain` 挂载 `GisView`
2. `GisView` 调用 `mapOrchestrator.mountView`
3. `mapOrchestrator` 调用 `MapClass.init`
4. `MapClass` 实际调用 `MapOl.init`
5. 编排器加载图层树和图例配置
6. 编排器初始化底图
7. 编排器加载点位数据
8. `PointLayerManager` 创建点图层并写入 Feature
9. `MapOl` 通过样式函数完成渲染
### 17.2 图层勾选链路
整体调用链如下:
1. 用户勾选 `LayerController`
2. 编排器接收图层选择结果
3. Store 更新图层选中状态
4. 地图实例更新图层显隐
5. 图例重新派生
### 17.3 图例点击链路
整体调用链如下:
1. 用户点击 `MapLegend`
2. 编排器接收图例切换
3. Store 更新图例选中状态
4. 地图实例更新图例对应点位显隐
### 17.4 筛选链路
整体调用链如下:
1. 用户操作 `MapFilter`
2. 编排器接收搜索、时间、基地或容量变化
3. Store 更新运行态
4. 必要时删图层、清缓存、重新请求数据
5. 地图引擎重新渲染
## 18. 当前模块特点
从现有实现看,地图模块有以下几个明显特点:
- 地图容器是全局常驻的
- 页面切换主要依赖 `pageKey` 重新编排
- OpenLayers 是当前 2D 主引擎
- 地图运行态是配置驱动的
- 点位图层按业务图层分层组织
- 图层树、图例、筛选、基地切换之间耦合较深
- 编排层已经出现,但部分旧逻辑仍在 Store 和引擎层混合存在
## 19. 相关文件清单
地图模块当前重点文件如下:
- `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`
- `src/views/system/map/index.vue`
- `src/views/system/map/components/LayerManagement/index.vue`
## 20. 结论
当前地图模块已经形成了比较清晰的主干结构:
- `GisView` 作为入口层
- `map-orchestrator` 作为编排层
- `MapClass` 作为统一门面
- `MapOl` 作为 2D 主执行层
- 多个 Store 共同维护配置、数据和视图状态
- 图层树、图例、筛选器、底图切换器作为前台交互入口
如果后续继续扩展专题图层、复杂筛选、点位布局和高缩放交互,建议都优先沿着这条主干扩展,而不是再回到页面组件里直接堆业务逻辑。

View File

@ -2,35 +2,37 @@
> 本记录用于跟踪项目开发过程中发现的问题,按日期倒序排列,并标注每项问题的发现人及处理状态。
---
***
## 问题列表2026-06-26
| 发现日期 | 解决人 | 编号 | 问题描述 | 状态 | 备注 |
|----------|--------|------|----------|------|------|
| 2026-06-26 | 扈 | 1 | 基础信息图片展示是什么逻辑 | ✅ | |
| 2026-06-26 | 扈 | 2 | 电站专题展示逻辑 | ✅ | |
| 2026-06-26 | | 3 | 实时视频回放 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | | 4 | 预警提示 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | 扈 | 5 | 生态流量 达标率查询不对 | ✅ | |
| 2026-06-26 | 王 | 6 | 鱼类适应性繁殖同期对比NAN | ⚠ | |
| 2026-06-26 | | 7 | 出库水温 综合分析 导出没做 | ⚠ | |
| 2026-06-26 | | 8 | 栖息地-流量监测 没有水位视频和流量视频 字段没数据 | ⚠ | |
| 2026-06-26 | | 9 | 栖息地 水温、水质、流量没有数据没法测 | ⚠ | |
| 2026-06-26 | | 10 | 鱼类增殖站 - 运行数据 要添加字典 | ⚠ | |
| 2026-06-26 | 扈 | 11 | 珍惜植物园 - 种植要求字段不知道 | ✅ | |
| 2026-06-26 | 扈 | 12 | 水生调查断面 - 监测数据 不知道电导率,性腺发育期,早期资源量和种类 | ✅ | |
| 2026-06-26 | 扈 | 13 | 水电告警情况 生态流量 不知道字段 接口传参有问题 | ✅ | |
| 2026-06-26 | | 14 | 地图抽吸 碰撞检测 放大到具体层级锚点抽吸了但是popup弹框还在显示 | ⚠ | |
| 2026-06-26 | | 15 | 地图比如积石峡 放大到层级被公伯峡隐藏掉了,然后在放大才显示 抽吸的问题 | ⚠ | |
| 2026-06-26 | | 16 | 运行情况 计划开始运行时间 接口没接 | ⚠ | |
| ---------- | ------ | -- | ------------------------------------- | -- | ------ |
| 2026-06-26 | 扈 | 1 | 基础信息图片展示是什么逻辑 | ✅ | <br /> |
| 2026-06-26 | 扈 | 2 | 电站专题展示逻辑 | ✅ | <br /> |
| 2026-06-26 | <br /> | 3 | 实时视频回放 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | <br /> | 4 | 预警提示 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | 扈 | 5 | 生态流量 达标率查询不对 | ✅ | <br /> |
| 2026-06-26 | 王 | 6 | 鱼类适应性繁殖同期对比 NAN | ⚠ | <br /> |
| 2026-06-26 | 扈 | 7 | 出库水温 综合分析 导出没做 | ✅ | <br /> |
| 2026-06-26 | <br /> | 8 | 栖息地-流量监测 没有水位视频和流量视频 字段没数据 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 9 | 栖息地 水温、水质、流量没有数据没法测 | ⚠ | <br /> |
| 2026-06-26 | 扈 | 10 | 鱼类增殖站 - 运行数据 要添加字典 | ✅ | <br /> |
| 2026-06-26 | 扈 | 11 | 珍惜植物园 - 种植要求字段不知道 | ✅ | <br /> |
| 2026-06-26 | 扈 | 12 | 水生调查断面 - 监测数据 不知道电导率,性腺发育期,早期资源量和种类 | ✅ | <br /> |
| 2026-06-26 | 扈 | 13 | 水电告警情况 生态流量 不知道字段 接口传参有问题 | ✅ | <br /> |
| 2026-06-26 | <br /> | 14 | 地图抽吸 碰撞检测 放大到具体层级锚点抽吸了但是 popup 弹框还在显示 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 15 | 地图比如积石峡 放大到层级被公伯峡隐藏掉了,然后在放大才显示 抽吸的问题 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 16 | 运行情况 计划开始运行时间 接口没接 | ⚠ | <br /> |
---
***
## 状态说明
- ✅ 已解决 / 已确认
- ⚠ 处理中 / 待解决
- ❌ 阻塞 / 未开始
---
***

View File

@ -7,7 +7,6 @@ const appStore = useAppStore();
</script>
<template>
<!-- 1. 数据查询 电站信息 编辑删除 自定义筛选列 没做-->
<el-config-provider :locale="appStore.locale" :size="appStore.size">
<a-config-provider :theme="usetTheme" :locale="locale">
<router-view />

View File

@ -119,6 +119,22 @@ export function getWaterDataMonthList(data) {
data
});
}
// 水质监测数据 编辑
export function updateWaterDataInfo(data) {
return request({
url: '/wq/updateWqRsData',
method: 'post',
data
});
}
// 水质监测数据 删除
export function deleteWaterDataInfo(data) {
return request({
url: '/wq/removeKendoByIds',
method: 'post',
data
});
}
// 垂向 - 列表
export function getVerticalList(data) {
return request({
@ -127,6 +143,22 @@ export function getVerticalList(data) {
data
});
}
// 垂向 - 编辑
export function updateVerticalInfo(data) {
return request({
url: '/wt/cxDetail/updateWtvtRData',
method: 'post',
data
});
}
// 垂向 - 删除
export function deleteVerticalInfo(data) {
return request({
url: '/wt/cxDetail/removeKendoByIds',
method: 'post',
data
});
}
// 表层水温 - 断面
export function getSurfaceTempSectionList(data) {
return request({
@ -159,6 +191,23 @@ export function getSurfaceTempMonthList(data) {
data
});
}
// 表层水温 - 编辑
export function updateSurfaceTempInfo(data) {
return request({
url: '/wt/alongDetail/updateWtrvRData',
method: 'post',
data
});
}
// 表层水温 - 删除
export function deleteSurfaceTempInfo(data) {
return request({
url: '/wt/alongDetail/drtp/removeKendoByIds',
method: 'post',
data
});
}
// 表层水温 - 数据分析 - 断面
export function getSurfaceTempAnalysisList(data) {
return request({
@ -242,3 +291,20 @@ export function getFlowStationMonthList(data) {
data
});
}
// 流量站 - 编辑
export function updateFlowStationInfo(data) {
return request({
url: '/zq/river/updateRiverRData',
method: 'post',
data
});
}
// 流量站 - 删除
export function deleteFlowStationInfo(data) {
return request({
url: '/zq/river/removeKendoByIds',
method: 'post',
data
});
}

View File

@ -35,7 +35,7 @@ export function postIdUrl(data: any) {
// 查询站点建设状态
export function getBuildState(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList',
url: '/wt/vmsstbprpt/GetKendoList',
method: 'post',
data: {
filter: data.filter,

View File

@ -40,7 +40,10 @@
import { ref, computed, onMounted, watch, nextTick, h, useSlots } from 'vue';
import { message, Tooltip } from 'ant-design-vue';
import { calcTableScrollY } from '@/utils/index';
import { exportTable as exportTableUtil } from '@/utils/exportExcel';
import {
exportTable as exportTableUtil,
type ExportColumn
} from '@/utils/exportExcel';
// --- Types ---
interface Props {
@ -328,6 +331,13 @@ interface ExportTableParams {
callback?: (message: string | boolean) => void;
tbtitle?: string;
tbsub?: string;
summary?: {
sheetName: string;
data: any[];
columns: ExportColumn[];
tbtitle?: string;
tbsub?: string;
};
}
const defaultExportCallback = (result: string | boolean) => {
@ -341,7 +351,7 @@ const defaultExportCallback = (result: string | boolean) => {
};
const exportTable = async (params: ExportTableParams): Promise<void> => {
const { fileName, callback, tbtitle, tbsub } = params;
const { fileName, callback, tbtitle, tbsub, summary } = params;
const cb = callback || defaultExportCallback;
// listUrl
@ -433,6 +443,7 @@ const exportTable = async (params: ExportTableParams): Promise<void> => {
data: records,
columns: props.columns,
fileName,
appendTables: summary ? [summary] : [],
callback: result => {
cb(result);
resolve();

View File

@ -90,9 +90,9 @@ import {
getEcologyList1,
getEcologyList2,
getEcologyList3,
getEcologyProtectType,
getEcologyYear
} from '@/api/mapModal';
import { getDictItemsByCode } from '@/api/dict';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
import CommonAttachmentModal from './CommonAttachmentModal.vue';
@ -473,7 +473,7 @@ const loadBatchOptions = async () => {
const loadProtectTypeOptions = async () => {
protectTypeLoading.value = true;
try {
const res: any = await getEcologyProtectType({
const res: any = await getDictItemsByCode({
dictCode: 'FISHTY'
});
const records = res?.data?.data || res?.data?.records || res?.data || [];
@ -486,8 +486,8 @@ const loadProtectTypeOptions = async () => {
...records
.filter((item: any) => String(item?.level) !== '1')
.map((item: any) => ({
label: item?.dictMeaning,
value: item?.dictValue
label: item?.dictName,
value: item?.itemCode
}))
.filter((item: any) => item.label && item.value)
];

View File

@ -301,12 +301,10 @@ const buildFishOptionItem = (item: any): OperationFilterOption => ({
});
const buildDictOptionItem = (item: any): OperationFilterOption => ({
label:
item?.dictMeaning || item?.label || item?.text || item?.dictLabel || '-',
value: item?.dictValue ?? item?.value ?? item?.code ?? '',
name:
item?.dictMeaning || item?.label || item?.text || item?.dictLabel || '-',
id: item?.dictValue ?? item?.value ?? item?.code ?? ''
label: item?.dictName || item?.label || item?.text || item?.dictLabel || '-',
value: item?.itemCode ?? item?.value ?? item?.code ?? '',
name: item?.dictName || item?.label || item?.text || item?.dictLabel || '-',
id: item?.itemCode ?? item?.value ?? item?.code ?? ''
});
const getFieldOptions = (field: OperationFilterField) => {

View File

@ -13,6 +13,7 @@ import {
yuLeiShouJin,
yuleifuhua
} from './proliferationColumns';
import { getDictItemsByCode } from '@/api/dict';
export interface OperationFilterOption {
label: string;
@ -152,7 +153,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'ctmo',
label: '培育方式',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'CTMO'
@ -218,7 +219,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'ftype',
label: '饲料种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'FEEDTP'
@ -312,7 +313,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'fpitocintype',
label: '催产剂种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'FPITOCINTYPE'
@ -361,7 +362,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'fishsrc',
label: '亲鱼来源',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'BSSR'
@ -430,7 +431,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'ftype',
label: '饲料种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'FEEDTP'
@ -478,7 +479,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'method',
label: '用药方式',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'method'
@ -488,7 +489,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'treateffect',
label: '治疗效果',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'TREATEFFECT'
@ -498,7 +499,7 @@ export const operationStepList: OperationStepItem[] = [
type: 'select',
name: 'fishdrugtype',
label: '鱼药种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
url: '/system/dictionary/getDictItemsByCode',
method: 'get',
params: {
dictCode: 'FISHDRUGTYPE'

View File

@ -19,6 +19,7 @@
v-if="isSummaryMode"
class="search-btn"
@click="handleExport"
:loading="exportLoading"
>
导出
</a-button>
@ -49,6 +50,7 @@
</div>
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:scrollY="480"
:scrollX="tableScrollX"
:columns="currentColumns"
@ -82,6 +84,8 @@ import BasicTable from '@/components/BasicTable/index.vue';
import { DateSetting } from '@/utils/enumeration';
const modelStore = useModelStore();
const exportLoading = ref(false);
const tableRef = ref<any>();
const props = defineProps({
isActive: { type: Boolean, default: false },
@ -617,8 +621,17 @@ const fetchData = async () => {
};
const handleSearch = () => fetchData();
const handleExport = () => {
/* 导出逻辑 */
const handleExport = () => {
exportLoading.value = true;
tableRef.value
.exportTable({
fileName: `综合分析_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
};
const initData = () => {

View File

@ -19,6 +19,7 @@
v-if="isSummaryMode"
class="search-btn"
@click="handleExport"
:loading="exportLoading"
>
导出
</a-button>
@ -49,6 +50,7 @@
</div>
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:scrollY="360"
:scrollX="tableScrollX"
:columns="currentColumns"
@ -140,6 +142,8 @@ const showTimeConfig = {
minuteStep: 5,
secondStep: 60
};
const tableRef = ref<any>();
const exportLoading = ref(false);
const disabledDate = (current: Dayjs) =>
current && current.isAfter(dayjs(), 'day');
@ -446,6 +450,42 @@ const summaryRows = computed(() => {
return rows;
});
const formatSummaryExportText = (cell?: { value: string; time: string }) => {
if (!cell) return '-';
const value = cell.value ?? '-';
if (!cell.time) return value;
const formattedTime = dayjs(cell.time).isValid()
? dayjs(cell.time).format('YYYY-MM-DD HH:mm')
: cell.time;
return `${value}\n时间:${formattedTime}`;
};
const summaryExportColumns = computed(() => [
{
title: '统计项',
dataIndex: 'label',
width: 120
},
...summaryColumns.value.map((col: any) => ({
title: col.title,
dataIndex: col.dataIndex,
width: col.width
}))
]);
const summaryExportData = computed(() =>
summaryRows.value.map((row: any) => {
const exportRow: Record<string, string> = { label: row.label };
summaryColumns.value.forEach((col: any) => {
exportRow[col.dataIndex] = formatSummaryExportText(
row.cells[col.dataIndex]
);
});
return exportRow;
})
);
// ==================== ====================
function shouldStagger(width: number, dataLen: number) {
return dataLen > Math.floor(width / 100);
@ -897,8 +937,25 @@ const fetchData = async () => {
};
const handleSearch = () => fetchData();
//
const handleExport = () => {
/* 导出逻辑 */
exportLoading.value = true;
tableRef.value
.exportTable({
fileName: `综合分析_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`,
summary:
summaryExportData.value.length > 0
? {
sheetName: '综合分析汇总',
data: summaryExportData.value,
columns: summaryExportColumns.value
}
: undefined
})
.finally(() => {
exportLoading.value = false;
});
};
const initData = () => {

View File

@ -4,14 +4,7 @@
<div ref="popupRef" class="map-popup-container" style="display: none"></div>
<!-- - 1. 切换菜单的时候图层没有切换 是因为接口的原因现在没有接口
2. 切换菜单的时候图例现在是不对的默认选中的数据他们没有做处理
1. 图层切换显示完成
2. 鼠标悬停点击完成
3. 文字重叠完成
4. 搜索 (完成)
5. 公共弹框基础信息是eng的时候才显示电站专题
6.图层加载的时候如果接口报错loading 一直加载完成
popup 隐藏还是有问题 完成 -->
-->
<!-- 地图图例 -->
<MapLegend />

View File

@ -1,5 +1,6 @@
import { ref, computed } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import { DateSetting } from '@/utils/enumeration';
export interface UseTimeScaleOptions {
defaultTimeScale?: string;
@ -7,6 +8,11 @@ export interface UseTimeScaleOptions {
onReset?: (values: any) => void;
}
export interface TimeShortcutItem {
label: string;
value: [Dayjs, Dayjs];
}
export function useTimeScale(options: UseTimeScaleOptions = {}) {
const defaultTimeScale = options.defaultTimeScale || 'tm';
const currentTimeScale = ref(defaultTimeScale);
@ -36,6 +42,70 @@ export function useTimeScale(options: UseTimeScaleOptions = {}) {
return undefined;
});
const getTmShortcutList = (): TimeShortcutItem[] => {
const getStartTime = () => dayjs().startOf('day').startOf('hour');
const getStartHourTime = () => dayjs().startOf('hour');
return [
{
label: '此刻',
value: [getStartTime(), getStartTime()]
},
{
label: '今天',
value: [getStartTime().startOf('day'), getStartTime().endOf('day')]
},
{
label: '昨天',
value: [
getStartTime().subtract(1, 'day'),
getStartTime().subtract(1, 'day').endOf('day')
]
},
{
label: '最近7小时',
value: [getStartHourTime().subtract(7, 'hour'), getStartHourTime()]
},
{
label: '本月',
value: [getStartTime().startOf('month'), getStartTime().endOf('month')]
}
];
};
const getMonthShortcutList = (): TimeShortcutItem[] => {
const now = dayjs();
return [
{
label: '本月',
value: [now.startOf('month'), now.endOf('month')]
},
{
label: '最近三个月',
value: [now.subtract(3, 'month').startOf('month'), now.endOf('month')]
},
{
label: '今年',
value: [now.startOf('year'), now.endOf('year')]
},
{
label: '最近半年',
value: [now.subtract(5, 'month').startOf('month'), now.endOf('month')]
},
{
label: '最近一年',
value: [now.subtract(12, 'month').startOf('month'), now.endOf('month')]
}
];
};
const timeShortcutList = computed<TimeShortcutItem[]>(() => {
if (currentTimeScale.value === 'tm') return getTmShortcutList();
if (currentTimeScale.value === 'dt') return DateSetting.RangeButton.days1;
return getMonthShortcutList();
});
// 设置默认时间范围
const setDefaultTimeRange = (timeScale: string) => {
const now = dayjs();
@ -125,6 +195,7 @@ export function useTimeScale(options: UseTimeScaleOptions = {}) {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -25,6 +25,46 @@ export interface ExportParams {
tbsub?: string;
/** 导出回调 */
callback?: (message: string | boolean) => void;
/** 附加导出表格,默认追加到主表末尾 */
appendTables?: ExportAppendTableConfig[];
}
export interface ExportAppendTableConfig {
/** 追加表格数据 */
data: any[];
/** 追加表格列配置 */
columns: ExportColumn[];
}
function normalizeExportValue(value: any): any {
if (value === null || value === undefined || value === '') return value;
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (Array.isArray(value)) {
const normalized = value
.map(item => normalizeExportValue(item))
.filter(item => item !== null && item !== undefined && item !== '');
return normalized.length > 0 ? normalized.join('') : '';
}
if (typeof value === 'object') {
if ('children' in value) {
return normalizeExportValue(value.children);
}
if ('value' in value) {
return normalizeExportValue(value.value);
}
}
return String(value);
}
/**
@ -48,23 +88,6 @@ function downloadExcel({
try {
const workbook = new ExcelJS.Workbook();
for (const sheet of sheets) {
const { columns, dataSource, tbtitle, tbsub, sheetName } = sheet;
// 添加 sheet
const worksheet = workbook.addWorksheet(sheetName || 'Sheet1', {
properties: { tabColor: { argb: 'FFC0000' } }
});
// 固定列宽(根据定义宽度计算导出宽度)
worksheet.columns = columns.map(col => {
const definedWidth = col.width || 150;
// < 200: 29.22, 200-300: 39.22, 300-400: 59.22
const exportWidth = 29.22 + Math.max(0, (definedWidth - 150) / 50) * 10;
return { width: exportWidth };
});
// 样式定义
const headerStyle: Partial<ExcelJS.Style> = {
font: { bold: true, size: 14, name: '微软雅黑' },
alignment: {
@ -83,28 +106,66 @@ function downloadExcel({
}
};
// 当前行号
let currentRow = 1;
const setWorksheetColumns = (
worksheet: ExcelJS.Worksheet,
columnGroups: ExportColumn[][]
) => {
const maxColumnCount = Math.max(
...columnGroups.map(cols => cols.length),
0
);
worksheet.columns = Array.from({ length: maxColumnCount }, (_, index) => {
const definedWidth = Math.max(
...columnGroups.map(cols => cols[index]?.width || 150),
150
);
const exportWidth = 29.22 + Math.max(0, (definedWidth - 150) / 50) * 10;
return { width: exportWidth };
});
};
// 写入表头标题(如果有)
if (tbtitle) {
const writeTableBlock = ({
worksheet,
columns,
dataSource,
startRow,
title,
subTitle,
freezeHeader = false,
showHeader = true
}: {
worksheet: ExcelJS.Worksheet;
columns: ExportColumn[];
dataSource: any[];
startRow: number;
title?: string;
subTitle?: string;
freezeHeader?: boolean;
showHeader?: boolean;
}) => {
let currentRow = startRow;
if (title) {
const titleRow = worksheet.getRow(currentRow);
titleRow.getCell(1).value = tbtitle;
titleRow.getCell(2).value = tbsub || '';
titleRow.getCell(1).value = title;
titleRow.getCell(2).value = subTitle || '';
currentRow++;
}
// 写入表头行
if (showHeader) {
const headerRow = worksheet.getRow(currentRow);
columns.forEach((col, index) => {
headerRow.getCell(index + 1).value = col.title;
headerRow.getCell(index + 1).style = headerStyle;
});
// 冻结表头行
worksheet.views = [{ state: 'frozen', ySplit: currentRow }];
currentRow++;
// 写入数据行
if (freezeHeader) {
worksheet.views = [{ state: 'frozen', ySplit: currentRow }];
}
currentRow++;
}
dataSource.forEach(record => {
const row = worksheet.getRow(currentRow);
columns.forEach((col, index) => {
@ -113,15 +174,18 @@ function downloadExcel({
let value = record[dataIndex];
// 如果有 customRender优先使用它处理后的值
if (customRender) {
try {
const renderResult = customRender({
text: value,
record,
index: currentRow - (tbtitle ? 3 : 2)
index:
currentRow - startRow - (title ? 1 : 0) - (showHeader ? 1 : 0)
});
value = renderResult !== undefined ? renderResult : value;
value =
renderResult !== undefined
? normalizeExportValue(renderResult)
: value;
} catch {
// customRender 执行失败,使用原始值
}
@ -133,12 +197,60 @@ function downloadExcel({
const cell = row.getCell(index + 1);
cell.value = value;
// 所有单元格统一应用样式,确保 - 占位符也居中对齐
cell.style = dataStyle;
});
currentRow++;
});
return currentRow;
};
for (const sheet of sheets) {
const {
columns,
dataSource,
tbtitle,
tbsub,
sheetName,
appendTables = []
} = sheet as {
sheetName: string;
columns: ExportColumn[];
dataSource: any[];
tbtitle?: string;
tbsub?: string;
appendTables?: ExportAppendTableConfig[];
};
// 添加 sheet
const worksheet = workbook.addWorksheet(sheetName || 'Sheet1', {
properties: { tabColor: { argb: 'FFC0000' } }
});
setWorksheetColumns(worksheet, [
columns,
...appendTables.map(table => table.columns)
]);
let currentRow = writeTableBlock({
worksheet,
columns,
dataSource,
startRow: 1,
title: tbtitle,
subTitle: tbsub,
freezeHeader: true
});
appendTables.forEach(table => {
currentRow = writeTableBlock({
worksheet,
columns: table.columns,
dataSource: table.data,
startRow: currentRow,
showHeader: false
});
});
}
// 生成 Excel 文件并下载
@ -190,6 +302,24 @@ function resolveColumnTitle(col: ExportColumn): string | null {
return null;
}
function resolveExportColumns(columns: ExportColumn[]): ExportColumn[] {
const exportColumns: ExportColumn[] = [];
for (const col of columns) {
if (!col.dataIndex || col.dataIndex === 'action' || col.excludeFromExport) {
continue;
}
const resolvedTitle = resolveColumnTitle(col);
if (!resolvedTitle) continue;
exportColumns.push({
...col,
title: resolvedTitle
});
}
return exportColumns;
}
/**
*
*/
@ -207,40 +337,34 @@ export function exportTable({
fileName = '导出数据',
tbtitle,
tbsub,
callback
callback,
appendTables = []
}: ExportParams) {
if (!data || data.length === 0) {
callback?.('没有可导出的数据');
return;
}
// 过滤掉不需要导出的列,并解析函数类型的 title
const exportColumns: ExportColumn[] = [];
for (const col of columns) {
if (!col.dataIndex || col.dataIndex === 'action' || col.excludeFromExport) {
continue;
}
const resolvedTitle = resolveColumnTitle(col);
// 如果没有解析出标题,跳过该列
if (!resolvedTitle) continue;
exportColumns.push({
...col,
title: resolvedTitle
});
}
downloadExcel({
filename: fileName,
sheets: [
const exportColumns = resolveExportColumns(columns);
const sheets = [
{
sheetName: fileName,
columns: exportColumns,
dataSource: data,
tbtitle,
tbsub
tbsub,
appendTables: appendTables
.filter(table => Array.isArray(table.data) && table.data.length > 0)
.map(table => ({
data: table.data,
columns: resolveExportColumns(table.columns)
}))
}
],
];
downloadExcel({
filename: fileName,
sheets,
callback
});
}

View File

@ -15,6 +15,14 @@
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="columns"
:search-params="{
sort: [
{
field: 'orderIndex',
order: 'asc'
}
]
}"
:list-url="getApprovalStatusList"
>
</BasicTable>

View File

@ -2,7 +2,7 @@
<!-- 第一步输入修改依据 -->
<a-modal
v-model:open="dataSourceVisible"
title="删除电站"
:title="title"
ok-text="确定"
cancel-text="取消"
@ok="handleDataSourceConfirm"
@ -24,13 +24,14 @@
ok-text="确认删除"
cancel-text="取消"
:ok-button-props="{ danger: true }"
:confirm-loading="confirmLoading"
@ok="handleFinalDelete"
>
<div>
<p style="color: red; font-weight: bold">请慎重操作</p>
<p>电站名称{{ deleteRecord?.stnm }}</p>
<p>{{ label }}名称{{ deleteRecord?.stnm }}</p>
<p>修改依据{{ dataSource || '无' }}</p>
<p>确定要删除该电站</p>
<p>确定要删除该{{ label }}</p>
</div>
</a-modal>
</template>
@ -40,10 +41,26 @@ import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { deletePowerInfo } from '@/api/DataQueryMenuModule';
const props = withDefaults(
defineProps<{
/** 自定义删除函数,接收 (record, reason) 参数 */
deleteFn?: (record: any, reason: string) => Promise<any>;
/** 弹窗标题 */
title?: string;
/** 显示标签(如"电站"、"数据" */
label?: string;
}>(),
{
title: '删除电站',
label: '电站'
}
);
const emit = defineEmits(['success']);
const dataSourceVisible = ref(false);
const confirmVisible = ref(false);
const confirmLoading = ref(false);
const dataSource = ref('');
const deleteRecord = ref<any>(null);
const onSuccess = ref<Function>(() => {});
@ -64,14 +81,21 @@ const handleDataSourceConfirm = () => {
//
const handleFinalDelete = async () => {
confirmLoading.value = true; //
try {
let res: any;
if (props.deleteFn) {
// 使
res = await props.deleteFn(deleteRecord.value, dataSource.value);
} else {
// 使
const params = {
ids: [deleteRecord.value.stcd],
source: dataSource.value
};
const res = await deletePowerInfo(params);
console.log(res);
if (res?.code == 0) {
res = await deletePowerInfo(params);
}
if (res?.code == 0 || res?.success) {
message.success('删除成功');
confirmVisible.value = false;
onSuccess.value();
@ -80,6 +104,8 @@ const handleFinalDelete = async () => {
}
} catch (error) {
message.error('删除失败,请重试');
} finally {
confirmLoading.value = false; //
}
};

View File

@ -149,14 +149,6 @@ const tableScrollX = computed(() =>
)
);
const columns: any = [
{
key: 'index',
title: '序号',
dataIndex: 'index',
visible: true,
width: 80,
fixed: 'left'
},
{
key: 'name',
title: '中文名称',
@ -165,6 +157,7 @@ const columns: any = [
width: 150,
ellipsis: true,
fixed: 'left',
sort: true,
customRender: ({ text }: any) => {
return text
? h('span', { style: { color: '#2f6b98', cursor: 'pointer' } }, text)
@ -178,6 +171,7 @@ const columns: any = [
visible: true,
width: 200,
ellipsis: true,
sort: true,
fixed: 'left'
},
{
@ -187,6 +181,7 @@ const columns: any = [
visible: true,
width: 100,
ellipsis: true,
sort: true,
fixed: 'left'
},
{
@ -194,6 +189,7 @@ const columns: any = [
title: '分类',
dataIndex: 'typeName',
visible: true,
sort: true,
width: 100,
ellipsis: true
},
@ -202,6 +198,7 @@ const columns: any = [
title: '种',
dataIndex: 'species',
visible: true,
sort: true,
width: 150,
ellipsis: true
},
@ -210,6 +207,7 @@ const columns: any = [
title: '属',
dataIndex: 'genus',
visible: true,
sort: true,
width: 100,
ellipsis: true
},
@ -218,6 +216,7 @@ const columns: any = [
title: '科',
dataIndex: 'family',
visible: true,
sort: true,
width: 100,
ellipsis: true
},
@ -227,6 +226,7 @@ const columns: any = [
dataIndex: 'orders',
visible: true,
width: 150,
sort: true,
ellipsis: true
},
// {
@ -244,6 +244,7 @@ const columns: any = [
dataIndex: 'alias',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -252,6 +253,7 @@ const columns: any = [
dataIndex: 'fsz',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -260,6 +262,7 @@ const columns: any = [
dataIndex: 'shapedesc',
visible: true,
width: 300,
sort: true,
ellipsis: true
},
{
@ -268,6 +271,7 @@ const columns: any = [
dataIndex: 'habitation',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -276,6 +280,7 @@ const columns: any = [
dataIndex: 'habitMigrat',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -284,6 +289,7 @@ const columns: any = [
dataIndex: 'feedingHabit',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -292,6 +298,7 @@ const columns: any = [
dataIndex: 'food',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -300,6 +307,7 @@ const columns: any = [
dataIndex: 'timeFeed',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -308,6 +316,7 @@ const columns: any = [
dataIndex: 'spawnMonth',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -316,6 +325,7 @@ const columns: any = [
dataIndex: 'orignDate',
visible: true,
width: 300,
sort: true,
ellipsis: true
},
{
@ -324,6 +334,7 @@ const columns: any = [
dataIndex: 'description',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
@ -332,6 +343,7 @@ const columns: any = [
dataIndex: 'pretemp',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -340,6 +352,7 @@ const columns: any = [
dataIndex: 'flowRate',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -348,6 +361,7 @@ const columns: any = [
dataIndex: 'depth',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -356,6 +370,7 @@ const columns: any = [
dataIndex: 'wqtq',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -364,6 +379,7 @@ const columns: any = [
dataIndex: 'spawnCharact',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -372,6 +388,7 @@ const columns: any = [
dataIndex: 'botmMater',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -380,6 +397,7 @@ const columns: any = [
dataIndex: 'ptypeName',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -388,6 +406,7 @@ const columns: any = [
dataIndex: 'specOriginName',
visible: true,
width: 100,
sort: true,
ellipsis: true
},
{
@ -396,6 +415,7 @@ const columns: any = [
dataIndex: 'vlsr',
visible: true,
width: 150,
sort: true,
ellipsis: true
}
// {

View File

@ -84,7 +84,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
stcd: '0086601020VP000006',
mway: '1'
};

View File

@ -22,7 +22,7 @@
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
v-for="item in timeDateList"
:key="item"
@click="handleRangeClick(item)"
>
@ -72,6 +72,27 @@ import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
const timeDateList = ref([
{
label: '一年',
value: [dayjs().startOf('year'), dayjs().endOf('year')] //
},
{
label: '两年',
value: [dayjs().subtract(1, 'year').startOf('year'), dayjs().endOf('year')] //
},
{
label: '三年',
value: [dayjs().subtract(2, 'year').startOf('year'), dayjs().endOf('year')] //
},
{
label: '五年',
value: [
dayjs().subtract(4, 'year').startOf('year'),
dayjs().startOf('year')
] //
}
]);
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;

View File

@ -18,12 +18,13 @@
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -43,12 +44,13 @@
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -68,7 +70,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
const emit = defineEmits<{
@ -76,7 +77,6 @@ const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
@ -86,7 +86,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
rstcd: '008660306300000001',
basin: null,
ruleType: null,
timeScale: 'tm'
@ -100,6 +100,7 @@ const {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -37,36 +37,29 @@ import {
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const currentSearchParams = ref<any>({});
const sort = computed(() => {
return [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'siteStepSort', dir: 'asc' },
currentSearchParams.value.timeScale == 'tm'
? { field: 'tm', dir: 'desc' }
: null,
currentSearchParams.value.timeScale == 'dt'
? { field: 'dt', dir: 'desc' }
: null,
currentSearchParams.value.timeScale == 'month'
? { field: 'year', dir: 'desc' }
: null,
currentSearchParams.value.timeScale == 'month'
? { field: 'month', dir: 'desc' }
: null
].filter(Boolean);
});
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
// timeScale API
@ -214,7 +207,7 @@ const ruleColumnsMap: any = {
dataIndex: 'mwrSfdbName',
visible: true,
sort: true,
width: 170,
width: 180,
title: () =>
h('span', { style: 'display:flex;align-items:center;gap:4px' }, [
'水利部要求是否达标',
@ -256,7 +249,7 @@ const ruleColumnsMap: any = {
dataIndex: 'avqLimit',
visible: true,
sort: true,
width: 150
width: 160
},
{
key: 'avqSfdb',
@ -264,7 +257,7 @@ const ruleColumnsMap: any = {
dataIndex: 'avqSfdbName',
visible: true,
sort: true,
width: 210,
width: 230,
title: () =>
h('span', { style: 'display:flex;align-items:center;gap:4px' }, [
'多年平均流量*10%是否达标',

View File

@ -0,0 +1,327 @@
<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>

View File

@ -18,12 +18,13 @@
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -43,12 +44,13 @@
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -68,7 +70,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
import { getFlowStationSectionList } from '@/api/DataQueryMenuModule';
@ -88,7 +89,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
stcd: '008640203500001067',
timeScale: 'tm'
};
@ -100,6 +101,7 @@ const {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -19,59 +19,99 @@
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, h } from 'vue';
import { ref, computed, onMounted, nextTick } from 'vue';
import dayjs from 'dayjs';
import FlowStationSearch from './FlowStationSearch.vue';
import { Tooltip } from 'ant-design-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,
getFlowStationDayList,
getFlowStationMonthList
getFlowStationMonthList,
deleteFlowStationInfo
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const sort = computed(() => {
//
const sortMap = {
tm: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
],
dt: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'dt', dir: 'desc' }
],
month: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'year', dir: 'desc' },
{ field: 'month', dir: 'desc' }
]
};
// timeScale
return sortMap[currentSearchParams.value.timeScale] || [];
});
const tableRef = ref();
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(() => {
const timeScale = currentSearchParams.value.timeScale;
console.log('timeScale:', timeScale);
if (timeScale === 'dt') return getFlowStationDayList;
if (timeScale === 'month') return getFlowStationMonthList;
return getFlowStationList; // tm
@ -121,6 +161,7 @@ const allColumns: any[] = [
title: '水位(m)',
dataIndex: 'z',
width: 100,
sort: true,
customRender: ({ record }: any) => record.z?.toFixed(2) || '-'
},
{
@ -128,6 +169,7 @@ const allColumns: any[] = [
title: '流量(m³/s)',
dataIndex: 'q',
width: 100,
sort: true,
customRender: ({ record }: any) => record.q?.toFixed(2) || '-'
},
{
@ -135,6 +177,7 @@ const allColumns: any[] = [
title: '流速(m³/s)',
dataIndex: 'v',
width: 100,
sort: true,
customRender: ({ record }: any) => record.v?.toFixed(2) || '-'
}
];
@ -169,10 +212,19 @@ const timeColumn = computed(() => ({
//
const Columns = computed(() => {
const result = [...allColumns];
const result = allColumns.map(col => ({ ...col }));
//
const baseIndex = result.findIndex(c => c.key === 'stlc');
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;
});
@ -205,8 +257,35 @@ 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) => {
console.log(values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {

View File

@ -0,0 +1,252 @@
<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>

View File

@ -18,12 +18,13 @@
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -43,12 +44,13 @@
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -68,7 +70,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
import { getSurfaceTempSectionList } from '@/api/DataQueryMenuModule';
@ -88,7 +89,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
stcd: '008640202300001013',
timeScale: 'tm'
};
@ -100,6 +101,7 @@ const {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -19,59 +19,97 @@
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, h } from 'vue';
import { ref, computed, onMounted, nextTick } from 'vue';
import dayjs from 'dayjs';
import SurfaceTempSearch from './SurfaceTempSearch.vue';
import { Tooltip } from 'ant-design-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
getSurfaceTempMonthList,
deleteSurfaceTempInfo
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const sort = computed(() => {
//
const sortMap = {
tm: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'ennm', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
],
dt: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'dt', dir: 'desc' }
],
month: [
{ field: 'stnm', dir: 'desc' },
{ field: 'year', dir: 'desc' },
{ field: 'month', dir: 'desc' }
]
};
// timeScale
return sortMap[currentSearchParams.value.timeScale] || [];
});
const tableRef = ref();
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(() => {
const timeScale = currentSearchParams.value.timeScale;
console.log('timeScale:', timeScale);
if (timeScale === 'dt') return getSurfaceTempDayList;
if (timeScale === 'month') return getSurfaceTempMonthList;
return getSurfaceTempList; // tm
@ -94,7 +132,6 @@ const allColumns: any[] = [
title: '断面名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
@ -103,7 +140,6 @@ const allColumns: any[] = [
title: '水温(℃)',
dataIndex: 'wt',
visible: true,
width: 200,
sort: true,
fixed: 'left',
customRender: ({ record }: any) => record.wt?.toFixed(1) || '-',
@ -122,7 +158,6 @@ const timeColumn = computed(() => ({
},
dataIndex: 'tm',
visible: true,
width: 150,
sort: true,
ellipsis: true,
customRender: ({ record }: any) => {
@ -141,10 +176,19 @@ const timeColumn = computed(() => ({
//
const Columns = computed(() => {
const result = [...allColumns];
const result = allColumns.map(col => ({ ...col }));
//
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;
});
@ -177,8 +221,35 @@ const exportBtn = () => {
});
};
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) => {
console.log(values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {

View File

@ -0,0 +1,288 @@
<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>

View File

@ -18,12 +18,13 @@
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -43,12 +44,13 @@
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -68,7 +70,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
import { getSurfaceTempSectionList } from '@/api/DataQueryMenuModule';
@ -88,7 +89,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
stcd: '008640202300001012',
timeScale: 'tm'
};
@ -100,6 +101,7 @@ const {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -58,9 +58,44 @@
:enable-row-highlight="false"
: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>
</div>
</div>
</template>
@ -76,9 +111,10 @@ import {
import dayjs from 'dayjs';
import * as echarts from 'echarts';
import VerticalTempSearch from './VerticalTempSearch.vue';
import { Tooltip } from 'ant-design-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 } from '@/api/DataQueryMenuModule';
import { getVerticalList, deleteVerticalInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const sort = ref<any>([
@ -93,6 +129,9 @@ const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
const tableData = ref<any[]>([]);
const editVisible = ref(false);
const editRecord = ref<any>(null);
const deleteModalRef = ref();
//
const chartRef = ref<HTMLElement>();
@ -154,7 +193,7 @@ const tableColumns = computed(() => {
{
title: '时间',
dataIndex: 'dt',
width: 140,
width: 160,
fixed: 'left',
customRender: ({ text }: any) =>
text
@ -183,7 +222,19 @@ const tableColumns = computed(() => {
}
}));
return [...fixedCols, ...dataCols];
const actionCols =
currentSearchParams.value.timeScale === 'tm'
? [
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 120
}
]
: [];
return [...fixedCols, ...dataCols, ...actionCols];
});
const tableScrollX = computed(() => {
@ -257,10 +308,6 @@ const updateChart = (selectedRows: any[]) => {
});
const option = {
title: {
text: '垂向水温分布',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
@ -387,6 +434,34 @@ 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 = [
values.rvcd && values.rvcd !== 'all'
@ -510,6 +585,7 @@ const transformData = (data: any) => {
dataList[col.dataIndex] = row.dataList[col.dataIndex];
});
return {
...row,
stnm: row.stnm,
dt: row.dt,
dataList
@ -534,7 +610,6 @@ const transformData = (data: any) => {
chartData.value = [];
if (chartInstance) chartInstance.clear();
}
console.log(formattedData);
return {
records: formattedData,
@ -613,10 +688,10 @@ onBeforeUnmount(() => {
//
:deep(.ant-table-tbody .ant-table-row:hover > td) {
background-color: transparent !important;
background-color: #ffffff !important;
}
:deep(.ant-table-tbody .ant-table-row-selected > td) {
background-color: transparent !important;
background-color: #ffffff !important;
}
:deep(.ant-table-tbody .ant-table-row > td) {
cursor: default !important;

View File

@ -0,0 +1,405 @@
<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>

View File

@ -18,12 +18,13 @@
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -43,12 +44,13 @@
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
:showNow="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
v-for="item in timeShortcutList"
:key="item.label"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
@ -68,7 +70,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/store/composables/useTimeScale';
import { getWaterDataSectionList } from '@/api/DataQueryMenuModule';
@ -77,7 +78,6 @@ const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
@ -87,7 +87,7 @@ const props = defineProps<{
const initSearchData = {
rvcd: 'all',
rstcd: null,
rstcd: '008660306300000001',
stcd: null,
mway: null,
timeScale: 'tm'
@ -101,6 +101,7 @@ const {
timePicker,
timeFormat,
timeShowTime,
timeShortcutList,
handleRangeClick,
disabledEndDate,
handleSearchFinish,

View File

@ -20,55 +20,98 @@
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>
<!-- 编辑水质数据 Modal -->
<EditWaterDataModal
v-model:open="editVisible"
:record="editRecord"
@success="handleEditSuccess"
/>
<!-- 删除确认 Modal -->
<DeleteConfirmModal
ref="deleteModalRef"
:delete-fn="handleDeleteFn"
title="删除水质数据"
label="数据"
@success="handleEditSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import WaterDataSearch from './WaterDataSearch.vue';
import EditWaterDataModal from './EditWaterDataModal.vue';
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getWaterDataList,
getWaterDataDayList,
getWaterDataMonthList
getWaterDataMonthList,
deleteWaterDataInfo
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const sort = computed(() => {
//
const sortMap = {
tm: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'ennm', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
],
dt: [
{ field: 'baseStepSort', dir: 'asc' },
{ field: 'rvcd', dir: 'asc' },
{ field: 'rvcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'dt', dir: 'desc' }
],
month: [
{ field: 'stnm', dir: 'desc' },
{ field: 'year', dir: 'desc' },
{ field: 'month', dir: 'desc' }
]
};
// timeScale
return sortMap[currentSearchParams.value.timeScale] || [];
});
const tableRef = ref();
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(() => {
const timeScale = currentSearchParams.value.timeScale;
@ -97,6 +140,7 @@ const allColumns: any[] = [
width: 200,
sort: true,
fixed: 'left',
merge: true,
ellipsis: true
},
{
@ -105,6 +149,7 @@ const allColumns: any[] = [
dataIndex: 'ennm',
visible: true,
width: 120,
merge: true,
ellipsis: true
},
{
@ -119,7 +164,7 @@ const allColumns: any[] = [
title: '地表水等级',
children: [
{
key: 'wwqtgName',
key: 'wwqtg',
title: '水质要求',
dataIndex: 'wwqtgName',
width: 100,
@ -128,7 +173,7 @@ const allColumns: any[] = [
}
},
{
key: 'wqgrdName',
key: 'wqgrd',
title: '水质等级',
dataIndex: 'wqgrdName',
width: 100,
@ -235,6 +280,13 @@ const allColumns: any[] = [
customRender: ({ record }: any) => {
return record.wtmp || '-';
}
},
{
key: 'action',
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 120
}
]
}
@ -270,10 +322,19 @@ const timeColumn = computed(() => ({
//
const Columns = computed(() => {
const result = [...allColumns];
const result = allColumns.map(col => ({ ...col }));
//
const baseIndex = result.findIndex(c => c.key === 'baseName');
result.splice(baseIndex + 1, 0, timeColumn.value);
// timeScale === 'tm'
if (currentSearchParams.value.timeScale !== 'tm') {
const wcsGroup = result.find(c => c.title === '五参数');
if (wcsGroup && wcsGroup.children) {
wcsGroup.children = wcsGroup.children.filter(
(c: any) => c.key !== 'action'
);
}
}
return result;
});
@ -349,6 +410,38 @@ const initTable = (values: any) => {
tableRef.value.getList(params);
};
//
const handleEdit = (record: any) => {
editRecord.value = { ...record };
editVisible.value = true;
};
//
const handleDeleteFn = async (record: any, reason: string) => {
console.log(record, reason);
return deleteWaterDataInfo({
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);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();

View File

@ -1,15 +1,15 @@
<script lang="ts">
export default {
name: "dept",
name: 'dept'
};
</script>
<script setup lang="ts">
import { onMounted, reactive, ref, nextTick } from "vue";
import { ElMessage, ElMessageBox, FormRules } from "element-plus";
import Sortable from "sortablejs";
import { useAppStore } from "@/store/modules/app";
import Page from "@/components/Pagination/page.vue";
import { onMounted, reactive, ref, nextTick } from 'vue';
import { ElMessage, ElMessageBox, FormRules } from 'element-plus';
import Sortable from 'sortablejs';
import { useAppStore } from '@/store/modules/app';
import Page from '@/components/Pagination/page.vue';
import {
getTreelist,
addDict,
@ -21,38 +21,38 @@ import {
updateDictionaryItem,
deleteDictItemById,
deleteDictItemByIds,
changeItemOrder,
} from "@/api/dict";
changeItemOrder
} from '@/api/dict';
const treeloading = ref(false);
const activeName = ref("00");
const activeName = ref('00');
// -
const treedata: any = ref([]);
const treeRef = ref();
const treeId = ref("");
const defaultProps = { label: "dictName" };
const treeId = ref('');
const defaultProps = { label: 'dictName' };
//
const title = ref("");
const title = ref('');
const dialogdict = ref(false);
const dictInfoRef = ref();
const dictInfo = ref({
id: "",
dictCode: "",
dictName: "",
dictType: "",
id: '',
dictCode: '',
dictName: '',
dictType: ''
});
//
const rules = reactive<FormRules>({
dictName: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
dictCode: [{ required: true, message: "请输入字典编码", trigger: "blur" }],
dictName: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
dictCode: [{ required: true, message: '请输入字典编码', trigger: 'blur' }]
});
//
const querystr = ref({
dictId: "",
dictName: "",
size: 10,
current: 1,
dictId: '',
dictName: '',
size: 20,
current: 1
});
//
const tableData = ref([]) as any;
@ -60,21 +60,21 @@ const multipleSelection = ref([]);
// const total = ref(0);
const tableloading = ref(false);
// /
const titleItem = ref("");
const titleItem = ref('');
const dialogdictItem = ref(false);
const dictInfoRefItem = ref();
const dictInfoItem = ref({
id: "",
dictId: "",
itemCode: "",
dictName: "",
parentCode: "",
custom1: "",
id: '',
dictId: '',
itemCode: '',
dictName: '',
parentCode: '',
custom1: ''
});
//
const dictItemrules = reactive<FormRules>({
dictName: [{ required: true, message: "请输入项名称", trigger: "blur" }],
itemCode: [{ required: true, message: "请输入项编码", trigger: "blur" }],
dictName: [{ required: true, message: '请输入项名称', trigger: 'blur' }],
itemCode: [{ required: true, message: '请输入项编码', trigger: 'blur' }]
});
//
onMounted(() => {
@ -86,23 +86,25 @@ const vMove = {
mounted(el: any) {
el.onmousedown = function (e: any) {
var init = e.clientX;
var parent: any = document.getElementById("silderLeft");
var parent: any = document.getElementById('silderLeft');
const initWidth: any = parent.offsetWidth;
document.onmousemove = function (e) {
var end = e.clientX;
var newWidth = end - init + initWidth;
parent.style.width = newWidth + "px";
parent.style.width = newWidth + 'px';
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
},
}
};
function rowDrop() {
const tbody = document.querySelector(".draggable .el-table__body-wrapper tbody");
const tbody = document.querySelector(
'.draggable .el-table__body-wrapper tbody'
);
Sortable.create(tbody, {
draggable: ".draggable .el-table__row",
draggable: '.draggable .el-table__row',
onEnd(data: any) {
const newIndex = data.newIndex;
const oldIndex = data.oldIndex;
@ -111,31 +113,31 @@ function rowDrop() {
}
const params = {
fromID: tableData.value[newIndex].id,
toID: tableData.value[oldIndex].id,
toID: tableData.value[oldIndex].id
};
changeItemOrder(params).then(() => {
init();
});
},
}
});
}
//
function getTree() {
const params = {
dictType: activeName.value,
dictType: activeName.value
};
treeloading.value = true;
getTreelist(params)
.then((res) => {
.then(res => {
treedata.value = res.data;
treeloading.value = false;
if (treeId.value == "") {
if (treeId.value == '') {
treeId.value = res.data[0].id;
}
nextTick(() => {
treeRef.value?.setCurrentKey(treeId.value);
});
if (res.data.length == 0) treeId.value = "";
if (res.data.length == 0) treeId.value = '';
init();
})
.catch(() => {
@ -155,37 +157,37 @@ function activeNameChange(name: any) {
//
function dictAdd() {
title.value = "新增字典";
title.value = '新增字典';
dialogdict.value = true;
const info = ref({
id: "",
dictCode: "",
dictName: "",
dictType: activeName.value,
id: '',
dictCode: '',
dictName: '',
dictType: activeName.value
});
dictInfo.value = info.value;
}
//
function dictEdit(data: any) {
title.value = "修改字典";
title.value = '修改字典';
dictInfo.value = JSON.parse(JSON.stringify(data));
dialogdict.value = true;
}
//
function dictDel(data: any) {
ElMessageBox.confirm("确定删除该字典及该字典下的所有项吗?", "删除提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('确定删除该字典及该字典下的所有项吗?', '删除提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let params = {
id: data.id,
id: data.id
};
deleteById(params).then(() => {
getTree();
ElMessage({
message: "删除成功",
type: "success",
message: '删除成功',
type: 'success'
});
});
});
@ -198,14 +200,14 @@ function dictSave(formEl: any) {
const params = {
dictCode: dictInfo.value.dictCode,
dictName: dictInfo.value.dictName,
dictType: dictInfo.value.dictType,
dictType: dictInfo.value.dictType
};
addDict(params).then(() => {
getTree();
dialogdict.value = false;
ElMessage({
type: "success",
message: "新增成功",
type: 'success',
message: '新增成功'
});
});
} else if (dictInfo.value.id) {
@ -213,14 +215,14 @@ function dictSave(formEl: any) {
dictCode: dictInfo.value.dictCode,
dictName: dictInfo.value.dictName,
dictType: dictInfo.value.dictType,
id: dictInfo.value.id,
id: dictInfo.value.id
};
updateDict(params).then(() => {
getTree();
dialogdict.value = false;
ElMessage({
type: "success",
message: "修改成功",
type: 'success',
message: '修改成功'
});
});
} else {
@ -233,7 +235,7 @@ function dictSave(formEl: any) {
function treenodeDrop(before: any, after: any) {
const params = {
fromID: before.data.id,
toID: after.data.id,
toID: after.data.id
};
changeDictOrder(params).then(() => {
getTree();
@ -241,15 +243,15 @@ function treenodeDrop(before: any, after: any) {
}
const allowDrop: any = (draggingNode: any, dropNode: any, type: any) => {
//
if (type === "inner" || Number(dropNode.data.pid) === 0) return;
if (type === 'inner' || Number(dropNode.data.pid) === 0) return;
if (draggingNode.nextSibling === undefined) {
return type === "prev";
return type === 'prev';
} else if (dropNode.nextSibling === undefined) {
return type === "next";
return type === 'next';
} else if (draggingNode.nextSibling.id !== dropNode.id) {
return type === "prev";
return type === 'prev';
} else {
return type === "next";
return type === 'next';
}
};
//
@ -281,62 +283,62 @@ function init() {
}
//
function addClick() {
titleItem.value = "新增项";
titleItem.value = '新增项';
dialogdictItem.value = true;
const info = ref({
id: "",
id: '',
dictId: querystr.value.dictId,
itemCode: "",
dictName: "",
parentCode: "",
custom1: "",
itemCode: '',
dictName: '',
parentCode: '',
custom1: ''
});
dictInfoItem.value = info.value;
}
//
function editClick(data: any) {
titleItem.value = "修改项";
titleItem.value = '修改项';
dictInfoItem.value = JSON.parse(JSON.stringify(data));
dialogdictItem.value = true;
}
//
function delClick(data: any) {
ElMessageBox.confirm("确定删除此项吗?", "删除提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('确定删除此项吗?', '删除提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let params = {
id: data.id,
id: data.id
};
deleteDictItemById(params).then(() => {
init();
ElMessage({
message: "删除成功",
type: "success",
message: '删除成功',
type: 'success'
});
});
});
}
//
function delsClick() {
ElMessageBox.confirm("确定删除已选择的项吗?", "删除提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('确定删除已选择的项吗?', '删除提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let id = [] as any[];
multipleSelection.value.forEach((item: any) => {
id.push(item.id);
});
let params = {
id: id.join(","),
id: id.join(',')
};
deleteDictItemByIds(params).then(() => {
init();
ElMessage({
message: "删除成功",
type: "success",
message: '删除成功',
type: 'success'
});
});
});
@ -356,14 +358,14 @@ function dictItemSave(formEl: any) {
itemCode: dictInfoItem.value.itemCode,
dictName: dictInfoItem.value.dictName,
parentCode: dictInfoItem.value.parentCode,
custom1: dictInfoItem.value.custom1,
custom1: dictInfoItem.value.custom1
};
addDictionaryItem(params).then(() => {
init();
dialogdictItem.value = false;
ElMessage({
type: "success",
message: "新增字典项成功",
type: 'success',
message: '新增字典项成功'
});
});
} else if (dictInfoItem.value.id) {
@ -373,14 +375,14 @@ function dictItemSave(formEl: any) {
dictName: dictInfoItem.value.dictName,
parentCode: dictInfoItem.value.parentCode,
custom1: dictInfoItem.value.custom1,
id: dictInfoItem.value.id,
id: dictInfoItem.value.id
};
updateDictionaryItem(params).then(() => {
init();
dialogdictItem.value = false;
ElMessage({
type: "success",
message: "修改字典项成功",
type: 'success',
message: '修改字典项成功'
});
});
} else {
@ -402,7 +404,9 @@ const total = ref();
<aside id="silderLeft">
<el-tabs
:class="
useAppStore().size === 'default' ? 'silderLeft-large' : 'silderLeft-default'
useAppStore().size === 'default'
? 'silderLeft-large'
: 'silderLeft-default'
"
v-model="activeName"
class="demo-tabs"
@ -432,7 +436,9 @@ const total = ref();
v-loading="treeloading"
ref="treeRef"
:class="
useAppStore().size === 'default' ? 'silderLeft-large' : 'silderLeft-default'
useAppStore().size === 'default'
? 'silderLeft-large'
: 'silderLeft-default'
"
node-key="id"
:allow-drop="allowDrop"
@ -451,10 +457,18 @@ const total = ref();
<span>{{ node.label }}</span>
</span>
<span>
<el-icon class="treeediticon" title="修改" @click="() => dictEdit(data)">
<el-icon
class="treeediticon"
title="修改"
@click="() => dictEdit(data)"
>
<EditPen />
</el-icon>
<el-icon class="treedelicon" title="删除" @click="() => dictDel(data)">
<el-icon
class="treedelicon"
title="删除"
@click="() => dictDel(data)"
>
<Delete />
</el-icon>
</span>
@ -510,7 +524,10 @@ const total = ref();
border
@selection-change="handleSelectionChange"
default-expand-all
:header-cell-style="{ background: 'rgb(250 250 250)', height: '50px' }"
:header-cell-style="{
background: 'rgb(250 250 250)',
height: '50px'
}"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="orderNo" label="序号" width="80">
@ -521,8 +538,16 @@ const total = ref();
</span>
</template>
</el-table-column>
<el-table-column prop="itemCode" label="项编码" width="160"></el-table-column>
<el-table-column prop="dictName" label="项名称" width="140"></el-table-column>
<el-table-column
prop="itemCode"
label="项编码"
width="160"
></el-table-column>
<el-table-column
prop="dictName"
label="项名称"
width="140"
></el-table-column>
<el-table-column
prop="parentCode"
label="父项编码"
@ -633,7 +658,9 @@ const total = ref();
-webkit-justify-content: flex-end;
"
>
<el-button style="padding: 10px 15px" @click="dictItemClose"> </el-button>
<el-button style="padding: 10px 15px" @click="dictItemClose"
> </el-button
>
<el-button
type="primary"
style="padding: 10px 15px"
@ -689,7 +716,9 @@ const total = ref();
"
>
<el-button @click="dictClose"> </el-button>
<el-button type="primary" @click="dictSave(dictInfoRef)">确定</el-button>
<el-button type="primary" @click="dictSave(dictInfoRef)"
>确定</el-button
>
</span>
</el-dialog>
</div>