Jelajahi Sumber

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	memory/index.md
#	src/views/subSystem/waterSupply/components/WaterSupplyHome.vue
Kazerin 3 minggu lalu
induk
melakukan
03befe2560
67 mengubah file dengan 4527 tambahan dan 11095 penghapusan
  1. 2 2
      CONTEXT.md
  2. 2 3
      memory/index.md
  3. 4 1
      memory/long-term.md
  4. 46 0
      memory/sessions/2026-09-21.md
  5. 25 0
      memory/sessions/2026-09-22.md
  6. 107 0
      memory/sessions/2026-09-23.md
  7. 14 0
      src/api/alarmReview.js
  8. 3 50
      src/api/drainage.js
  9. 12 85
      src/api/pipeNetwork/basic.js
  10. 0 9
      src/api/pipeNetwork/gasMonitor.js
  11. 19 31
      src/api/pipeNetwork/pdeviceInform.js
  12. 1 41
      src/api/pipeNetwork/waterSupply.js
  13. 21 0
      src/api/warningThreshold.js
  14. 8 1
      src/components/GisMapCanvas.vue
  15. 0 34
      src/router/modules/waterSupplyRouter.js
  16. 17 0
      src/utils/coordTransform.js
  17. 0 64
      src/utils/waterSupplyUiModel.js
  18. 42 0
      src/views/subSystem/alarm/AlarmAttachments.vue
  19. 18 0
      src/views/subSystem/alarm/AlarmSummary.vue
  20. 28 0
      src/views/subSystem/alarm/alarmReviewModel.js
  21. 41 0
      src/views/subSystem/alarm/alarmReviewModel.test.mjs
  22. 173 0
      src/views/subSystem/alarm/index.vue
  23. 0 605
      src/views/subSystem/basic/Equipment.vue
  24. 0 308
      src/views/subSystem/basic/GasThreshold.vue
  25. 5 1087
      src/views/subSystem/basic/GisDashboard.vue
  26. 139 9
      src/views/subSystem/drainage/home.vue
  27. 0 546
      src/views/subSystem/drainage/jcbj/bjsh.vue
  28. 160 5
      src/views/subSystem/drainage/jcsj/gwyxqk.vue
  29. 0 721
      src/views/subSystem/drainage/jcsj/sbjcgl.vue
  30. 0 311
      src/views/subSystem/drainage/jcyj/DrainageThreshold.vue
  31. 1 1
      src/views/subSystem/drainage/jcyj/dqyjgl.vue
  32. 1 1
      src/views/subSystem/drainage/jcyj/lsyjgl.vue
  33. 121 505
      src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue
  34. 0 438
      src/views/subSystem/gas/gasOperamon/krqildjc.vue
  35. 43 12
      src/views/subSystem/gas/gasjcyj/gxylyctx.vue
  36. 36 11
      src/views/subSystem/gas/gasjcyj/krqindbj.vue
  37. 161 101
      src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue
  38. 152 83
      src/views/subSystem/hidden/HazardList.vue
  39. 578 0
      src/views/subSystem/hidden/HazardOperationPage.vue
  40. 7 0
      src/views/subSystem/hidden/yhbs.vue
  41. 20 0
      src/views/subSystem/hidden/yhpc.vue
  42. 7 0
      src/views/subSystem/hidden/yhzlfk.vue
  43. 33 0
      src/views/subSystem/lifeCompany/warningThreshold/index.vue
  44. 0 769
      src/views/subSystem/manholeCover/alarm/alarmList.vue
  45. 0 668
      src/views/subSystem/manholeCover/alarm/index.vue
  46. 0 586
      src/views/subSystem/manholeCover/device/index.vue
  47. 8 2
      src/views/subSystem/manholeCover/home/index.vue
  48. 8 2
      src/views/subSystem/manholeCover/operation/faultMana.vue
  49. 7 1
      src/views/subSystem/manholeCover/operation/mOdataStatistics.vue
  50. 2 2
      src/views/subSystem/pipeNetwork/ops/workOrder/list.vue
  51. 0 16
      src/views/subSystem/pipeNetwork/pdeviceInform/Pbasicinfo.vue
  52. 0 850
      src/views/subSystem/pipeNetwork/pdeviceInform/PequipmentLed.vue
  53. 1242 0
      src/views/subSystem/pipeNetwork/pdeviceInform/index.vue
  54. 20 153
      src/views/subSystem/waterSupply/components/WaterAlarmPage.vue
  55. 0 1404
      src/views/subSystem/waterSupply/components/WaterDevicePage.vue
  56. 1135 899
      src/views/subSystem/waterSupply/components/WaterSupplyHome.vue
  57. 0 520
      src/views/subSystem/waterSupply/components/WaterThresholdPage.vue
  58. 0 11
      src/views/subSystem/waterSupply/device/abnormal.vue
  59. 0 1
      src/views/subSystem/waterSupply/device/ledger.vue
  60. 0 1
      src/views/subSystem/waterSupply/device/maintain.vue
  61. 0 1
      src/views/subSystem/waterSupply/device/record.vue
  62. 0 1
      src/views/subSystem/waterSupply/monitorAlarm/audit.vue
  63. 0 1
      src/views/subSystem/waterSupply/monitorAlarm/threshold.vue
  64. 0 1
      src/views/subSystem/waterSupply/wjcbj/bjsh.vue
  65. 0 1
      src/views/subSystem/waterSupply/wjcbj/yzgl.vue
  66. 58 42
      tests/waterAlarmAuditWorkbench.test.mjs
  67. 0 98
      tests/waterSupplyThreshold.test.mjs

+ 2 - 2
CONTEXT.md

@@ -13,7 +13,7 @@ _Avoid_: 维修任务、异常工单
 _Avoid_: 报警处理、报警派单
 
 **报警审核**:
-对报警是否属实做出“转工单”或“误报解除”结论的审核活动;完整工单处置由统一工单页承载。
+四个子系统共用审核页,对未处理报警做出“确认报警”或“误报警”结论。确认报警创建待派单工单,但两种审核都不修改设备状态或报警处理状态;完整工单处置由统一工单页承载,审核页只展示处置记录。
 _Avoid_: 工单审核、工单处置
 
 **报警等级**:
@@ -21,7 +21,7 @@ _Avoid_: 工单审核、工单处置
 _Avoid_: 工单优先级
 
 **报警展示状态**:
-由报警持久化状态、审核结果和关联工单推导出的面向用户的状态,用于区分待审核、已转工单和已解除;它不替代设备报警状态。
+由报警持久化状态、审核结果和关联工单推导出的面向用户的状态,用于区分待审核、已转工单、已解除和误报警;它不替代设备报警状态。
 _Avoid_: 报警记录持久化状态、设备报警状态
 
 **报警记录状态**:

+ 2 - 3
memory/index.md

@@ -63,6 +63,5 @@ npm run dev       # 默认 http://localhost:80
 - [2026-09-09:water_pipe_info 收尾迁移(E 顺序执行完成,旧表链路全删)](sessions/2026-09-09.md)
 - [2026-09-10:WGS84→BD09 诊断与管理端工单生命周期共识更新](sessions/2026-09-10.md)
 - [2026-09-16:监测报警 GIS 接入统一画布(Ticket #4)](sessions/2026-09-16.md)
-- [2026-09-17:供水首页工作台 #58 四项修正收尾(前端呈现 + 后端实时报警根因)](sessions/2026-09-17.md)
-- [2026-09-18:首页 GIS 点位颜色语义统一(仅首页)](sessions/2026-09-18.md)
-- [2026-09-18(续):供水设施 GIS 一张图统一按设施状态着色](sessions/2026-09-18-facility-gis-colors.md)
+- [2026-09-22:报警阈值移除备注字段](sessions/2026-09-22.md)
+- [2026-09-23:感知设备管理、案件 equipment_id、四子系统统一报警审核](sessions/2026-09-23.md)

+ 4 - 1
memory/long-term.md

@@ -56,7 +56,10 @@
 - 供水报警与工单前端统一使用唯一接单责任人;无手机号不阻断派单。
 - 供水、排水、燃气、窨井共用统一工单业务流程,系统展示范围由关联设备类型过滤;前端优先复用既有 REST 接口,不因页面入口创建平行工单生命周期。
 - 工单系统归属以关联设备类型树稳定业务编码判定;`/api/work-order` 是跨系统规范 API,旧入口只做兼容适配;统一工单继续采用数据库状态机和统一日志语义。
-- 报警管理侧重报警态势和派单,报警审核保留原菜单名并承载供水故障维修工单全流程,供水工单总览集中展示全部供水类型工单。
+- 报警管理侧重只读态势与台账;四子系统报警审核共用 `src/views/subSystem/alarm/index.vue`,`sys_menu.path` 均为 `alarmAudit`,菜单 `query.rootTypeId` 按设备类型树隔离,燃气再限定 `warning_code`。确认报警创建待派单工单但不改设备状态;工单后续操作仍在统一工单页。此约定取代早期“审核页承载工单全流程/误报解除”的设计。
+- 报警继续使用 `alarm_level`:1~4 为一级~四级报警,一级最严重;`audit_status` 独立于 `alarm_status`。本次数据库脚本不迁移历史数据,不引入 `source_key`;`alarm_log.log_id` 使用 Kingbase `int8` 和独立序列 `alarm_log_id_seq`。
+- 报警审核附件只支持 JPG/JPEG/PNG 图片(最多 5 张、单张 10 MB),前后端禁止 PDF;处置记录以缩略图展示,点击查看大图,可切换同条记录的多张图片。
+- 报警实际值为空时不展示实际值/阈值行,0 仍正常展示;审核转工单的 `work_order.order_desc` 保存根据报警信息生成的故障描述,审核意见单独保存在报警日志中。
 - 日常巡检和设备保养工单由其他业务系统创建及首次派发;派给当前用户后可以在供水工单总览继续处置。
 - 设备报警状态、报警记录状态和工单处置状态必须分别展示,不得互相替代。
 - 管理端统一工单生命周期采用数据库与前端一致的 `0–7` 契约(0 待派单/1 待接单/2 已接单/3 处理中/4 待验收/5 已结案/6 延期中/7 已取消);终态仅已结案和已取消,延期中是处理旁路。验收驳回是“待验收(4)回退到处理中(3)”的动作而非状态。调用方必须按接口契约解释状态编码并以状态名展示。三仓共识见本仓 docs/domain/work-order-lifecycle.md。

+ 46 - 0
memory/sessions/2026-09-21.md

@@ -0,0 +1,46 @@
+# 2026-09-21:统一设备报警阈值配置
+
+## 目标
+
+- 新增统一报警阈值配置,按设备编码所属设备类型读取 `equipment_warning_type` 指标元数据。
+- 支持 data_type 0(最小/最大)、1(仅最小)、2(仅最大),支持设备类型批量保存。
+- 供水、排水、燃气、窨井旧阈值入口迁移到生命线统一页面,并按根设备类型隔离。
+
+## 实现
+
+- 后端新增 `base/warning` 下设备指标元数据、阈值视图、保存请求、Mapper 和服务;`zksy-admin` 新增 `/warning/threshold/types|devices|save`。
+- 保存按设备编码 + warning_code 幂等更新已有 `warning_threshold`,并校验设备范围、指标归属和上下限关系。
+- 前端新增 `src/views/subSystem/lifeCompany/equipmentWarningType/index.vue` 与 API;旧供水/排水/燃气阈值页面改为统一页面兼容入口。
+- 新增 `migration_equipment_warning_threshold_config.sql`,创建 `equipment_warning_type` 表、新菜单,并迁移旧菜单路由。
+
+## 验证
+
+- 后端 `mvn -o -pl zksy-system,zksy-admin -am -DskipTests compile` 通过。
+- 前端 `npm run build:prod` 通过;仅有项目既有 Sass 弃用与大 chunk 警告。
+- 两仓 `git diff --check` 通过。
+
+## 风险
+
+- 菜单脚本需要在 KingbaseES 执行后生效;生产库需先维护各设备类型的 `equipment_warning_type` 指标数据。
+- 当前统一页面默认根类型为供水,旧入口 wrapper 分别传入 1/2/3;动态菜单通过 query.rootTypeId 传入根类型。
+
+## 菜单脚本修订
+
+- 旧阈值入口改为按固定 `menu_id` 精准更新:2119(供水)、2194(排水)、2017(窨井)、2192(燃气)。
+- 新增固定两级菜单:2241“阈值管理”(`parent_id=2006`)和 2242“报警阈值”,子菜单指向统一页面。
+- 脚本不再使用菜单名称或模糊路径作为更新条件。
+
+## 旧功能清理
+
+- 删除供水、排水、燃气及窨井旧阈值配置 Vue 页面和供水阈值专属测试。
+- 删除旧 `/warningThreshold/*` 管理 Controller、供水专属阈值 Controller/Service/DTO 及测试。
+- 删除窨井旧阈值设置页、`saveWarningThreshold` 专属 API/Controller 入口及其入参 DTO;保留报警列表、设备详情读取能力。
+- 删除前端旧阈值 API、payload 工具函数和测试;保留水务 UI 模型中仍被 GIS/设备/报警使用的通用函数。
+- 精简通用 `WarningThresholdService` 为底层 CRUD,统一管理查询和批量配置由 `EquipmentThresholdService` 承担;实体与 Mapper 保留供运行时读取和新服务复用。
+
+## 清理后验证
+
+- `npm run build:prod` 通过(仅既有 Sass 弃用及大 chunk 警告)。
+- `node --test tests/*.test.mjs`:125 项全部通过。
+- `mvn -o -pl zksy-system,zksy-admin -am -DskipTests compile` 通过。
+- 两仓 `git diff --check` 通过;旧页面、旧管理路径和已删除 DTO 无残留引用。

+ 25 - 0
memory/sessions/2026-09-22.md

@@ -0,0 +1,25 @@
+# 2026-09-22:报警阈值移除备注字段
+
+## 目标
+
+- 删除 `warning_threshold.remark`,同步阈值管理列表、编辑/新增/批量配置表单及相关后端模型。
+
+## 完成内容
+
+- 主后端 `WarningThreshold`、保存请求项、查询项及保存服务不再映射、读取或提交 `warning_threshold.remark`。
+- 迁移脚本增加 `ALTER TABLE app_user.warning_threshold DROP COLUMN IF EXISTS remark;`,并同步清理阈值测试数据脚本中的 `warning_type`、`remark` 列和值。
+- 前端报警阈值页面移除列表备注列、指标表备注列/输入框、编辑备注表单和提交字段;指标编辑表按原型保持“监测指标 / 单位 / 阈值范围 / 生效时段”四列,并优化编辑设备信息布局。
+- 根据最新运行截图继续调整前端:新增配置、批量配置指标表移除独立“单位”列,将单位显示保留在阈值输入值后;列表监测指标列改为可伸缩列,操作列固定在右侧以消除右侧空白。
+- 阈值列表接口新增已配置设备筛选,主列表只请求 `warning_threshold` 中已有记录的设备;服务端和前端均按设备编码升序、同设备按阈值创建时间降序排列,并在设备编码下显示设备名称。
+- 阈值列表改为 `/warning/threshold/findByPage` 后端分页接口,支持设备编码、设备名称、监测指标条件;列表新增独立设备名称列,单条编辑复用批量配置的指标/时间段编辑器;`/warning/threshold/devices?configuredOnly=true` 同时只返回实际已配置的阈值指标。
+- 旧 `zk-api-service` 阈值实体、Mapper、查询服务以及采集服务引用同步适配单设备编码和新字段,避免继续查询已删除列;窨井盖阈值输出 DTO 移除旧字段。
+
+## 验证
+
+- `city-life-line`: `npm run build:prod` 通过(仅有既有 Sass 弃用警告)。
+- `pipe-network-service`: `mvn -o -pl zksy-system,zksy-admin -am -DskipTests compile` 通过。
+- 根后端旧阈值引用模块:`mvn -o -pl zk-api-service,environment-service,flammable-gas-service,telemetry-service,radar-service,audio-service,water-level-service -am -DskipTests compile` 通过。
+
+## 风险/后续
+
+- 数据库迁移脚本需要在目标 KingbaseES 环境执行后,确认现网 `warning_threshold` 已无 `remark` 列及依赖该列的自定义 SQL。

+ 107 - 0
memory/sessions/2026-09-23.md

@@ -0,0 +1,107 @@
+# 2026-09-23:物联感知设备管理页三处调整
+
+## 目标
+
+用户要求调整 `src/views/subSystem/pipeNetwork/pdeviceInform/index.vue`(设备管理页):
+1. 表单页把「规格参数」移到「生产日期」之后,输入框宽度同步调整。
+2. 表单页地图选点时,自动把解析出的地址回填到「安装位置」。
+3. 详情弹窗按表单编辑页的分区方式展示。
+
+## 结论与改动
+
+文件:`src/views/subSystem/pipeNetwork/pdeviceInform/index.vue`
+
+- 基础信息区:新增「规格参数」`el-col :span="12"`,紧跟在生产日期之后(与生产日期同一行,宽度随半栏自适应);从资产信息区移除原来的 `span=24` 整行版本。
+- 资产信息区:保留 资产状态 / 资产价值 / 使用年限 三项(span 12 不变),不再包含规格参数。
+- 地图选点回填地址:
+  - 新增 `reverseGeocode(lng, lat)`(基于 `window.BMapGL.Geocoder` 的逆地理编码,BD-09 坐标)与 `extractAddress(result)`(兼容 `address` / `content.address` / `addressComponents` / `address_detail` 多种返回形态,失败返回空串)。
+  - `handleMapClick(point)` 在写入经纬度(bd09 → wgs84)后调用 `fillLocationByPoint(point.lng, point.lat)`,解析成功即覆盖 `form.equipmentLocation`,失败保留原值。
+  - 新增 `locationTip` 提示文案(解析中 / 已回填 / 解析失败),绑定在坐标输入下方的 `.coordinate-tip`,并在 `reset()` 与 `openMapPicker()` 中清空。
+  - 地图选点弹窗底部新增「解析地址」一行,实时显示回填结果。
+- 详情弹窗:由单个 `el-descriptions` 拆成 基础信息 / 资产信息 / 部署运维 三段,分别用 `.form-section-title.detail-section-title` 分区标题,字段顺序与表单页一致;新增 `.detail-section-title { margin-top: 18px; }` 样式。
+
+## 验证
+
+- `read_lints` 对该文件仅返回既有的 38 条警告(未知 element 标签、未解析变量等旧告警),无新增错误。
+- 未执行构建/运行时验证:逆地理编码依赖 `index.html` 全局加载的百度地图 WebGL SDK 与网络可达性,需前端实际点选地图确认。
+
+## 风险与后续
+
+- 逆地理编码在地图 SDK 未加载或解析失败时静默降级,仅通过提示文案告知用户,不会阻断保存。
+- 回填会覆盖已手工填写的安装位置;若后续希望「仅在为空时回填」,需调整 `fillLocationByPoint`。
+
+## 第二轮:列表区布局调整
+
+- 删除「设备台账」卡片副标题“一览管网监测设备台账与运行状态”。
+- 查询条件(筛选表单)从设备台账卡片中移出,单独成卡 `.filter-card`,放在 KPI「设备统计」框下方;「新增设备」按钮上移到页头右侧。
+- 「设备台账」改为分区标题(复用 `.form-section-title` + 新增 `.section-block-title`),放在设备列表卡片之上,参考「设备统计」页头的摆放方式。
+- 同步删除已无用的 `.card-header-row / .card-title / .card-subtitle / .ledger-actions` 样式。
+
+## 第三轮:设备台账标题样式与位置
+
+- 「设备台账」标题改用与「设备统计」相同的 `.page-title` 字体(20px / 600 / #0a1628),不再使用分区蓝边小标题样式。
+- 将「设备台账」标题从设备列表卡片上方移到查询条件输入框上方(`.filter-card` 之前),并用 `.section-block-title` 保留上下间距。列表卡片不再带标题。
+
+## 案件设备关联调整:case_info 改用 equipment_id
+
+### 目标与结论
+
+- 用户要求 `case_info.device_id` 更名为 `equipment_id`、删除 `device_code`;案件设备关联统一按设备主键查询。
+- 后端两份 `CaseInfo` 实体、Mapper、保存 DTO、案件列表/详情及三处自动建案逻辑已同步。新增案件提交 `equipmentId`,设备 ID 必填且必须存在,不再接收旧 `deviceId` 作为关联字段。
+- 案件接口的 `deviceCode` 仅作为出参展示字段保留,按 `equipmentId` 从 `equipment_base.equipment_code` 读取,设备编码变更不影响案件关联。
+- 已检查 `src/api/pipeNetwork/basic.js` 和 `src/views/subSystem/manholeCover/caseHandling/circulation.vue`:前端只展示 `deviceCode`,没有使用旧 `deviceId` 或新建案件调用,因此本轮不改前端运行代码;保留设备管理页既有修改。
+
+### 脚本与验证
+
+- 后端新增 `pipe-network-service/sql/migration_case_info_equipment_id.sql`,支持有效 ID 保留、旧编码唯一回填、幂等重跑、异常回滚;不强制历史无关联案件补设备,不对依赖对象使用 CASCADE。
+- 管理端与 `zk-api-service` 编译通过;案件 service/controller、工单、清警共 33 项测试通过。
+- `pipe-network-service/sql/tests/case_info_equipment_id_migration.test.mjs` 在内存 PGlite 中通过 11 个场景,覆盖重复执行、编码变更、重复编码、新旧 ID 冲突、孤立关联及依赖视图的失败回滚。
+- 未连接或修改实际数据库。PGlite 仅作 PostgreSQL 兼容性验证,最终仍需在 KingbaseES 验收;上线前备份并停止案件写入,完整执行脚本后部署管理端和 API 服务,避免旧版本继续访问已删除的列。
+
+## 四子系统统一报警审核
+
+### 目标与结果
+
+- 新建 `src/views/subSystem/alarm/index.vue`、`AlarmSummary.vue`、`alarmReviewModel.js` 及 `src/api/alarmReview.js`,按用户原型提供查询、分页、审核弹窗和处置时间线,样式使用系统 Element Plus 卡片/表格/分页。
+- 删除被替代的供水两个审核 wrapper、排水 bjsh、燃气 krqildjc、井盖 alarmList,以及独占的旧 API。供水 WaterAlarmPage 保留管理/GIS,移除旧审核实现并导航至 `/monitorAlarm/alarmAudit`。
+- 菜单 2118/2142/2097/2024 均复用已有行,path 统一 `alarmAudit`、component 为 `subSystem/alarm/index`,query 按设备根类型和燃气报警编码隔离;后端再校验实际授权菜单。
+- 报警等级使用 `alarm_level`,1~4 对应一级~四级(红/橙/蓝/绿);审核结果由后端枚举提供。审核意见两种结果均必填,附件最多 5 个/每个 10MB,未保存退出需确认,失败保留输入。
+- 确认报警创建待派单工单,但不改变设备状态或报警处理状态。误报警仅记录审核结论;状态与工单处置独立。
+- 后端脚本 `pipe-network-service/sql/migration_alarm_review.sql`:Kingbase `int8` 主键 + 独立 `alarm_log_id_seq` + nextval;不做历史迁移、不增加 source_key。新报警初始化及处理/工单过程通过触发器写 alarm_log,审核意见附件由审核事务记录。
+
+### 验证
+
+- 前端 `npm run build:prod` 通过,既有 Sass 弃用和包体积提示未扩大处理范围。
+- 报警 UI 模型、审核新契约、原有报警 GIS、统一叶子 path 路由唯一性共 14 项 Node 测试通过。
+- 后端管理端 clean test 共 55 项通过;SQL 内存 PGlite 7 项通过。未连接真实数据库,未完成 KingbaseES 实机或真实登录 E2E。
+- `zk-api-service` 仍被既有缺失 `CollectionAlarmWriter` 类阻塞编译;已核对 HEAD 引用与文件树,未重写无关采集逻辑。部署说明在后端 `docs/alarm-review-2026-09-23.md`。
+
+### 后续
+
+- 补回采集 API 的缺失类后重新编译;Kingbase 执行脚本并同步部署、重新登录验证 4 个菜单的数据隔离、审核/建单/时间线及实际附件访问。
+- 既有同设备未办结工单冲突规则保留,确认审核遇到冲突会整体回滚并提示,不会半成功。
+
+## 报警审核列表底部文案清理
+
+- 按用户要求删除 `src/views/subSystem/alarm/index.vue` 分页下方说明文案及其专用 `.list-note` 样式,查询、审核和建单逻辑不变。
+- 同步更新 `tests/waterAlarmAuditWorkbench.test.mjs`,4 项测试通过,`git diff --check` 通过;本轮仅为文案清理,未重跑完整构建。
+
+## 报警审核查询区域上下留白
+
+- 按用户反馈调整查询卡片:上下内边距统一为 16px、左右 20px,移除表单及表单项额外外边距;换行间距改由 flex gap 统一控制(行 12px、列 24px)。只影响报警审核页面查询区域。
+- 相关前端回归 5 项通过,Vue 模板及 scoped CSS 编译通过,`git diff --check` 通过;未重跑全量构建或真实页面截图验收。
+
+## 报警处置记录图片缩略图与预览
+
+- 根据运行截图,将处置记录里的图片文件名链接改为 96×72 缩略图;点击使用 Element Plus 图片查看器放大,支持同条记录多图切换,预览层传送到 body 防止被抽屉裁切。
+- 新增 `AlarmAttachments.vue`,图片加载中/失败有占位提示;`alarmReviewModel.js` 统一筛选安全图片 URL,支持绝对地址、站内路径、大小写后缀以及查询参数/片段,不为非图片文件渲染链接。
+- 用户追加明确不要 PDF:审核表单文案、accept、前端校验和后端上传白名单/文件头校验均只允许 JPG/JPEG/PNG;尺寸及数量限制不变。本条取代前面初版支持 PDF 的描述,不删除历史存储文件。
+- 前端图片模型及审核页 11 项测试通过,Vue 脚本/模板/scoped CSS 编译通过;后端上传/审核/controller 共 13 项测试通过(含真实 PDF 头和伪装 PNG 的拒绝用例)。两个仓库 `git diff --check` 通过。未进行真实登录、实际图片网络加载的浏览器验收。
+- 本轮 `npm run build:prod` 生产构建通过。
+
+## 报警空值展示与工单故障描述
+
+- 新增 `hasActualValue` 判断,列表及复用的审核/处置记录摘要在实际值为 null/undefined/空字符串时隐藏整行“实际值;阈值”;数字 0 和字符串 0 保持展示。
+- 修正后端审核建单传参:不再将审核意见传作 orderDesc,改由工单服务根据原报警生成故障描述,写入 `work_order.order_desc`。审核意见仍单独写入审核日志,审核事务、设备状态逻辑不变。
+- 自动生成的工单描述同样忽略没有实际值时的数值/阈值占位。不改历史工单,不需要结构脚本。
+- 前端相关 13 项测试和两个 SFC 的脚本/模板/样式编译通过;后端审核/controller/工单共 27 项测试通过,新增检查实际写入的 WorkOrder.orderDesc,覆盖实际值为 0 和为空。两个仓库 `git diff --check` 通过;未连接真实数据库或执行浏览器 E2E。

+ 14 - 0
src/api/alarmReview.js

@@ -0,0 +1,14 @@
+import request from '@/utils/request'
+
+export const getAlarmReviewOptions = () => request({ url: '/api/alarm/reviews/options', method: 'get' })
+export const getAlarmReviewPage = params => request({ url: '/api/alarm/reviews', method: 'get', params })
+export const getAlarmReviewStatistics = params => request({ url: '/api/alarm/reviews/statistics', method: 'get', params })
+export const getAlarmReviewDetail = (id, params) => request({ url: `/api/alarm/reviews/${encodeURIComponent(id)}`, method: 'get', params })
+export const getAlarmLogs = (id, params) => request({ url: `/api/alarm/${encodeURIComponent(id)}/logs`, method: 'get', params })
+export const submitAlarmReview = (id, params, data) => request({ url: `/api/alarm/${encodeURIComponent(id)}/reviews`, method: 'post', params, data })
+export const uploadAlarmReviewAttachment = (id, params, file) => {
+  const data = new FormData()
+  data.append('file', file)
+  // 多附件会并发上传,FormData 不能参与框架 JSON 字符串防重(均被转成 {})。
+  return request({ url: `/api/alarm/${encodeURIComponent(id)}/attachments`, method: 'post', params, data, headers: { repeatSubmit: false }, timeout: 60000 })
+}

+ 3 - 50
src/api/drainage.js

@@ -73,10 +73,11 @@ async function getDrainageEquipmentTypeId() {
     drainageEquipmentTypeIdPromise = getEquipmentTypeList()
       .then(res => {
         const list = (res && res.code !== undefined ? res.data : res) || []
+        // 设备类别主键:2=排水设备(EquipmentTypeEnum.ROOT_DRAINAGE_DEVICE)
         const drainageParent = Array.isArray(list)
-          ? list.find(item => item.typeId === 'drainage' && String(item.parentTypeId) === '0')
+          ? list.find(item => Number(item.id) === 2 && String(item.parentId) === '0')
           : null
-        return drainageParent && drainageParent.id ? String(drainageParent.id) : 'drainage'
+        return drainageParent && drainageParent.id ? String(drainageParent.id) : '2'
       })
       .catch(error => {
         drainageEquipmentTypeIdPromise = null
@@ -100,32 +101,12 @@ export function getEquipmentById(id) {
 export function getEquipmentList() {
   return request({ url: '/api/drainage/equipment/base/list', method: 'get' })
 }
-export function addEquipment(data) {
-  return request({ url: '/api/drainage/equipment/base', method: 'post', data })
-}
-export function updateEquipment(data) {
-  return request({ url: '/api/drainage/equipment/base', method: 'put', data })
-}
-export function deleteEquipment(id) {
-  return request({ url: '/api/drainage/equipment/base/' + id, method: 'delete' })
-}
-export function batchDeleteEquipment(ids) {
-  return request({ url: '/api/drainage/equipment/base/batch', method: 'delete', data: ids })
-}
 export function getEquipmentTypeList(parentTypeId) {
   const params = {}
   if (parentTypeId) params.parentTypeId = parentTypeId
   return request({ url: '/api/drainage/equipmentType/list', method: 'get', params })
 }
 
-// ============ 设备状态 ============
-export function addEquipmentStatus(data) {
-  return request({ url: '/api/drainage/equipment/status', method: 'post', data })
-}
-export function updateEquipmentStatus(data) {
-  return request({ url: '/api/drainage/equipment/status', method: 'put', data })
-}
-
 // ============ 预警管理 ============
 
 // 分页查询当前预警(今天的预警)
@@ -277,11 +258,6 @@ export function handleAlarm(alarmId, handleUser, handleRemark) {
   })
 }
 
-// 报警审核通过后自动创建排水运维工单
-export function approveAlarmToWorkOrder(data) {
-  return request({ url: '/api/alarm/approve-to-work-order', method: 'post', data })
-}
-
 // 根据ID获取报警详情
 export function getAlarmById(id) {
   return request({
@@ -327,26 +303,3 @@ export function bindPumpStation(data) {
 export function addWorkOrder(data) {
   return request({ url: '/manhole/workOrder/addWorkOrder', method: 'post', data })
 }
-
-// ============ 预警阈值配置 ============
-export function getThresholdPage(pageNum, pageSize, params) {
-  return request({ url: '/warningThreshold/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
-}
-export function getThresholdModulePage(pageNum, pageSize, typeName, params) {
-  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, typeName, ...params } })
-}
-export function getThresholdList(params) {
-  return request({ url: '/warningThreshold/getWarningThresholdList', method: 'get', params })
-}
-export function getThresholdById(id) {
-  return request({ url: '/warningThreshold/getById/' + id, method: 'get' })
-}
-export function saveThreshold(data, moduleType) {
-  return request({ url: '/warningThreshold/save', method: 'post', data, params: moduleType ? { moduleType } : undefined })
-}
-export function updateThreshold(data, moduleType) {
-  return request({ url: '/warningThreshold/update', method: 'post', data, params: moduleType ? { moduleType } : undefined })
-}
-export function deleteThreshold(ids) {
-  return request({ url: '/warningThreshold/deleteBatch', method: 'post', data: ids })
-}

+ 12 - 85
src/api/pipeNetwork/basic.js

@@ -47,14 +47,6 @@ export function deleteManhole(id) {
 }
 
 // ============ 窨井井下设备关联 ============
-// 查询设备已关联的井下设备列表
-export function getManholeDeviceRels(equipmentId) {
-  return request({ url: '/manhole/device/rel/list/' + equipmentId, method: 'get' })
-}
-// 搜索可关联设备(排除窨井类型5/9/10,排除已关联设备ID)
-export function searchManholeRelDevices(params) {
-  return request({ url: '/manhole/device/rel/search', method: 'get', params })
-}
 // 查询窨井关联的设备列表(兼容 Manhole.vue,manholeId 为 ManholeBase 的 ID)
 export function getManholeDevices(manholeId) {
   return request({ url: '/manhole/device/rel/list/' + manholeId, method: 'get' })
@@ -73,10 +65,6 @@ export function deleteManholeDeviceRel(relId) {
 export function getManholeDevicePage(data) {
   return request({ url: '/manhole/device/findByPage', method: 'post', data })
 }
-// 监测设备台账详情
-export function getManholeDeviceById(id) {
-  return request({ url: '/manhole/device/getById/' + id, method: 'get' })
-}
 // 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
 export function getManholeDeviceStatistics(data) {
   return request({ url: '/manhole/device/statistics', method: 'post', data })
@@ -154,10 +142,6 @@ export function getManholeAlarmMonitorList(data = {}) {
 export function getManholeAlarmDeviceDetail(id) {
   return request({ url: '/manhole/device/getById/' + id, method: 'get' })
 }
-export function saveManholeAlarmThreshold(data) {
-  return request({ url: '/manhole/device/saveWarningThreshold', method: 'post', data })
-}
-
 export function getManholeTerminalFaultPage(data) {
   return request({ url: '/manhole/terminalFault/findByPage', method: 'post', data })
 }
@@ -227,19 +211,20 @@ export function getGasAlarmHistory(pageNum, pageSize, startDate, endDate, device
 export function getGasAlarmStats() {
   return request({ url: '/api/gas-alarm/stats', method: 'get' })
 }
-export function resolveGasAlarm(id) {
-  return request({ url: '/api/gas-alarm/resolve/' + id, method: 'put' })
+export function resolveGasAlarm(id, handleRemark) {
+  return request({ url: '/api/gas-alarm/resolve/' + id, method: 'put', params: handleRemark ? { handleRemark } : undefined })
 }
-export function dispatchGasAlarm(id) {
-  return request({ url: '/api/gas-alarm/dispatch/' + id, method: 'post' })
+export function dispatchGasAlarm(id, data) {
+  return request({ url: '/api/gas-alarm/dispatch/' + id, method: 'post', data })
 }
 
 // 管线压力报警
 export function getPressureAlarmEquipment() { return request({ url: '/api/pressure-alarm/equipment', method: 'get' }) }
 export function getPressureAlarmHistory(pageNum, pageSize, startDate, endDate, deviceCode) { return request({ url: '/api/pressure-alarm/history', method: 'get', params: { pageNum, pageSize, startDate, endDate, deviceCode } }) }
 export function getPressureAlarmStats() { return request({ url: '/api/pressure-alarm/stats', method: 'get' }) }
-export function resolvePressureAlarm(id) { return request({ url: '/api/pressure-alarm/resolve/' + id, method: 'put' }) }
-export function dispatchPressureAlarm(id) { return request({ url: '/api/pressure-alarm/dispatch/' + id, method: 'post' }) }
+export function resolvePressureAlarm(id, handleRemark) { return request({ url: '/api/pressure-alarm/resolve/' + id, method: 'put', params: handleRemark ? { handleRemark } : undefined }) }
+export function dispatchPressureAlarm(id, data) { return request({ url: '/api/pressure-alarm/dispatch/' + id, method: 'post', data }) }
+
 // 培训课程
 export function getCourseList(params) { return request({ url: '/api/drill/course/list', method: 'get', params }) }
 export function saveCourse(data) { return request({ url: '/api/drill/course', method: 'post', data }) }
@@ -312,50 +297,23 @@ export function deleteRepairRecord(id) {
 
 // ============ 监测设备管理 ============
 export function getEquipmentList() {
-  return request({ url: '/EquipmentBase/getList', method: 'get' })
+  return request({ url: '/equipment-base/all', method: 'get' })
 }
 export function getEquipmentPage(pageNum, pageSize, params) {
-  return request({ url: '/EquipmentBase/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
-}
-export function getEquipmentFullPage(pageNum, pageSize, params) {
-  return request({ url: '/api/equipment/findFullByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+  return request({ url: '/equipment-base', method: 'get', params: { pageNum, pageSize, ...params } })
 }
 export function getEquipmentById(id) {
-  return request({ url: '/EquipmentBase/getById/' + id, method: 'get' })
-}
-export function addEquipment(data) {
-  const params = {}
-  if (data.networkId) params.networkId = data.networkId
-  if (data.pointId) params.pointId = data.pointId
-  return request({ url: '/api/equipment/save', method: 'post', data, params })
-}
-export function updateEquipment(data) {
-  const params = {}
-  if (data.networkId) params.networkId = data.networkId
-  if (data.pointId) params.pointId = data.pointId
-  return request({ url: '/api/equipment/updateById', method: 'put', data, params })
-}
-export function deleteEquipment(id) {
-  return request({ url: '/api/equipment/deleteById', method: 'delete', params: { id } })
+  return request({ url: '/equipment-base/' + id, method: 'get' })
 }
 export function getEquipmentTypeList() {
-  return request({ url: '/EquipmentType/getList', method: 'get' })
+  return request({ url: '/equipmentType', method: 'get' })
 }
 export function getEquipmentByTopLevelType(typeName) {
-  return request({ url: '/EquipmentBase/findByTopLevelType', method: 'get', params: { typeName } })
+  return request({ url: '/equipment-base/top-level-type', method: 'get', params: { typeName } })
 }
 export function getGasDashboardSummary() {
   return request({ url: '/api/gas-dashboard/summary', method: 'get' })
 }
-export function getEquipmentStatus(equipmentId) {
-  return request({ url: '/api/equipment/status/' + equipmentId, method: 'get' })
-}
-export function getEquipmentAbnormalList() {
-  return request({ url: '/api/equipment/abnormal', method: 'get' })
-}
-export function getPointRelByEquipment(equipmentId) {
-  return request({ url: '/api/equipment/point-rel/' + equipmentId, method: 'get' })
-}
 export function getEquipmentByPoint(pointId) {
   return request({ url: '/api/equipment/point-rel/by-point/' + pointId, method: 'get' })
 }
@@ -366,9 +324,6 @@ export function getPipePointDevices(pointId) {
 export function addPipePointDeviceRel(data) {
   return request({ url: '/api/gas-pipe-point/device/save', method: 'post', data })
 }
-export function saveEquipmentDeviceRel(data) {
-  return request({ url: '/api/equipment/device-rel/save', method: 'post', data })
-}
 export function addPipePointDeviceRelBatch(dataList) {
   return request({ url: '/api/gas-pipe-point/device/save-batch', method: 'post', dataList })
 }
@@ -432,31 +387,3 @@ export function getGisManholes() {
 export function getGisEquipments(params) {
   return request({ url: '/api/gis/equipments', method: 'get', params })
 }
-
-// ============ 预警阈值配置 ============
-export function getWarningThresholdPage(pageNum, pageSize, params) {
-  return request({ url: '/warningThreshold/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
-}
-export function getWarningThresholdModulePage(pageNum, pageSize, typeNameOrParams, moduleParams) {
-  // 新版阈值页面按固定业务模块查询,不再把“燃气”作为查询条件传给后端。
-  // 保留旧的四参数调用方式,兼容其他页面。
-  const params = moduleParams === undefined
-    ? (typeNameOrParams || {})
-    : { typeName: typeNameOrParams, ...(moduleParams || {}) }
-  return request({ url: '/warningThreshold/findByModulePage', method: 'get', params: { pageNum, pageSize, ...params } })
-}
-export function getWarningThresholdList(params) {
-  return request({ url: '/warningThreshold/getWarningThresholdList', method: 'get', params })
-}
-export function getWarningThresholdById(id) {
-  return request({ url: '/warningThreshold/getById/' + id, method: 'get' })
-}
-export function saveWarningThreshold(data, moduleType) {
-  return request({ url: '/warningThreshold/save', method: 'post', data, params: moduleType ? { moduleType } : undefined })
-}
-export function updateWarningThreshold(data, moduleType) {
-  return request({ url: '/warningThreshold/update', method: 'post', data, params: moduleType ? { moduleType } : undefined })
-}
-export function deleteWarningThreshold(ids) {
-  return request({ url: '/warningThreshold/deleteBatch', method: 'post', data: ids })
-}

+ 0 - 9
src/api/pipeNetwork/gasMonitor.js

@@ -1,14 +1,5 @@
 import request from '@/utils/request'
 
-// ============ 场站可燃气体浓度监测 (krqildjc.vue) ============
-export function getGasMonitorPoints() {
-  return request({ url: '/api/gas-monitor/points', method: 'get' })
-}
-
-export function getGasMonitorTrend(pointId, hours = 24) {
-  return request({ url: `/api/gas-monitor/point/${pointId}/trend`, method: 'get', params: { hours } })
-}
-
 // ============ 管线压力监测 (gxyljc.vue) ============
 export function getPressureMonitorPoints() {
   return request({ url: '/api/pressure-monitor/points', method: 'get' })

+ 19 - 31
src/api/pipeNetwork/pdeviceInform.js

@@ -130,10 +130,10 @@ export function unbindPointEquipment(pointId, equipmentIds) {
   });
 }
 
-// 物联感知设备
+// 物联感知设备(RESTful)
 export function listEquipment(query) {
   return request({
-    url: "/EquipmentBase/findByPage",
+    url: "/equipment-base",
     method: "get",
     params: query
   });
@@ -141,14 +141,14 @@ export function listEquipment(query) {
 
 export function getEquipment(id) {
   return request({
-    url: `/EquipmentBase/getById/${id}`,
+    url: `/equipment-base/${id}`,
     method: "get"
   });
 }
 
 export function addEquipment(data) {
   return request({
-    url: "/EquipmentBase/save",
+    url: "/equipment-base",
     method: "post",
     data
   });
@@ -156,7 +156,7 @@ export function addEquipment(data) {
 
 export function updateEquipment(data) {
   return request({
-    url: "/EquipmentBase/updateById",
+    url: `/equipment-base/${data.equipmentId}`,
     method: "put",
     data
   });
@@ -164,58 +164,46 @@ export function updateEquipment(data) {
 
 export function delEquipment(id) {
   return request({
-    url: "/EquipmentBase/deleteById",
-    method: "delete",
-    params: { id }
+    url: `/equipment-base/${id}`,
+    method: "delete"
   });
 }
 
 export function getEquipmentDetail(equipmentId) {
   return request({
-    url: `/EquipmentBase/detail/${equipmentId}`,
+    url: `/equipment-base/${equipmentId}/detail`,
     method: "get"
   });
 }
 
 export function listEquipmentByPoint(pointId) {
   return request({
-    url: `/EquipmentBase/byPoint/${pointId}`,
+    url: `/equipment-base/point/${pointId}`,
     method: "get"
   });
 }
 
 export function updateEquipmentStatus(equipmentId, currentStatus) {
   return request({
-    url: "/EquipmentBase/updateStatus",
+    url: `/equipment-base/${equipmentId}/status`,
     method: "put",
-    params: { equipmentId, currentStatus }
+    params: { currentStatus }
   });
 }
 
-export function testEquipment(equipmentId, payload = {}) {
+export function getEquipmentStatusStatistics(rootTypeId) {
   return request({
-    url: "/EquipmentBase/test",
-    method: "post",
-    params: {
-      equipmentId,
-      testType: payload.testType,
-      testDetail: payload.testDetail,
-      testUser: payload.testUser
-    }
+    url: "/equipment-base/statistics",
+    method: "get",
+    params: { rootTypeId }
   });
 }
 
 // 基础辅助数据
-export function listEquipmentTypeAll() {
+export function listEquipmentTypeAll(rootTypeId) {
   return request({
-    url: "/EquipmentType/getList",
-    method: "get"
-  });
-}
-
-export function listEquipmentStatusAll() {
-  return request({
-    url: "/EquipmentStatus/getList",
-    method: "get"
+    url: "/equipmentType",
+    method: "get",
+    params: { rootTypeId }
   });
 }

+ 1 - 41
src/api/pipeNetwork/waterSupply.js

@@ -18,53 +18,13 @@ export const getAlarmStats = () => get('/waterSupply/alarm/stats')
 export const listRealtimeAlarms = () => get('/waterSupply/alarm/realtime')
 export const listAlarmHistory = (params) => get('/waterSupply/alarm/history', params)
 export const getAlarmDetail = (id) => get(`/waterSupply/alarm/detail/${id}`)
-// 已停用(#46 收口裁定):后端这两个写入口统一返回停用提示,供水报警的写入口只有 auditAlarm,
-// 设备异常管理与报警管理均为只读;保留函数定义以免历史调用点报错,去留由后续工单决定。
-export const resolveAlarm = (id, handleRemark) => request({ url: `/waterSupply/alarm/resolve/${id}`, method: 'put', params: { handleRemark } })
-export const dispatchAlarm = (data) => request({ url: '/waterSupply/alarm/dispatch', method: 'post', data })
-export const auditAlarm = (data) => request({ url: '/waterSupply/alarm/audit', method: 'post', data })
-export const listAlarmAudits = (alarmId) => get('/waterSupply/alarm/audit/list', { alarmId })
-export const getAuditOrderStats = () => get('/waterSupply/alarm/audit/order-stats')
-export const getAlarmFlow = (id) => get(`/waterSupply/alarm/flow/${id}`)
 export const listAlarmMapPoints = () => get('/waterSupply/alarm/map/points')
 // T02 报警管理只读台账与态势统计
 export const listAlarmLedger = (params) => get('/waterSupply/alarm/ledger/page', params)
 export const getAlarmLedgerDashboard = (days = 7) => get('/waterSupply/alarm/ledger/dashboard', { days })
 export const exportAlarmLedger = (params) => request({ url: '/waterSupply/alarm/ledger/export', method: 'post', params, responseType: 'blob' })
 
-export const listThresholds = (params) => get('/waterSupply/alarm/threshold/page', params)
-export const listThresholdDevices = () => get('/waterSupply/alarm/threshold/devices')
-export const getThreshold = (id) => get(`/waterSupply/alarm/threshold/detail/${id}`)
-export const saveThresholds = (data) => request({ url: '/waterSupply/alarm/threshold/save', method: 'post', data })
-export const updateThreshold = (data) => request({ url: '/waterSupply/alarm/threshold/update', method: 'put', data })
-export const deleteThresholds = (ids) => request({ url: '/waterSupply/alarm/threshold/delete', method: 'delete', params: { ids } })
-
-export const createWaterDevice = (data) => request({ url: '/waterSupply/device/ledger/create', method: 'post', data })
-export const getWaterDeviceById = (id) => get(`/EquipmentBase/getById/${id}`)
-export const updateWaterDevice = (data) => request({ url: '/EquipmentBase/updateById', method: 'put', data })
-export const deleteWaterDevices = (ids) => request({ url: '/EquipmentBase/deleteBatchById', method: 'delete', params: { ids: Array.isArray(ids) ? ids.join(',') : ids } })
-export const getDeviceLedgerStatistics = () => get('/waterSupply/device/ledger/statistics')
-export const listDeviceLedger = (params) => get('/waterSupply/device/ledger/page', params)
-export const getDeviceTypeStatistics = () => get('/waterSupply/device/ledger/typeStatistics')
-export const listDeviceGisPoints = (params) => get('/waterSupply/device/ledger/gisPoints', params)
-export const exportDeviceLedger = (params) => request({ url: '/waterSupply/device/ledger/export', method: 'post', params, responseType: 'blob' })
-export const importDeviceLedger = (file, updateSupport = false) => {
-  const formData = new FormData()
-  formData.append('file', file)
-  formData.append('updateSupport', String(updateSupport))
-  return request({ url: '/waterSupply/device/ledger/import', method: 'post', data: formData })
-}
-export const listAbnormalDevices = (params) => get('/waterSupply/device/abnormal/page', params)
-export const getAbnormalDeviceStats = () => get('/waterSupply/device/abnormal/stats')
-export const listAbnormalGisPoints = (params) => get('/waterSupply/device/abnormal/gisPoints', params)
-export const exportAbnormalDevices = (params) => request({ url: '/waterSupply/device/abnormal/export', method: 'post', params, responseType: 'blob' })
-export const createMaintainOrder = (data) => request({ url: '/waterSupply/device/maintain/order', method: 'post', data })
-export const createAbnormalOrder = (data) => request({ url: '/waterSupply/device/abnormal/order', method: 'post', data })
-export const listMaintainRecords = (params) => get('/waterSupply/device/maintain/record/page', params)
-export const saveMaintainRecord = (data) => request({ url: '/waterSupply/device/maintain/record', method: 'post', data })
-export const updateMaintainRecord = (data) => request({ url: '/waterSupply/device/maintain/record', method: 'put', data })
-export const deleteMaintainRecord = (id) => request({ url: `/waterSupply/device/maintain/record/${id}`, method: 'delete' })
-
+// 供水设备 GIS 一张图(WaterDeviceGisMap / WaterSupplyHome 使用)
 export const getPipeNetworkDashboard = (params) => get('/waterSupply/pipeNetwork/statistics/dashboard', params)
 export const exportPipeNetworkStatistics = (params) => request({ url: '/waterSupply/pipeNetwork/statistics/export', method: 'get', params, responseType: 'blob' })
 export const getWaterFacilityFeatures = (params) => get('/waterSupply/gis/map/features', params)

+ 21 - 0
src/api/warningThreshold.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function listWarningTypes(rootTypeId) {
+  return request({ url: '/warning/threshold/types', method: 'get', params: { rootTypeId } })
+}
+
+export function listWarningThresholdDevices(params) {
+  return request({ url: '/warning/threshold/devices', method: 'get', params })
+}
+
+export function pageWarningThresholds(params) {
+  return request({ url: '/warning/threshold/findByPage', method: 'get', params })
+}
+
+export function saveWarningThresholds(data) {
+  return request({ url: '/warning/threshold', method: 'put', data })
+}
+
+export function deleteWarningThreshold(params) {
+  return request({ url: '/warning/threshold', method: 'delete', params })
+}

+ 8 - 1
src/components/GisMapCanvas.vue

@@ -82,7 +82,14 @@ async function initMap() {
     mapInstance.centerAndZoom(new BMapGL.Point(lng, lat), props.zoom)
     mapInstance.enableScrollWheelZoom(true)
     mapInstance.setMapStyleV2({ styleId: props.mapStyleId })
-    mapInstance.addEventListener('click', () => emit('map-click'))
+    mapInstance.addEventListener('click', (event) => {
+      const point = event?.latLng || event?.latlng || event?.point || event?.target?.point
+      const lng = typeof point?.lng === 'function' ? point.lng() : point?.lng
+      const lat = typeof point?.lat === 'function' ? point.lat() : point?.lat
+      if (point && Number.isFinite(Number(lng)) && Number.isFinite(Number(lat))) {
+        emit('map-click', { lng: Number(lng), lat: Number(lat) })
+      }
+    })
     mapInstance.addEventListener('moveend', renderScene)
     mapInstance.addEventListener('zoomend', renderScene)
     renderScene()

+ 0 - 34
src/router/modules/waterSupplyRouter.js

@@ -1,34 +0,0 @@
-import Layout from '@/layout'
-
-// 供水管网安全运行监测系统 - 前端静态菜单
-// 说明:如果后端动态菜单已配置,可移除本文件或保留为兜底入口。
-export default [
-  {
-    path: '/waterSupply',
-    component: Layout,
-    redirect: '/waterSupply/facility/gis',
-    name: 'WaterSupply',
-    alwaysShow: true,
-    meta: { title: '供水管网安全运行监测系统', icon: 'example' },
-    children: [
-      {
-        path: 'facility/gis',
-        component: () => import('@/views/subSystem/waterSupply/facility/gis.vue'),
-        name: 'WaterSupplyFacilityGis',
-        meta: { title: '供水设施GIS一张图', icon: 'map' }
-      },
-      {
-        path: 'facility/statistics',
-        component: () => import('@/views/subSystem/waterSupply/facility/statistics.vue'),
-        name: 'WaterSupplyFacilityStatistics',
-        meta: { title: '管网数据统计分析', icon: 'chart' }
-      },
-      {
-        path: 'monitorAlarm/workOrder',
-        component: () => import('@/views/subSystem/waterSupply/monitorAlarm/workOrder.vue'),
-        name: 'WaterSupplyMonitorAlarmWorkOrder',
-        meta: { title: '报警工单处置', icon: 'work-order' }
-      }
-    ]
-  }
-]

+ 17 - 0
src/utils/coordTransform.js

@@ -50,6 +50,23 @@ export function wgs84ToBd09(lng, lat) {
   return gcj02ToBd09(gcjLng, gcjLat)
 }
 
+/** 百度 BD-09 → WGS84,用于地图选点回填业务坐标 */
+export function bd09ToWgs84(lng, lat) {
+  const x = Number(lng) - 0.0065
+  const y = Number(lat) - 0.006
+  const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI)
+  const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI)
+  const gcjLng = z * Math.cos(theta)
+  const gcjLat = z * Math.sin(theta)
+  const [wgsLng, wgsLat] = wgs84ToGcj02Inverse(gcjLng, gcjLat)
+  return [wgsLng, wgsLat]
+}
+
+function wgs84ToGcj02Inverse(lng, lat) {
+  const [forwardLng, forwardLat] = wgs84ToGcj02(lng, lat)
+  return [lng * 2 - forwardLng, lat * 2 - forwardLat]
+}
+
 /** 转换管线数据中的坐标数组 */
 export function convertPipeline(pipeline) {
   return {

+ 0 - 64
src/utils/waterSupplyUiModel.js

@@ -194,67 +194,3 @@ export function getDeviceOrderEndpoint(view) {
     ? '/waterSupply/device/abnormal/order'
     : null
 }
-
-function normalizeThresholdValue(value) {
-  if (value === '' || value === null || value === undefined) {
-    return undefined
-  }
-  const parsed = Number(value)
-  return Number.isFinite(parsed) ? parsed : value
-}
-
-function normalizeThresholdPeriod(form = {}) {
-  const periodStart = form.periodStart?.trim() || undefined
-  const periodEnd = form.periodEnd?.trim() || undefined
-  return { periodStart, periodEnd }
-}
-
-function removeUndefined(payload) {
-  return Object.fromEntries(
-    Object.entries(payload).filter(([, value]) => value !== undefined)
-  )
-}
-
-export function buildThresholdSavePayload(form = {}) {
-  const deviceCodes = Array.from(new Set((form.deviceCodes || [])
-    .map(code => code?.trim())
-    .filter(Boolean)))
-  const { periodStart, periodEnd } = normalizeThresholdPeriod(form)
-
-  return removeUndefined({
-    deviceCodes,
-    warningType: form.warningType?.trim() || undefined,
-    warningCode: form.warningCode?.trim() || undefined,
-    minValue: normalizeThresholdValue(form.minValue),
-    maxValue: normalizeThresholdValue(form.maxValue),
-    periodStart,
-    periodEnd,
-    remark: form.remark?.trim() || undefined
-  })
-}
-
-export function buildThresholdUpdatePayload(form = {}) {
-  const { periodStart, periodEnd } = normalizeThresholdPeriod(form)
-
-  return removeUndefined({
-    id: form.id,
-    deviceCode: form.deviceCode?.trim() || undefined,
-    warningType: form.warningType?.trim() || undefined,
-    warningCode: form.warningCode?.trim() || undefined,
-    minValue: normalizeThresholdValue(form.minValue),
-    maxValue: normalizeThresholdValue(form.maxValue),
-    periodStart,
-    periodEnd,
-    remark: form.remark?.trim() || undefined
-  })
-}
-
-export function buildThresholdQuery(query = {}) {
-  return removeUndefined({
-    pageNum: query.pageNum || 1,
-    pageSize: query.pageSize || 10,
-    deviceCode: query.deviceCode?.trim() || undefined,
-    warningType: query.warningType?.trim() || undefined,
-    keyword: query.keyword?.trim() || undefined
-  })
-}

+ 42 - 0
src/views/subSystem/alarm/AlarmAttachments.vue

@@ -0,0 +1,42 @@
+<template>
+  <div v-if="images.length" class="alarm-attachments">
+    <div class="attachment-images">
+      <el-image
+        v-for="(file, index) in images"
+        :key="file.url"
+        class="attachment-thumbnail"
+        :src="file.url"
+        :alt="file.name"
+        :title="`点击查看大图:${file.name}`"
+        :preview-src-list="previewUrls"
+        :initial-index="index"
+        preview-teleported
+        fit="cover"
+        lazy
+      >
+        <template #placeholder><span class="image-placeholder">加载中…</span></template>
+        <template #error><span class="image-placeholder">图片加载失败</span></template>
+      </el-image>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { imageAttachments } from './alarmReviewModel'
+
+const props = defineProps({
+  value: { type: String, default: '' },
+  baseUrl: { type: String, default: '' }
+})
+const images = computed(() => imageAttachments(props.value, props.baseUrl))
+const previewUrls = computed(() => images.value.map(file => file.url))
+</script>
+
+<style scoped>
+.alarm-attachments { margin-top: 12px; }
+.attachment-images { display: flex; flex-wrap: wrap; gap: 10px; }
+.attachment-thumbnail { width: 96px; height: 72px; flex: 0 0 96px; border: 1px solid #e4e7ed; border-radius: 4px; background: #fff; cursor: zoom-in; }
+.attachment-thumbnail:hover { border-color: var(--el-color-primary); }
+.image-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 12px; }
+</style>

+ 18 - 0
src/views/subSystem/alarm/AlarmSummary.vue

@@ -0,0 +1,18 @@
+<template>
+  <section class="alarm-summary">
+    <div class="summary-title"><div><strong>{{ alarm.deviceName || '未命名设备' }}</strong><p>{{ alarm.deviceCode || '—' }}</p></div><el-tag :type="levelTone">{{ alarm.alarmLevelName || '未知等级' }}</el-tag></div>
+    <div class="summary-body">
+      <div>{{ alarmDescription(alarm) }}</div>
+      <p v-if="hasActualValue(alarm)">实际值:<span class="value">{{ valueText(alarm.actualValue, alarm.warningUnit) }}</span>;阈值:{{ thresholdText(alarm) }}</p>
+      <p>安装位置:{{ alarm.equipmentLocation || '—' }}</p>
+      <p>报警时间:{{ alarm.alarmTime || '—' }}</p>
+    </div>
+  </section>
+</template>
+<script setup>
+import { alarmDescription, thresholdText, valueText, hasActualValue } from './alarmReviewModel'
+defineProps({ alarm: { type: Object, required: true }, levelTone: { type: String, default: 'info' } })
+</script>
+<style scoped>
+.alarm-summary{padding:18px 20px;background:#f7f9fc;border:1px solid #e8edf3;border-radius:6px;margin-bottom:24px}.summary-title{display:flex;justify-content:space-between;gap:16px}.summary-title strong{font-size:16px;color:#303133}.summary-title p,.summary-body p{margin:6px 0 0;color:#909399;font-size:13px;overflow-wrap:anywhere}.summary-body{border-top:1px solid #e4e7ed;margin-top:14px;padding-top:14px;line-height:1.7}.summary-body .value{color:#e65c40;font-weight:600}
+</style>

+ 28 - 0
src/views/subSystem/alarm/alarmReviewModel.js

@@ -0,0 +1,28 @@
+// 报警值及阈值来自事件快照,不使用设备当前阈值重算历史事件。
+const present = value => value !== null && value !== undefined && value !== ''
+export const hasActualValue = row => present(row?.actualValue)
+export const valueText = (value, unit = '') => present(value) ? `${value}${unit ? ` ${unit}` : ''}` : '—'
+export function thresholdText(row = {}) {
+  const unit = row.warningUnit || ''
+  if (present(row.minValue) && present(row.maxValue)) return `${row.minValue} ~ ${valueText(row.maxValue, unit)}`
+  if (present(row.minValue)) return `下限 ${valueText(row.minValue, unit)}`
+  if (present(row.maxValue)) return `上限 ${valueText(row.maxValue, unit)}`
+  return '—'
+}
+export function alarmDescription(row = {}) {
+  const metric = row.warningType || row.warningCode || '监测指标'
+  if (present(row.actualValue) && present(row.minValue) && Number(row.actualValue) <= Number(row.minValue)) return `${metric}达到或低于下限`
+  if (present(row.actualValue) && present(row.maxValue) && Number(row.actualValue) >= Number(row.maxValue)) return `${metric}达到或超过上限`
+  return `${metric}报警`
+}
+export function imageAttachments(value, baseUrl = '') {
+  return String(value || '').split(',').map(path => path.trim()).filter(Boolean).flatMap(path => {
+    // 历史附件也只允许 HTTP(S) 或站内资源,拒绝脚本 URL 和协议相对路径。
+    if (!/^https?:\/\//i.test(path) && !/^\/(?!\/)/.test(path)) return []
+    const pathname = path.split(/[?#]/)[0]
+    if (!/\.(jpe?g|png|gif|webp|bmp|avif)$/i.test(pathname)) return []
+    const url = path.startsWith('/') ? `${baseUrl}${path}` : path
+    const name = pathname.split('/').pop() || '图片'
+    return [{ url, name }]
+  })
+}

+ 41 - 0
src/views/subSystem/alarm/alarmReviewModel.test.mjs

@@ -0,0 +1,41 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { valueText, thresholdText, alarmDescription, imageAttachments, hasActualValue } from './alarmReviewModel.js'
+
+test('zero is a value, not an empty threshold', () => {
+  assert.equal(valueText(0,'MPa'),'0 MPa')
+  assert.equal(thresholdText({minValue:0,maxValue:10,warningUnit:'MPa'}),'0 ~ 10 MPa')
+  assert.equal(thresholdText({maxValue:0}),'上限 0')
+  assert.equal(valueText(null),'—')
+})
+test('snapshot visibility hides empty actual values but preserves numeric zero', () => {
+  for (const actualValue of [null, undefined, '']) assert.equal(hasActualValue({ actualValue, maxValue: 0 }), false)
+  assert.equal(hasActualValue({}), false)
+  assert.equal(hasActualValue({ actualValue: 0 }), true)
+  assert.equal(hasActualValue({ actualValue: '0' }), true)
+  assert.equal(hasActualValue({ actualValue: 336 }), true)
+})
+test('single-sided limits and equality follow collection semantics', () => {
+  assert.equal(thresholdText({minValue:1,warningUnit:'m'}),'下限 1 m')
+  assert.equal(alarmDescription({warningType:'压力',actualValue:1,minValue:1}),'压力达到或低于下限')
+  assert.equal(alarmDescription({warningType:'压力',actualValue:10,maxValue:10}),'压力达到或超过上限')
+  assert.equal(alarmDescription({warningCode:'WARN',actualValue:null}),'WARN报警')
+})
+test('attachments reject executable or protocol-relative links', () => {
+  assert.deepEqual(imageAttachments('javascript:alert(1),//evil.test/a,data:text/html;base64,eA=='),[])
+  assert.deepEqual(imageAttachments('/profile/a.png','/dev-api'),[{url:'/dev-api/profile/a.png',name:'a.png'}])
+  assert.equal(imageAttachments('https://files.example/a.png')[0].url,'https://files.example/a.png')
+})
+
+test('image preview groups preserve image order and exclude PDF documents', () => {
+  const images = imageAttachments('/profile/first.jpeg,/profile/report.pdf,/profile/second.PNG?version=1', '/dev-api')
+  assert.deepEqual(images.map(file => file.url), ['/dev-api/profile/first.jpeg', '/dev-api/profile/second.PNG?version=1'])
+})
+test('image grouping handles absolute URLs, fragments and invalid attachments', () => {
+  const images = imageAttachments('https://files.example/a.JPG?version=2#view,/profile/b.png#image,javascript:bad,//evil.test/a.jpg', '/dev-api')
+  assert.equal(images.length, 2)
+  assert.equal(images[0].url, 'https://files.example/a.JPG?version=2#view')
+  assert.equal(images[1].name, 'b.png')
+  assert.deepEqual(imageAttachments(null), [])
+  assert.deepEqual(imageAttachments('/profile/report.pdf'), [])
+})

+ 173 - 0
src/views/subSystem/alarm/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="order-container alarm-review-page">
+    <el-card class="search-card" shadow="never">
+      <el-form :inline="true" :model="filters" class="search-form" @submit.prevent="search">
+        <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码 / 名称" @keyup.enter="search" /></el-form-item>
+        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in options.alarmLevels" :key="item.key" :label="item.label" :value="item.value" /></el-select></el-form-item>
+        <el-form-item label="审核结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.label" :value="item.value" /></el-select></el-form-item>
+        <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
+        <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card class="table-card" shadow="never">
+      <el-tabs :model-value="activeTab" @tab-change="changeTab">
+        <el-tab-pane name="all"><template #label>全部报警 <span class="tab-count">{{ statistics.total }}</span></template></el-tab-pane>
+        <el-tab-pane name="pending"><template #label>待审核 <span class="tab-count">{{ statistics.pending }}</span></template></el-tab-pane>
+      </el-tabs>
+      <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" class="load-error"><template #default><el-button link type="primary" @click="load">重新加载</el-button></template></el-alert>
+      <el-table v-loading="loading" :data="rows" row-key="id" border stripe class="table" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :default-sort="{ prop: 'alarmTime', order: 'descending' }" @sort-change="sortChanged">
+        <el-table-column prop="deviceCode" label="设备编码" width="200" show-overflow-tooltip />
+        <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="equipmentLocation" label="安装位置" min-width="210" show-overflow-tooltip><template #default="{ row }">{{ row.equipmentLocation || '—' }}</template></el-table-column>
+        <el-table-column label="报警描述" min-width="275"><template #default="{ row }"><div class="description">{{ alarmDescription(row) }}</div><div v-if="hasActualValue(row)" class="snapshot">实际值:<span class="value">{{ valueText(row.actualValue, row.warningUnit) }}</span>;阈值:{{ thresholdText(row) }}</div></template></el-table-column>
+        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)" effect="light">{{ row.alarmLevelName }}</el-tag></template></el-table-column>
+        <el-table-column prop="alarmTime" label="报警时间" width="175" sortable="custom" />
+        <el-table-column label="审核结果" width="110" align="center"><template #default="{ row }"><el-tag :type="auditTone(row.auditStatus)" effect="light">{{ row.auditStatusName }}</el-tag></template></el-table-column>
+        <el-table-column label="操作" width="160" align="center" fixed="right"><template #default="{ row }"><el-button v-if="isUnreviewed(row)" v-hasPermi="['alarm:review:audit']" link type="primary" @click="openReview(row)">审核</el-button><el-button link type="primary" @click="openHistory(row)">处置记录</el-button></template></el-table-column>
+        <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配的未处理报警'" /></template>
+      </el-table>
+      <el-pagination class="pagination" v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" />
+    </el-card>
+
+    <el-dialog v-model="reviewVisible" title="报警审核" width="680px" append-to-body destroy-on-close :close-on-click-modal="false" :before-close="closeReview">
+      <div v-loading="detailLoading">
+        <AlarmSummary v-if="current" :alarm="current" :level-tone="levelTone(current.alarmLevel)" />
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="submitting || detailLoading">
+          <el-form-item label="审核结果" prop="auditStatus"><el-radio-group v-model="form.auditStatus" class="review-choices"><el-radio v-for="item in reviewChoices" :key="item.key" :label="item.value" border>{{ item.label }}</el-radio></el-radio-group></el-form-item>
+          <el-form-item label="审核意见" prop="opinion"><el-input v-model="form.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请填写核实情况、判定依据或误报原因" /></el-form-item>
+          <el-form-item label="图片"><div class="attachments"><el-upload v-model:file-list="files" :http-request="uploadFile" :before-upload="beforeUpload" :limit="5" :on-exceed="exceedFiles" :disabled="submitting || detailLoading" accept=".jpg,.jpeg,.png"><el-button :icon="Upload">选择图片</el-button><template #tip><div class="el-upload__tip">JPG、JPEG、PNG,最多 5 张,单张不超过 10 MB。</div></template></el-upload></div></el-form-item>
+        </el-form>
+        <el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon />
+      </div>
+      <template #footer><el-button :disabled="submitting || uploading" @click="closeReview()">取消</el-button><el-button type="primary" :loading="submitting" :disabled="detailLoading || uploading || !current" @click="submit">提交审核</el-button></template>
+    </el-dialog>
+
+    <el-drawer v-model="historyVisible" title="处置记录" size="640px" append-to-body destroy-on-close>
+      <div v-loading="historyLoading"><AlarmSummary v-if="historyAlarm" :alarm="historyAlarm" :level-tone="levelTone(historyAlarm.alarmLevel)" /><el-alert v-if="historyError" :title="historyError" type="error" :closable="false" /><div class="timeline-heading">处置全流程 <span>{{ logs.length }} 个节点</span></div><el-timeline><el-timeline-item v-for="item in logs" :key="item.logId" :timestamp="item.operTime" placement="top" type="primary" hollow><div class="timeline-title">{{ item.operTypeName }} <small>{{ item.operUserName || '系统' }}</small></div><div class="timeline-content"><div class="operation-text">{{ item.operDesc || '—' }}</div><AlarmAttachments v-if="item.attachUrl" :value="item.attachUrl" :base-url="baseUrl" /></div></el-timeline-item></el-timeline><el-empty v-if="!logs.length && !historyLoading && !historyError" description="暂无操作记录" /></div>
+      <template #footer><el-button @click="historyVisible = false">关闭</el-button></template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
+import { useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Refresh, Search, Upload } from '@element-plus/icons-vue'
+import { getAlarmReviewOptions, getAlarmReviewPage, getAlarmReviewStatistics, getAlarmReviewDetail, getAlarmLogs, submitAlarmReview, uploadAlarmReviewAttachment } from '@/api/alarmReview'
+import AlarmSummary from './AlarmSummary.vue'
+import AlarmAttachments from './AlarmAttachments.vue'
+import { alarmDescription, thresholdText, valueText, hasActualValue } from './alarmReviewModel'
+
+const route = useRoute()
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+const options = reactive({ auditStatuses: [], alarmLevels: [] })
+const filters = reactive({ keyword: '', alarmLevel: undefined, auditStatus: undefined })
+const dateRange = ref([]), rows = ref([]), logs = ref([]), total = ref(0), pageNum = ref(1), pageSize = ref(10)
+const statistics = reactive({ total: 0, pending: 0 })
+const loading = ref(false), error = ref(''), ascending = ref(false)
+const reviewVisible = ref(false), detailLoading = ref(false), current = ref(null), formRef = ref(), files = ref([])
+const form = reactive({ auditStatus: undefined, opinion: '' }), formError = ref(''), submitting = ref(false)
+const historyVisible = ref(false), historyLoading = ref(false), historyAlarm = ref(null), historyError = ref('')
+let listRequest = 0, detailRequest = 0, historyRequest = 0
+const scope = computed(() => ({ rootTypeId: Number(route.query.rootTypeId), warningCode: route.query.warning_code || route.query.warningCode || undefined }))
+const scopeKey = computed(() => JSON.stringify(scope.value))
+const unreviewed = computed(() => options.auditStatuses.find(item => item.key === 'UNREVIEWED')?.value)
+const reviewChoices = computed(() => options.auditStatuses.filter(item => item.key !== 'UNREVIEWED'))
+const activeTab = computed(() => filters.auditStatus !== undefined && filters.auditStatus === unreviewed.value ? 'pending' : 'all')
+const uploading = computed(() => files.value.some(file => ['ready', 'uploading'].includes(file.status)))
+const dirty = computed(() => form.auditStatus !== undefined || form.opinion.trim() || files.value.length)
+const rules = {
+  auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
+  opinion: [{ validator: (_, value, callback) => callback(String(value || '').trim() ? undefined : new Error('请填写审核意见')), trigger: 'blur' }]
+}
+const message = e => e?.response?.data?.msg || e?.message || '请求失败,请重试'
+const isUnreviewed = row => unreviewed.value !== undefined && row.auditStatus === unreviewed.value
+const levelTone = value => ({ LEVEL_ONE: 'danger', LEVEL_TWO: 'warning', LEVEL_THREE: 'primary', LEVEL_FOUR: 'success' })[options.alarmLevels.find(item => item.value === value)?.key] || 'info'
+const auditTone = value => ({ UNREVIEWED: 'warning', CONFIRMED: 'primary', FALSE_ALARM: 'info' })[options.auditStatuses.find(item => item.value === value)?.key] || 'info'
+
+function params() {
+  return { ...scope.value, keyword: filters.keyword.trim() || undefined, alarmLevel: filters.alarmLevel === '' ? undefined : filters.alarmLevel,
+    auditStatus: filters.auditStatus === '' ? undefined : filters.auditStatus, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value,
+    startTime: dateRange.value?.[0] ? `${dateRange.value[0]} 00:00:00` : undefined, endTime: dateRange.value?.[1] ? `${dateRange.value[1]} 23:59:59.999999` : undefined }
+}
+async function load() {
+  const request = ++listRequest
+  loading.value = true; error.value = ''
+  try {
+    if (!Number.isInteger(scope.value.rootTypeId) || scope.value.rootTypeId <= 0) throw new Error('菜单缺少有效的 rootTypeId,请检查菜单 query 配置')
+    if (!options.auditStatuses.length) Object.assign(options, (await getAlarmReviewOptions()).data)
+    if (request !== listRequest) return
+    const [page, count] = await Promise.all([getAlarmReviewPage(params()), getAlarmReviewStatistics(params())])
+    if (request !== listRequest) return
+    rows.value = page.data.records || []; total.value = Number(page.data.total || 0)
+    Object.assign(statistics, { total: Number(count.data.total || 0), pending: Number(count.data.pending || 0) })
+  } catch (e) {
+    if (request === listRequest) { error.value = message(e); rows.value = []; total.value = 0; Object.assign(statistics, { total: 0, pending: 0 }) }
+  } finally { if (request === listRequest) loading.value = false }
+}
+function search() { pageNum.value = 1; return load() }
+function reset() { Object.assign(filters, { keyword: '', alarmLevel: undefined, auditStatus: undefined }); dateRange.value = []; return search() }
+function changeTab(tab) { filters.auditStatus = tab === 'pending' ? unreviewed.value : undefined; search() }
+function sortChanged({ order }) { ascending.value = order === 'ascending'; search() }
+async function openReview(row) {
+  const request = ++detailRequest
+  reviewVisible.value = true; detailLoading.value = true; current.value = null; files.value = []; formError.value = ''
+  Object.assign(form, { auditStatus: undefined, opinion: '' })
+  try {
+    const response = await getAlarmReviewDetail(row.id, { ...scope.value })
+    if (request !== detailRequest) return
+    if (!isUnreviewed(response.data)) { reviewVisible.value = false; ElMessage.warning('该报警已审核,请刷新'); return load() }
+    current.value = response.data
+  } catch (e) { if (request === detailRequest) formError.value = message(e) }
+  finally { if (request === detailRequest) detailLoading.value = false }
+}
+function beforeUpload(file) {
+  if (!/\.(jpe?g|png)$/i.test(file.name) || file.size > 10 * 1024 * 1024 || !file.size) { ElMessage.error('请选择不超过10MB的JPG、JPEG或PNG图片'); return false }
+  return true
+}
+function exceedFiles() { ElMessage.warning('最多上传5张图片') }
+function uploadFile({ file }) { return uploadAlarmReviewAttachment(current.value.id, { ...scope.value }, file).then(response => response.data) }
+async function submit() {
+  if (submitting.value || uploading.value || !current.value) return
+  if (!await formRef.value.validate().catch(() => false)) return
+  if (files.value.some(file => !file.response?.token)) { formError.value = '存在未上传成功的附件,请移除后重新上传'; return }
+  submitting.value = true; formError.value = ''
+  try {
+    await submitAlarmReview(current.value.id, { ...scope.value }, { auditStatus: form.auditStatus, opinion: form.opinion.trim(), attachmentTokens: files.value.map(file => file.response.token) })
+    reviewVisible.value = false; files.value = []; Object.assign(form, { auditStatus: undefined, opinion: '' })
+    ElMessage.success('审核已提交'); await load()
+  } catch (e) { formError.value = message(e) }
+  finally { submitting.value = false }
+}
+async function canLeave() {
+  if (submitting.value || uploading.value) { ElMessage.warning('请等待上传或提交完成'); return false }
+  if (reviewVisible.value && dirty.value) {
+    try { await ElMessageBox.confirm('尚未提交的审核意见和附件选择将丢失,是否放弃?', '放弃本次审核', { confirmButtonText: '放弃修改', cancelButtonText: '继续编辑', type: 'warning' }) } catch { return false }
+  }
+  return true
+}
+async function closeReview(done) { if (await canLeave()) { ++detailRequest; reviewVisible.value = false; if (typeof done === 'function') done() } }
+async function openHistory(row) {
+  const request = ++historyRequest
+  historyVisible.value = true; historyLoading.value = true; historyAlarm.value = null; logs.value = []; historyError.value = ''
+  try {
+    const [detail, history] = await Promise.all([getAlarmReviewDetail(row.id, { ...scope.value }), getAlarmLogs(row.id, { ...scope.value })])
+    if (request === historyRequest) { historyAlarm.value = detail.data; logs.value = history.data || [] }
+  } catch (e) { if (request === historyRequest) historyError.value = message(e) }
+  finally { if (request === historyRequest) historyLoading.value = false }
+}
+watch(scopeKey, async () => {
+  ++detailRequest; ++historyRequest; reviewVisible.value = false; historyVisible.value = false
+  await reset()
+  if (route.query.alarmId && !error.value) openReview({ id: route.query.alarmId })
+}, { immediate: true })
+onBeforeRouteLeave(canLeave)
+onBeforeRouteUpdate(canLeave)
+onBeforeUnmount(() => { ++listRequest; ++detailRequest; ++historyRequest })
+</script>
+
+<style scoped lang="scss">
+.order-container{padding:20px;background:#f5f7fa;min-height:calc(100vh - 84px)}.search-card{margin-bottom:20px;border-radius:8px}.search-form{display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;margin:0}.search-form :deep(.el-form-item){margin:0}.search-form :deep(.el-input){width:220px}.search-form :deep(.el-select){width:160px}.search-form :deep(.el-date-editor){width:300px}.search-card :deep(.el-card__body){padding:16px 20px}.table-card{border-radius:8px}.table{width:100%}.pagination{margin-top:20px;justify-content:flex-end}.tab-count{margin-left:8px;background:#f0f2f5;padding:1px 7px;border-radius:4px;font-size:12px}.description{font-weight:500;margin-bottom:5px}.snapshot{font-size:12px;color:#909399;line-height:1.6}.value{color:#e65c40}.load-error{margin-bottom:16px}.review-choices{display:flex;flex-wrap:wrap;gap:12px}.review-choices .el-radio{margin-right:0;min-width:160px}.attachments{width:100%;padding:14px 16px;background:#fafbfc;border:1px dashed #dcdfe6;border-radius:6px}.timeline-heading{display:flex;justify-content:space-between;margin:24px 0;color:#303133;font-weight:600}.timeline-heading span{color:#909399;font-size:13px;font-weight:400}.timeline-title{font-weight:600}.timeline-title small{margin-left:12px;font-weight:400;color:#909399}.timeline-content{padding:12px 14px;background:#f7f9fc;border-radius:4px;margin-top:10px}.operation-text{white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.7}:deep(.el-tabs__header){margin-bottom:18px}:deep(.el-table .cell){line-height:1.7}@media(max-width:900px){.order-container{padding:12px}.pagination{overflow:auto;justify-content:flex-start}}
+</style>

+ 0 - 605
src/views/subSystem/basic/Equipment.vue

@@ -1,605 +0,0 @@
-<template>
-  <div class="equipment-container">
-    <el-card class="search-card">
-      <el-form :inline="true" :model="searchForm" class="search-form">
-        <el-form-item label="设备编码">
-          <el-input v-model="searchForm.equipmentCode" placeholder="请输入设备编码" clearable style="width:180px" />
-        </el-form-item>
-        <el-form-item label="设备名称">
-          <el-input v-model="searchForm.equipmentName" placeholder="请输入设备名称" clearable style="width:180px" />
-        </el-form-item>
-        <el-form-item label="设备型号">
-          <el-input v-model="searchForm.equipmentModel" placeholder="请输入设备型号" clearable style="width:160px" />
-        </el-form-item>
-        <el-form-item label="制造商">
-          <el-input v-model="searchForm.manufacturer" placeholder="请输入制造商" clearable style="width:160px" />
-        </el-form-item>
-        <el-form-item label="所属片区">
-          <el-select v-model="searchForm.district" placeholder="请选择所属片区" clearable style="width:120px">
-            <el-option v-for="opt in districtOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button @click="handleReset">重置</el-button>
-          <el-button type="success" @click="handleAdd">新增</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-card class="table-card">
-      <el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
-        <el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
-        <el-table-column prop="district" label="所属片区" min-width="100">
-          <template #default="{ row }">
-            <span>{{ row.district || '-' }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="设备图片" width="100">
-          <template #default="{ row }">
-            <div v-if="row.equipmentImage" class="table-image-cell">
-              <el-image
-                :src="row.equipmentImage"
-                fit="cover"
-                :preview-src-list="[row.equipmentImage]"
-                preview-teleported
-                hide-on-click-pdf
-                class="equipment-table-image"
-              />
-            </div>
-            <el-tag v-else type="info" size="small">无图</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="equipmentName" label="设备名称" min-width="160" />
-        <el-table-column prop="equipmentModel" label="设备型号" width="110" />
-        <el-table-column prop="manufacturer" label="制造商" width="110" />
-        <el-table-column prop="networkName" label="所属管线" min-width="150">
-          <template #default="{ row }">
-            <span v-if="row.networkName">{{ row.networkName }}</span>
-            <el-tag v-else type="info" size="small">未关联</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="pointName" label="所属管点" min-width="150">
-          <template #default="{ row }">
-            <span v-if="row.pointName">{{ row.pointName }}</span>
-            <el-tag v-else type="info" size="small">未关联</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="运行状态" width="90">
-          <template #default="{ row }">
-            <el-tag v-if="row.currentStatus === 1" type="success" size="small">在用</el-tag>
-            <el-tag v-else-if="row.currentStatus === 2" type="warning" size="small">闲置</el-tag>
-            <el-tag v-else-if="row.currentStatus === 3" type="danger" size="small">维修</el-tag>
-            <el-tag v-else-if="row.currentStatus === 4" type="info" size="small">报废</el-tag>
-            <el-tag v-else-if="row.currentStatus === 5" type="warning" size="small">待入库</el-tag>
-            <el-tag v-else type="info" size="small">未知</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="报警" width="70">
-          <template #default="{ row }">
-            <el-tag :type="row.alarmStatus === 1 ? 'danger' : 'success'" size="small">
-              {{ row.alarmStatus === 1 ? '报警' : '正常' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="在线" width="70">
-          <template #default="{ row }">
-            <el-tag :type="row.onlineStatus === 1 ? 'success' : 'info'" size="small">
-              {{ row.onlineStatus === 1 ? '在线' : '离线' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="220" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <el-button type="info" size="small" @click="handleDetail(row)">详情</el-button>
-            <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
-        @change="loadData" style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
-
-    <el-drawer title="设备详情" v-model="detailVisible" size="1100px" class="detail-drawer" direction="rtl">
-      <el-descriptions v-if="detailData" :column="2" border>
-        <el-descriptions-item label="设备编码" :span="1">{{ detailData.equipmentCode }}</el-descriptions-item>
-        <el-descriptions-item label="设备名称" :span="1">{{ detailData.equipmentName }}</el-descriptions-item>
-        <el-descriptions-item label="设备型号">{{ detailData.equipmentModel }}</el-descriptions-item>
-        <el-descriptions-item label="设备类别">{{ detailData.typeName || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="规格参数">{{ detailData.equipmentSpec }}</el-descriptions-item>
-        <el-descriptions-item label="制造商">{{ detailData.manufacturer }}</el-descriptions-item>
-        <el-descriptions-item label="生产日期">{{ detailData.productionDate }}</el-descriptions-item>
-        <el-descriptions-item label="所属管线">{{ detailData.networkName || '未关联' }}</el-descriptions-item>
-        <el-descriptions-item label="管线等级">{{ detailData.networkLevel || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="所属管点">{{ detailData.pointName || '未关联' }}</el-descriptions-item>
-        <el-descriptions-item label="管点类型">{{ detailData.pointType || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="安装位置">{{ detailData.equipmentLocation }}</el-descriptions-item>
-        <el-descriptions-item label="安装时间">{{ detailData.installTime }}</el-descriptions-item>
-        <el-descriptions-item label="运行状态">
-          <el-tag v-if="detailData.currentStatus === 1" type="success" size="small">在用</el-tag>
-          <el-tag v-else-if="detailData.currentStatus === 2" type="warning" size="small">闲置</el-tag>
-          <el-tag v-else-if="detailData.currentStatus === 3" type="danger" size="small">维修</el-tag>
-          <el-tag v-else-if="detailData.currentStatus === 4" type="info" size="small">报废</el-tag>
-          <el-tag v-else-if="detailData.currentStatus === 5" type="warning" size="small">待入库</el-tag>
-          <span v-else>-</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="报警状态">
-          <el-tag :type="detailData.alarmStatus === 1 ? 'danger' : 'success'" size="small">
-            {{ detailData.alarmStatus === 1 ? '报警' : '正常' }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="在线状态">
-          <el-tag :type="detailData.onlineStatus === 1 ? 'success' : 'info'" size="small">
-            {{ detailData.onlineStatus === 1 ? '在线' : '离线' }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="维护人员">{{ detailData.maintainer || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="联系方式">{{ detailData.maintainerPhone || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="所属片区">{{ detailData.district || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="经度">{{ detailData.longitude || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="纬度">{{ detailData.latitude || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="设备图片" :span="2">
-          <el-image
-            v-if="detailData.equipmentImage"
-            :src="detailData.equipmentImage"
-            fit="cover"
-            :preview-src-list="[detailData.equipmentImage]"
-            preview-teleported
-            class="detail-equipment-image"
-          />
-          <span v-else>-</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
-      </el-descriptions>
-    </el-drawer>
-
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="1100px" destroy-on-close>
-      <el-form :model="formData" :rules="rules" ref="formRef" label-width="110px" size="default">
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="所属片区" prop="district">
-              <el-select v-model="formData.district" placeholder="请选择所属片区" style="width:100%" @change="onDistrictChange">
-                <el-option v-for="opt in districtOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备编码" prop="equipmentCode">
-              <el-input v-model="formData.equipmentCode" placeholder="保存时自动生成" readonly />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="设备名称" prop="equipmentName">
-              <el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备型号">
-              <el-input v-model="formData.equipmentModel" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="规格参数">
-              <el-input v-model="formData.equipmentSpec" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="制造商">
-              <el-input v-model="formData.manufacturer" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="设备类别" prop="equipmentTypeId">
-              <el-select v-model="formData.equipmentTypeId" placeholder="请选择设备类别" style="width:100%">
-                <el-option v-for="t in equipmentTypeOptions" :key="t.id" :label="t.typeName" :value="t.id" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="安装位置">
-              <el-input v-model="formData.equipmentLocation" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="维护人员">
-              <el-input v-model="formData.maintainer" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="所属管线">
-              <el-select v-model="formData.networkId" placeholder="请选择管线" clearable style="width:100%" @change="onNetworkChange">
-                <el-option v-for="n in networkOptions" :key="n.networkId" :label="`${n.networkName}(${n.networkCode})`" :value="n.networkId" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="关联管点">
-              <el-select v-model="formData.pointId" placeholder="请先选择管线" clearable style="width:100%">
-                <el-option v-for="p in pointOptions" :key="p.pointId" :label="`${p.pointName}(${p.pointCode})`" :value="p.pointId" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="经度">
-              <el-input-number v-model="formData.longitude" :precision="10" style="width:100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
-          <el-col :span="12">
-            <el-form-item label="纬度">
-              <el-input-number v-model="formData.latitude" :precision="10" style="width:100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="联系方式">
-              <el-input v-model="formData.maintainerPhone" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" />
-        </el-form-item>
-        <el-form-item label="设备图片">
-          <el-upload
-            class="image-uploader"
-            action="#"
-            :auto-upload="false"
-            :show-file-list="false"
-            :http-request="handleUploadRequest"
-            :on-change="handleImageChange"
-            :on-remove="handleImageRemove"
-            accept="image/jpeg,image/png,image/jpg"
-          >
-            <div v-if="formData.equipmentImage || equipmentImagePreview">
-              <img :src="equipmentImagePreview || formData.equipmentImage" alt="设备图片" class="preview-image" />
-              <div class="image-uploader-cover">
-                <el-icon><Plus /></el-icon>
-              </div>
-            </div>
-            <div v-else class="image-uploader-placeholder">
-              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
-              <div>点击上传图片</div>
-            </div>
-          </el-upload>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSave">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, UploadFilled } from '@element-plus/icons-vue'
-import request from '@/utils/request'
-import { getEquipmentFullPage, addEquipment, updateEquipment, deleteEquipment, getEquipmentTypeList, getPipeNetworkOptions, getGasPipePointPage } from '@/api/pipeNetwork/basic'
-
-const baseApi = import.meta.env.VITE_APP_BASE_API
-
-const loading = ref(false)
-const tableData = ref([])
-const pageNum = ref(1)
-const pageSize = ref(10)
-const total = ref(0)
-const searchForm = reactive({ equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '', district: '' })
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增')
-const formRef = ref(null)
-
-const rules = {
-  equipmentName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
-  equipmentTypeId: [{ required: true, message: '请选择设备类别', trigger: 'change' }],
-  district: [{ required: true, message: '请选择所属片区', trigger: 'change' }]
-}
-
-const detailVisible = ref(false)
-const detailData = ref(null)
-const equipmentTypeOptions = ref([])
-const networkOptions = ref([])
-const pointOptions = ref([])
-const equipmentImagePreview = ref('')
-let equipmentImageFile = null
-
-const districtOptions = [
-  { label: '城南', value: '城南', prefix: 'CN' },
-  { label: '城北', value: '城北', prefix: 'CB' },
-  { label: '太常', value: '太常', prefix: 'TC' }
-]
-
-const generateEquipmentCode = () => {
-  if (!formData.district) return ''
-  const opt = districtOptions.find(o => o.value === formData.district)
-  if (opt && opt.prefix) {
-    const now = new Date()
-    const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
-    const timeStr = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
-    return `${opt.prefix}-GAS-${dateStr}${timeStr}`
-  }
-  return ''
-}
-
-const formData = reactive({
-  district: '',
-  equipmentCode: '',
-  equipmentName: '', equipmentModel: '',
-  equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '',
-  networkId: '', pointId: '',
-  longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '',
-  equipmentImage: ''
-})
-
-async function loadData() {
-  loading.value = true
-  try {
-    const res = await getEquipmentFullPage(pageNum.value, pageSize.value, searchForm)
-    tableData.value = (res.data && res.data.records) ? res.data.records : []
-    total.value = (res.data && res.data.total) ? res.data.total : 0
-  } catch (e) { console.error(e) } finally { loading.value = false }
-}
-function handleSearch() { pageNum.value = 1; loadData() }
-function handleReset() {
-  Object.assign(searchForm, { equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '', district: '' })
-  handleSearch()
-}
-async function loadEquipmentTypes() {
-  if (equipmentTypeOptions.value.length > 0) return
-  try {
-    const res = await getEquipmentTypeList()
-    const all = res.data || []
-    equipmentTypeOptions.value = all.filter(t => t.parentTypeId === '3')
-  } catch (e) { console.error(e) }
-}
-async function loadNetworkOptions() {
-  if (networkOptions.value.length > 0) return
-  try { const res = await getPipeNetworkOptions(); networkOptions.value = res.data || [] } catch (e) { console.error(e) }
-}
-async function loadPointOptions(networkId) {
-  try {
-    const params = networkId ? { networkId } : {}
-    const res = await getGasPipePointPage(1, 200, params)
-    pointOptions.value = (res.data && res.data.records) ? res.data.records : []
-  } catch (e) { console.error(e) }
-}
-function onNetworkChange(val) {
-  formData.pointId = ''
-  if (val) { loadPointOptions(val) } else { pointOptions.value = [] }
-}
-function onDistrictChange() {
-  // 当片区变更时,如果正在新增模式下,自动生成设备编码
-  if (!formData.equipmentId && formData.district) {
-    formData.equipmentCode = generateEquipmentCode()
-  }
-}
-/** 覆盖 el-upload 默认上传行为,不做任何请求 */
-function handleUploadRequest() {
-  // 空函数,阻止 el-upload 默认的 action 请求
-}
-
-/** 图片选择后处理(验证 + 预览) */
-function handleImageChange(file) {
-  if (!file.raw) return
-  const isImage = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
-  if (!isImage) {
-    ElMessage.error('只能上传图片文件 (JPG/PNG)!')
-    return
-  }
-  const isLt5M = file.raw.size / 1024 / 1024 < 5
-  if (!isLt5M) {
-    ElMessage.error('图片大小不能超过 5MB!')
-    return
-  }
-  equipmentImageFile = file.raw
-  const reader = new FileReader()
-  reader.readAsDataURL(file.raw)
-  reader.onload = () => {
-    equipmentImagePreview.value = reader.result
-  }
-}
-
-/** 图片删除 */
-function handleImageRemove() {
-  equipmentImageFile = null
-  equipmentImagePreview.value = ''
-  formData.equipmentImage = ''
-}
-
-function handleAdd() {
-  dialogTitle.value = '新增'
-  const template = { 
-    equipmentCode: '', 
-    equipmentName: '', 
-    equipmentModel: '', 
-    equipmentSpec: '', 
-    manufacturer: '', 
-    equipmentLocation: '', 
-    equipmentTypeId: '', 
-    networkId: '', 
-    pointId: '', 
-    longitude: null, 
-    latitude: null, 
-    maintainer: '', 
-    maintainerPhone: '', 
-    remark: '',
-    district: '',
-    equipmentImage: ''
-  }
-  if (formData.equipmentId) delete formData.equipmentId
-  Object.assign(formData, template)
-  equipmentImageFile = null
-  equipmentImagePreview.value = ''
-  dialogVisible.value = true
-  loadEquipmentTypes(); loadNetworkOptions()
-}
-function handleEdit(row) {
-  dialogTitle.value = '编辑'
-  Object.assign(formData, row)
-  // 如果有已有图片,直接使用返回的 URL 生成预览
-  if (row.equipmentImage) {
-    equipmentImagePreview.value = row.equipmentImage
-  } else {
-    equipmentImagePreview.value = ''
-  }
-  equipmentImageFile = null
-  dialogVisible.value = true
-  loadEquipmentTypes(); loadNetworkOptions()
-  if (row.networkId) loadPointOptions(row.networkId)
-}
-async function handleSave() {
-  const valid = await formRef.value.validate().catch(() => false)
-  if (!valid) return
-  
-  try {
-    // 如果有新图片,先通过 MinIO 通用接口上传到 MinIO
-    if (equipmentImageFile) {
-      const uploadFormData = new FormData()
-      uploadFormData.append('file', equipmentImageFile)
-      uploadFormData.append('dir', 'upload') // 指定存储目录(可选)
-      const uploadResponse = await request({
-        url: '/minio/upload',
-        method: 'post',
-        data: uploadFormData
-      })
-      formData.equipmentImage = uploadResponse.url || uploadResponse.fileName
-      // 上传完成后清空文件,避免重复上传
-      equipmentImageFile = null
-    }
-    
-    const isEdit = !!formData.equipmentId
-    if (isEdit) {
-      await updateEquipment(formData)
-    } else {
-      const res = await addEquipment(formData)
-      formData.equipmentId = res.data // 后端 saveWithCheck 返回实体 UUID
-      if (!formData.equipmentId) {
-        ElMessage.error('新增失败,未获取到设备 ID')
-        return
-      }
-    }
-    ElMessage.success(isEdit ? '修改成功' : '新增成功')
-    dialogVisible.value = false
-    loadData()
-  } catch (e) {
-    const msg = e?.response?.data?.msg || e?.message || '操作失败'
-    ElMessage.error(msg)
-  }
-}
-function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该设备信息?', '提示', { type: 'warning' }).then(async () => {
-    await deleteEquipment(row.equipmentId); ElMessage.success('删除成功'); loadData()
-  }).catch(() => {})
-}
-async function handleDetail(row) {
-  detailData.value = row
-  detailVisible.value = true
-}
-
-onMounted(() => { loadData() })
-</script>
-
-<style scoped>
-.equipment-container { padding: 16px; }
-.search-card { margin-bottom: 16px; }
-.search-form { display: flex; flex-wrap: wrap; }
-
-/* 表格图片样式 */
-.table-image-cell {
-  width: 80px;
-}
-
-.equipment-table-image {
-  width: 64px;
-  height: 64px;
-  border-radius: 4px;
-  cursor: pointer;
-  object-fit: cover;
-}
-
-/* 详情图片样式 */
-.detail-equipment-image {
-  width: 200px;
-  height: 200px;
-  border-radius: 6px;
-  cursor: pointer;
-  object-fit: cover;
-}
-
-/* 图片上传样式 */
-.image-uploader {
-  position: relative;
-  border: 1px dashed #d9d9d9;
-  border-radius: 6px;
-  cursor: pointer;
-  width: 200px;
-  height: 200px;
-}
-
-.image-uploader:hover {
-  border-color: #409eff;
-}
-
-.image-uploader .preview-image {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  display: block;
-  border-radius: 6px;
-}
-
-.image-uploader-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  height: 100%;
-  color: #909399;
-  font-size: 14px;
-}
-
-.image-uploader-placeholder .el-icon--upload {
-  font-size: 28px;
-  margin-bottom: 8px;
-}
-
-.image-uploader-cover {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  opacity: 0;
-  transition: opacity 0.3s;
-  border-radius: 6px;
-}
-
-.image-uploader:hover .image-uploader-cover {
-  opacity: 1;
-}
-
-.image-uploader-cover .el-icon {
-  color: #fff;
-  font-size: 28px;
-}
-</style>

+ 0 - 308
src/views/subSystem/basic/GasThreshold.vue

@@ -1,308 +0,0 @@
-<template>
-  <div class="app-container">
-    <!-- 搜索区 -->
-    <el-card class="search-card">
-      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-        <el-form-item label="设备编码">
-          <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleQuery">查询</el-button>
-          <el-button @click="resetQuery">重置</el-button>
-          <el-button type="success" @click="handleAdd">按设备类型批量修改阈值</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <!-- 表格 -->
-    <el-card class="table-card">
-      <el-table :data="tableData" v-loading="loading" border style="width: 100%">
-        <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
-        <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
-          <template #default="{ row }">
-            {{ row.equipmentName || getDeviceName(row.deviceCode) }}
-          </template>
-        </el-table-column>
-        <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
-          <template #default="{ row }">
-            {{ row.equipmentTypeName || getDeviceType(row.deviceCode) }}
-          </template>
-        </el-table-column>
-        <el-table-column label="最小值" prop="minValue" width="90" align="center" />
-        <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
-        <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
-        <el-table-column label="操作" width="160" align="center" fixed="right">
-          <template #default="{ row }">
-            <el-button v-if="row.id" type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <span v-else class="muted">未配置</span>
-            <el-button v-if="row.id" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
-        @size-change="handleSizeChange" @current-change="handlePageChange"
-        style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
-
-    <!-- 新增/编辑对话框 -->
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
-      <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="设备类型" prop="deviceType">
-          <el-select v-model="formData.deviceType" placeholder="请选择设备类型" filterable clearable style="width: 100%" @change="onDeviceTypeChange">
-            <el-option v-for="type in deviceTypeOptions" :key="type" :label="type" :value="type" />
-          </el-select>
-        </el-form-item>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="最小值" prop="minValue">
-              <el-input-number v-model="formData.minValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最小值" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="最大值" prop="maxValue">
-              <el-input-number v-model="formData.maxValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最大值" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注信息" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSave" :loading="submitLoading">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="GasThreshold">
-import { ref, reactive, onMounted } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { getWarningThresholdModulePage, saveWarningThreshold, updateWarningThreshold, deleteWarningThreshold, getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
-
-const GAS_MODULE_TYPE = '燃气'
-
-const deviceTypeOptions = ref([])
-const filteredDeviceOptions = ref([])
-
-// ==================== 搜索参数 ====================
-const queryParams = reactive({
-  deviceCode: ''
-})
-const queryRef = ref(null)
-
-function handleQuery() {
-  pageNum.value = 1
-  loadData()
-}
-function resetQuery() {
-  queryParams.deviceCode = ''
-  pageNum.value = 1
-  loadData()
-}
-
-// ==================== 表格数据 ====================
-const tableData = ref([])
-const total = ref(0)
-const loading = ref(false)
-const pageNum = ref(1)
-const pageSize = ref(10)
-
-async function loadData() {
-  loading.value = true
-  try {
-    const params = {}
-    if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
-    const res = await getWarningThresholdModulePage(pageNum.value, pageSize.value, params)
-    const pageData = res.data || res
-    tableData.value = pageData.records || []
-    total.value = pageData.total || 0
-  } catch (e) {
-    console.error('加载阈值列表失败', e)
-    ElMessage.error('加载阈值列表失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-function handlePageChange() { loadData() }
-function handleSizeChange() { pageNum.value = 1; loadData() }
-
-// ==================== 设备名称/类型映射 ====================
-const deviceInfoMap = ref({})
-
-async function ensureDeviceMap() {
-  if (Object.keys(deviceInfoMap.value).length > 0) return
-  try {
-    const res = await getEquipmentByTopLevelType(GAS_MODULE_TYPE)
-    const list = res.data || []
-    const map = {}
-    list.forEach(d => { map[d.equipmentCode] = d })
-    deviceInfoMap.value = map
-    deviceTypeOptions.value = [...new Set(list.map(d => d.equipmentTypeName).filter(Boolean))]
-    filteredDeviceOptions.value = list
-  } catch (e) { console.error('加载燃气设备列表失败', e) }
-}
-
-function getDeviceName(deviceCode) {
-  return String(deviceCode || '').split(',').map(code => {
-    const d = deviceInfoMap.value[code.trim()]
-    return d ? (d.equipmentName || code.trim()) : code.trim()
-  }).filter(Boolean).join('、') || '-'
-}
-
-function getDeviceType(deviceCode) {
-  const types = String(deviceCode || '').split(',').map(code => deviceInfoMap.value[code.trim()])
-    .filter(Boolean).map(d => d.equipmentTypeName).filter(Boolean)
-  return [...new Set(types)].join('、') || '-'
-}
-
-// ==================== 新增/编辑 ====================
-const dialogVisible = ref(false)
-const dialogTitle = ref('按设备类型批量修改阈值')
-const formRef = ref(null)
-const submitLoading = ref(false)
-const isEdit = ref(false)
-
-const rules = {
-  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
-}
-
-const formData = reactive({
-  id: '',
-  deviceType: '',
-  deviceCodes: [],
-  deviceCode: '',
-  minValue: null,
-  maxValue: null,
-  remark: ''
-})
-
-const deviceOptions = ref([])
-
-async function loadGasDeviceOptions() {
-  if (deviceOptions.value.length > 0) return
-  try {
-    const res = await getEquipmentByTopLevelType(GAS_MODULE_TYPE)
-    deviceOptions.value = res.data || []
-    deviceTypeOptions.value = [...new Set(deviceOptions.value.map(d => d.equipmentTypeName).filter(Boolean))]
-    filteredDeviceOptions.value = deviceOptions.value
-  } catch (e) {
-    console.error('加载燃气设备列表失败', e)
-  }
-}
-
-function resetForm() {
-  formData.id = ''
-  formData.deviceType = ''
-  formData.deviceCodes = []
-  formData.deviceCode = ''
-  formData.minValue = null
-  formData.maxValue = null
-  formData.remark = ''
-  isEdit.value = false
-  dialogTitle.value = '按设备类型批量修改阈值'
-}
-
-function onDeviceTypeChange(type) {
-  filteredDeviceOptions.value = deviceOptions.value.filter(d => d.equipmentTypeName === type)
-  formData.deviceCodes = filteredDeviceOptions.value.map(d => d.equipmentCode)
-}
-
-async function handleAdd() {
-  resetForm()
-  await loadGasDeviceOptions()
-  dialogVisible.value = true
-}
-
-async function handleEdit(row) {
-  await loadGasDeviceOptions()
-  isEdit.value = true
-  dialogTitle.value = '编辑阈值'
-  formData.id = row.id || ''
-  formData.deviceCode = row.deviceCode
-  formData.deviceCodes = String(row.deviceCode || '').split(',').map(v => v.trim()).filter(Boolean)
-  const firstDevice = deviceOptions.value.find(d => formData.deviceCodes.includes(d.equipmentCode))
-  formData.deviceType = firstDevice?.equipmentTypeName || ''
-  onDeviceTypeChange(formData.deviceType)
-  formData.minValue = row.minValue
-  formData.maxValue = row.maxValue
-  formData.remark = row.remark || ''
-  dialogVisible.value = true
-}
-
-async function handleSave() {
-  const valid = await formRef.value.validate().catch(() => false)
-  if (!valid) return
-  if (!formData.deviceCodes.length) {
-    ElMessage.warning('该设备类型下没有可配置的设备')
-    return
-  }
-  submitLoading.value = true
-  try {
-    const data = {
-      deviceCode: formData.deviceCodes.join(','),
-      minValue: formData.minValue,
-      maxValue: formData.maxValue,
-      remark: formData.remark || undefined
-    }
-    if (isEdit.value) {
-      data.id = formData.id
-      await updateWarningThreshold(data, GAS_MODULE_TYPE)
-      ElMessage.success('设备类型阈值批量修改成功')
-    } else {
-      await saveWarningThreshold(data, GAS_MODULE_TYPE)
-      ElMessage.success('设备类型阈值批量配置成功')
-    }
-    dialogVisible.value = false
-    loadData()
-    // 刷新设备映射
-    deviceInfoMap.value = {}
-    await ensureDeviceMap()
-  } catch (e) {
-    const msg = e?.response?.data?.msg || e?.message || '操作失败'
-    ElMessage.error(msg)
-  } finally {
-    submitLoading.value = false
-  }
-}
-
-// ==================== 删除 ====================
-function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该阈值配置?', '提示', { type: 'warning' }).then(async () => {
-    try {
-      await deleteWarningThreshold([row.id])
-      ElMessage.success('删除成功')
-      loadData()
-    } catch (e) {
-      ElMessage.error(e?.response?.data?.msg || '删除失败')
-    }
-  }).catch(() => {})
-}
-
-// ==================== 生命周期 ====================
-onMounted(async () => {
-  await ensureDeviceMap()
-  loadData()
-})
-</script>
-
-<style scoped>
-.app-container {
-  padding: 16px;
-}
-.search-card {
-  margin-bottom: 16px;
-}
-.search-form {
-  display: flex;
-  flex-wrap: wrap;
-}
-.table-card {
-  min-height: 400px;
-}
-</style>

+ 5 - 1087
src/views/subSystem/basic/GisDashboard.vue

@@ -1,1090 +1,8 @@
-<template>
-  <div class="gis-dashboard">
-    <div class="dashboard-header">
-      <div class="header-title">
-        <div class="title-main">
-          <el-icon><MapLocation /></el-icon>
-          <span>燃气管网GIS一张图</span>
-        </div>
-        <div class="title-metrics">
-          <span class="metric metric-danger">管线 {{ summaryStats.pipelineLength }}km</span>
-          <span class="metric metric-primary">燃气管点 {{ summaryStats.pipePointCount }}个</span>
-          <span class="metric metric-warning">窨井 {{ summaryStats.manholeCount }}个</span>
-        </div>
-      </div>
-      <div class="header-actions">
-        <el-input
-          v-model="searchKeyword"
-          class="search-input"
-          placeholder="搜索管点/井盖编号/位置"
-          clearable
-          @keyup.enter="handleSearch"
-        >
-          <template #prefix>
-            <el-icon><Search /></el-icon>
-          </template>
-        </el-input>
-        <el-button type="primary" @click="handleSearch">
-          <el-icon><Search /></el-icon>
-          搜索
-        </el-button>
-        <el-button @click="refreshScene">
-          <el-icon><RefreshRight /></el-icon>
-          刷新
-        </el-button>
-        <el-button @click="toggleFullscreen">
-          <el-icon><FullScreen /></el-icon>
-          全图
-        </el-button>
-      </div>
-    </div>
-
-    <div ref="screenRef" class="dashboard-content">
-      <aside class="dashboard-sidebar">
-        <el-card shadow="never" class="panel-card">
-          <template #header>
-            <span>图层控制</span>
-          </template>
-          <div class="layer-group">
-            <div class="layer-title">燃气管线</div>
-            <el-checkbox v-model="layerState.pipelines.high">高压管线</el-checkbox>
-            <el-checkbox v-model="layerState.pipelines.subHigh">次高压管线</el-checkbox>
-            <el-checkbox v-model="layerState.pipelines.medium">中压管线</el-checkbox>
-            <el-checkbox v-model="layerState.pipelines.low">低压管线</el-checkbox>
-          </div>
-          <div class="layer-group">
-            <div class="layer-title">燃气管点</div>
-            <el-checkbox v-model="layerState.pipePoints.valve">阀门</el-checkbox>
-            <el-checkbox v-model="layerState.pipePoints.regulator">调压器</el-checkbox>
-            <el-checkbox v-model="layerState.pipePoints.flowmeter">流量计</el-checkbox>
-            <el-checkbox v-model="layerState.pipePoints.terminal">终端节点</el-checkbox>
-          </div>
-          <div class="layer-group layer-group-last">
-            <div class="layer-title">窨井设施</div>
-            <el-checkbox v-model="layerState.manholes.cover">窨井盖</el-checkbox>
-            <el-checkbox v-model="layerState.manholes.inspection">检查井</el-checkbox>
-          </div>
-        </el-card>
-
-        <el-card shadow="never" class="panel-card">
-          <template #header>
-            <span>图例说明</span>
-          </template>
-          <div class="legend-list">
-            <div class="legend-item"><span class="legend-line legend-high"></span><span>高压管线</span></div>
-            <div class="legend-item"><span class="legend-line legend-sub-high"></span><span>次高压管线</span></div>
-            <div class="legend-item"><span class="legend-line legend-medium"></span><span>中压管线</span></div>
-            <div class="legend-item"><span class="legend-line legend-low"></span><span>低压管线</span></div>
-            <div class="legend-item"><span class="legend-dot legend-valve"></span><span>阀门</span></div>
-            <div class="legend-item"><span class="legend-dot legend-regulator"></span><span>调压器</span></div>
-            <div class="legend-item"><span class="legend-dot legend-flowmeter"></span><span>流量计</span></div>
-            <div class="legend-item"><span class="legend-dot legend-terminal"></span><span>终端节点</span></div>
-            <div class="legend-item"><span class="legend-square legend-cover"></span><span>窨井盖</span></div>
-            <div class="legend-item"><span class="legend-square legend-inspection"></span><span>检查井</span></div>
-          </div>
-        </el-card>
-
-        <el-card shadow="never" class="panel-card stats-card">
-          <template #header>
-            <span>测绘数据统计</span>
-          </template>
-          <div class="stats-list">
-            <div class="stats-row"><span>管线总长</span><strong>{{ summaryStats.pipelineLength }} km</strong></div>
-            <div class="stats-row"><span>管点总数</span><strong>{{ summaryStats.pipePointCount }}</strong></div>
-            <div class="stats-row"><span>窨井总数</span><strong>{{ summaryStats.manholeCount }}</strong></div>
-            <div class="stats-row"><span>覆盖面积</span><strong>{{ summaryStats.coverageArea }} km²</strong></div>
-          </div>
-        </el-card>
-
-        <el-card shadow="never" class="panel-card detail-card">
-          <template #header>
-            <span>要素详情</span>
-          </template>
-          <div v-if="selectedFeature" class="detail-body">
-            <div class="detail-name">{{ selectedFeature.name }}</div>
-            <div class="detail-tag">{{ selectedFeature.category }}</div>
-            <el-descriptions :column="1" border size="small">
-              <el-descriptions-item
-                v-for="item in selectedFeatureDetails"
-                :key="item.label"
-                :label="item.label"
-              >
-                {{ item.value }}
-              </el-descriptions-item>
-            </el-descriptions>
-          </div>
-          <el-empty v-else description="点击地图要素查看详情" :image-size="72" />
-        </el-card>
-      </aside>
-
-      <section class="dashboard-map-panel">
-        <div class="map-toolbar">
-          <div class="toolbar-left">
-            <el-button @click="zoomIn">
-              <el-icon><Plus /></el-icon>
-              放大
-            </el-button>
-            <el-button @click="zoomOut">
-              <el-icon><Minus /></el-icon>
-              缩小
-            </el-button>
-            <el-button @click="locateCenter">
-              <el-icon><Location /></el-icon>
-              定位
-            </el-button>
-          </div>
-          <div class="toolbar-right">当前坐标: {{ currentCoordinate }}</div>
-        </div>
-        <div id="gisMap" ref="mapRef" class="gis-map"></div>
-        <div v-if="mapError" class="map-error">{{ mapError }}</div>
-      </section>
-    </div>
-  </div>
-</template>
-
+<!-- 燃气管网 GIS 菜单实际入口,统一复用真实管网数据页面。 -->
 <script setup>
-import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
-import { FullScreen, Location, MapLocation, Minus, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
-import { convertCoord } from '@/utils/coordTransform'
-import { getPipeNetworkOptions, getGasPipePointPage, getManholeList } from '@/api/pipeNetwork/basic'
-
-const centerPoint = { lng: 110.386, lat: 28.445 }
-
-const MAP_BOUNDS = {
-  southWest: { lng: 110.355, lat: 28.428 },
-  northEast: { lng: 110.410, lat: 28.460 }
-}
-const MAP_MIN_ZOOM = 13
-const MAP_MAX_ZOOM = 19
-const MAP_DEFAULT_ZOOM = 15
-
-const defaultLayerState = () => ({
-  pipelines: { high: true, subHigh: true, medium: true, low: true },
-  pipePoints: { valve: true, regulator: true, flowmeter: true, terminal: true },
-  manholes: { cover: true, inspection: true }
-})
-
-const boundaryPoints = [
-  { lng: 110.358, lat: 28.456 },
-  { lng: 110.375, lat: 28.458 },
-  { lng: 110.392, lat: 28.458 },
-  { lng: 110.408, lat: 28.455 },
-  { lng: 110.410, lat: 28.448 },
-  { lng: 110.407, lat: 28.440 },
-  { lng: 110.400, lat: 28.436 },
-  { lng: 110.390, lat: 28.434 },
-  { lng: 110.378, lat: 28.433 },
-  { lng: 110.368, lat: 28.436 },
-  { lng: 110.360, lat: 28.445 }
-]
-
-const pipelineData = [
-  {
-    id: 'pipe-high-01', level: 'high', name: '燃气1', code: 'GX-HP-001', length: 4.1,
-    material: 'L290N 无缝钢管', diameter: 'DN300', pressure: '高压A (4.0MPa)', road: '燃气1主干线',
-    status: '运行正常', layingMethod: '直埋敷设', depth: '1.8m', commissioningDate: '2026-06',
-    points: [{lng:110.404947,lat:28.433419},{lng:110.404836,lat:28.433429},{lng:110.403275,lat:28.433571},{lng:110.401084,lat:28.433876},{lng:110.399601,lat:28.433876},{lng:110.398112,lat:28.433979},{lng:110.396717,lat:28.434314},{lng:110.394299,lat:28.434538},{lng:110.394352,lat:28.434887},{lng:110.391969,lat:28.434985},{lng:110.384374,lat:28.435695},{lng:110.381418,lat:28.43559},{lng:110.377993,lat:28.436193},{lng:110.376062,lat:28.436385},{lng:110.374231,lat:28.43655},{lng:110.372584,lat:28.437107},{lng:110.371292,lat:28.4377},{lng:110.370581,lat:28.438198},{lng:110.369831,lat:28.438821}]
-  },
-  {
-    id: 'pipe-subhigh-01', level: 'subHigh', name: '燃气2', code: 'GX-SHP-002', length: 3.8,
-    material: 'L245N 无缝钢管', diameter: 'DN250', pressure: '次高压A (1.6MPa)', road: '燃气2支线',
-    status: '运行正常', layingMethod: '直埋敷设', depth: '1.6m', commissioningDate: '2026-03',
-    points: [{lng:110.361433,lat:28.454979},{lng:110.362363,lat:28.453169},{lng:110.362851,lat:28.452213},{lng:110.364377,lat:28.452443},{lng:110.365138,lat:28.45246},{lng:110.366462,lat:28.452157},{lng:110.369549,lat:28.451441},{lng:110.37219,lat:28.450897},{lng:110.372753,lat:28.450901},{lng:110.373875,lat:28.451194},{lng:110.373913,lat:28.451107},{lng:110.374597,lat:28.451476}]
-  }
-]
-
-const pipePointData = ref([])
-const manholeData = ref([])
-
-async function loadGisData() {
-  try {
-    const [pointRes, manholeRes] = await Promise.all([
-      getGasPipePointPage(1, 500, {}),
-      getManholeList()
-    ])
-    const points = (pointRes.data && pointRes.data.records) ? pointRes.data.records : []
-    pipePointData.value = points.filter(p => p.longitude && p.latitude).map(p => ({
-      id: 'point-' + p.pointId, type: mapPointType(p.pointType), pipelineId: p.networkId,
-      name: p.pointName, code: p.pointCode, road: p.networkName || '', status: '在线',
-      deviceModel: p.material || '', caliber: p.diameter ? p.diameter + 'mm' : '',
-      lng: Number(p.longitude), lat: Number(p.latitude)
-    }))
-    const manholes = manholeRes.data || []
-    manholeData.value = manholes.filter(m => m.longitude && m.latitude).map(m => ({
-      id: 'manhole-' + m.manholeId, type: 'cover', pipelineId: m.networkId || '',
-      name: m.manholeName, code: m.manholeCode, road: '', status: m.status === '0' ? '完好' : '异常',
-      material: m.material || '', size: m.manholeShape || '', lng: Number(m.longitude), lat: Number(m.latitude)
-    }))
-    if (mapInstance && typeof BMapGL !== 'undefined') renderMapScene()
-  } catch (e) { console.error(e) }
-}
-
-function mapPointType(type) {
-  if (!type) return 'terminal'
-  if (type.includes('阀门') || type === 'valve') return 'valve'
-  if (type.includes('调压') || type === 'regulator') return 'regulator'
-  if (type.includes('计量') || type.includes('流量') || type === 'flowmeter') return 'flowmeter'
-  return 'terminal'
-}
-
-const colorMap = {
-  high: '#e63946',
-  subHigh: '#f77f00',
-  medium: '#fcbf49',
-  low: '#2a9d8f',
-  valve: '#457b9d',
-  regulator: '#e76f51',
-  flowmeter: '#2d6a4f',
-  terminal: '#6c757d',
-  cover: '#7f4f24',
-  inspection: '#b08968'
-}
-
-const labelMap = {
-  high: '高压管线',
-  subHigh: '次高压管线',
-  medium: '中压管线',
-  low: '低压管线',
-  valve: '阀门',
-  regulator: '调压器',
-  flowmeter: '流量计',
-  terminal: '终端节点',
-  cover: '窨井盖',
-  inspection: '检查井'
-}
-
-const mapRef = ref(null)
-const screenRef = ref(null)
-const searchKeyword = ref('')
-const currentCoordinate = ref(`${centerPoint.lng}, ${centerPoint.lat}`)
-const selectedFeature = ref(null)
-const mapError = ref('')
-const layerState = reactive(defaultLayerState())
-
-let mapInstance = null
-let activeInfoWindow = null
-let overlayClicked = false
-
-const summaryStats = computed(() => ({
-  pipelineLength: pipelineData.reduce((sum, item) => sum + item.length, 0).toFixed(1),
-  pipePointCount: pipePointData.value.length,
-  manholeCount: manholeData.value.length,
-  coverageArea: '18.6'
-}))
-
-const selectedFeatureDetails = computed(() => {
-  if (!selectedFeature.value) return []
-  return Object.entries(selectedFeature.value.details).map(([label, value]) => ({ label, value }))
-})
-
-const searchableFeatures = computed(() => {
-  const pipelines = pipelineData.map(item => ({
-    kind: 'pipeline',
-    type: item.level,
-    name: item.name,
-    code: item.code,
-    road: item.road,
-    lng: getMidPoint(item.points).lng,
-    lat: getMidPoint(item.points).lat,
-    raw: item
-  }))
-  const pipePoints = pipePointData.value.map(item => ({
-    kind: 'pipePoint',
-    type: item.type,
-    name: item.name,
-    code: item.code,
-    road: item.road,
-    lng: item.lng,
-    lat: item.lat,
-    raw: item
-  }))
-  const manholes = manholeData.value.map(item => ({
-    kind: 'manhole',
-    type: item.type,
-    name: item.name,
-    code: item.code,
-    road: item.road,
-    lng: item.lng,
-    lat: item.lat,
-    raw: item
-  }))
-  return [...pipelines, ...pipePoints, ...manholes]
-})
-
-watch(layerState, () => {
-  renderMapScene()
-}, { deep: true })
-
-onMounted(async () => {
-  await nextTick()
-  await loadGisData()
-  waitForBMapGL()
-  document.addEventListener('fullscreenchange', handleFullscreenChange)
-})
-
-onBeforeUnmount(() => {
-  document.removeEventListener('fullscreenchange', handleFullscreenChange)
-  if (mapInstance) {
-    mapInstance.clearOverlays()
-    mapInstance = null
-  }
-})
-
-function waitForBMapGL() {
-  if (typeof BMapGL !== 'undefined') {
-    initMap()
-    return
-  }
-
-  let attempts = 0
-  const maxAttempts = 50
-  const timer = setInterval(() => {
-    attempts++
-    if (typeof BMapGL !== 'undefined') {
-      clearInterval(timer)
-      initMap()
-      return
-    }
-    if (attempts >= maxAttempts) {
-      clearInterval(timer)
-      mapError.value = '百度地图脚本加载超时,请刷新页面重试。'
-    }
-  }, 200)
-}
-
-function initMap() {
-  const el = document.getElementById('gisMap')
-  if (!el) {
-    mapError.value = '地图容器未找到。'
-    return
-  }
-
-  try {
-    mapInstance = new BMapGL.Map('gisMap', { enableMapClick: true })
-  } catch (e) {
-    console.error('创建地图实例失败:', e)
-    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
-    return
-  }
-
-  try {
-    const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
-    const centerBMapPoint = new BMapGL.Point(clng, clat)
-    mapInstance.centerAndZoom(centerBMapPoint, MAP_DEFAULT_ZOOM)
-    mapInstance.enableScrollWheelZoom(true)
-  } catch (e) {
-    console.error('设置地图中心失败:', e)
-  }
-
-  try {
-    if (typeof mapInstance.setMinZoom === 'function') {
-      mapInstance.setMinZoom(MAP_MIN_ZOOM)
-    }
-    if (typeof mapInstance.setMaxZoom === 'function') {
-      mapInstance.setMaxZoom(MAP_MAX_ZOOM)
-    }
-  } catch (e) {
-    console.warn('设置缩放限制失败:', e)
-  }
-
-  try {
-    if (typeof mapInstance.setMaxBounds === 'function') {
-      const sw = new BMapGL.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
-      const ne = new BMapGL.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
-      mapInstance.setMaxBounds(new BMapGL.Bounds(sw, ne))
-    }
-  } catch (e) {
-    console.warn('设置地图范围限制失败:', e)
-  }
-
-  mapInstance.addEventListener('click', () => {
-    if (overlayClicked) {
-      overlayClicked = false
-      return
-    }
-    closeInfoWindow()
-    selectedFeature.value = null
-  })
-
-  mapInstance.addEventListener('mousemove', (event) => {
-    try {
-      if (event && event.latlng) {
-        currentCoordinate.value = `${event.latlng.lng.toFixed(4)}, ${event.latlng.lat.toFixed(4)}`
-      }
-    } catch (_) { /* ignore mousemove errors */ }
-  })
-
-  try {
-    renderMapScene()
-  } catch (e) {
-    console.error('渲染地图要素失败:', e)
-    mapError.value = '地图要素渲染失败。'
-  }
-}
-
-function renderMapScene() {
-  if (!mapInstance || typeof BMapGL === 'undefined') return
-
-  closeInfoWindow()
-  try { mapInstance.clearOverlays() } catch (_) { /* ignore */ }
-  try { drawPipelines() } catch (e) { console.warn('绘制管线失败:', e) }
-  try { drawPipePoints() } catch (e) { console.warn('绘制管点失败:', e) }
-  try { drawManholes() } catch (e) { console.warn('绘制窨井失败:', e) }
-}
-
-function drawBoundary() {
-  const pts = boundaryPoints.map(item => { const [lng, lat] = convertCoord(item.lng, item.lat); return new BMapGL.Point(lng, lat) })
-  const polygon = new BMapGL.Polygon(pts, {
-    strokeColor: '#52b788',
-    strokeWeight: 2.5,
-    strokeOpacity: 0.85,
-    strokeStyle: 'dashed',
-    fillColor: '#d8f3dc',
-    fillOpacity: 0.22
-  })
-  mapInstance.addOverlay(polygon)
-}
-
-function drawPipelines() {
-  pipelineData.forEach(item => {
-    try {
-      if (!layerState.pipelines[item.level]) return
-      const pts = item.points.map(p => { const [lng, lat] = convertCoord(p.lng, p.lat); return new BMapGL.Point(lng, lat) })
-      const polyline = new BMapGL.Polyline(pts, {
-        strokeColor: colorMap[item.level],
-        strokeWeight: 6,
-        strokeOpacity: 0.92
-      })
-      const firstPt = { lng: convertCoord(item.points[0].lng, item.points[0].lat)[0], lat: convertCoord(item.points[0].lng, item.points[0].lat)[1] }
-      try {
-        polyline.addEventListener('click', (e) => {
-          overlayClicked = true
-          e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
-          showPopup(firstPt.lng, firstPt.lat, buildPopupHTML(buildPipelineDetail(item)))
-          selectedFeature.value = buildPipelineDetail(item)
-        })
-      } catch (_) { /* polyline click listener failed */ }
-      mapInstance.addOverlay(polyline)
-
-      const rawMid = getMidPoint(item.points)
-      const mid = { lng: convertCoord(rawMid.lng, rawMid.lat)[0], lat: convertCoord(rawMid.lng, rawMid.lat)[1] }
-      addPipeLabel(mid.lng, mid.lat, item.name, colorMap[item.level], item)
-    } catch (e) {
-      console.warn(`绘制管线 ${item.name} 失败:`, e)
-    }
-  })
-}
-
-function drawPipePoints() {
-  pipePointData.value.forEach(item => {
-    try {
-      if (!layerState.pipePoints[item.type]) return
-      const [plng, plat] = convertCoord(item.lng, item.lat)
-      addPointMarker(plng, plat, colorMap[item.type], item.name, 'dot', () => {
-        showPopup(plng, plat, buildPopupHTML(buildPipePointDetail(item)))
-        selectedFeature.value = buildPipePointDetail(item)
-      })
-    } catch (e) {
-      console.warn(`绘制管点 ${item.name} 失败:`, e)
-    }
-  })
-}
-
-function drawManholes() {
-  manholeData.value.forEach(item => {
-    try {
-      if (!layerState.manholes[item.type]) return
-      const [mlng, mlat] = convertCoord(item.lng, item.lat)
-      addPointMarker(mlng, mlat, colorMap[item.type], item.name, 'square', () => {
-        showPopup(mlng, mlat, buildPopupHTML(buildManholeDetail(item)))
-        selectedFeature.value = buildManholeDetail(item)
-      })
-    } catch (e) {
-      console.warn(`绘制窨井 ${item.name} 失败:`, e)
-    }
-  })
-}
-
-function addPipeLabel(lng, lat, text, color, rawItem) {
-  const point = new BMapGL.Point(lng, lat)
-  const html = `<div class="pipe-label" style="color:${color}">${text}</div>`
-  const label = new BMapGL.Label(html, {
-    position: point,
-    offset: new BMapGL.Size(-28, -8)
-  })
-  label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
-  try {
-    label.addEventListener('click', (e) => {
-      overlayClicked = true
-      e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
-      showPopup(lng, lat, buildPopupHTML(buildPipelineDetail(rawItem)))
-      selectedFeature.value = buildPipelineDetail(rawItem)
-    })
-  } catch (_) { /* label click listener failed */ }
-  mapInstance.addOverlay(label)
-}
-
-function addPointMarker(lng, lat, color, name, shape, onClick) {
-  const point = new BMapGL.Point(lng, lat)
-  const size = 22
-  const borderRadius = shape === 'square' ? '3px' : '50%'
-  const html = `<div class="marker-wrap" style="cursor:pointer">
-    <div style="width:${size}px;height:${size}px;background:${color};border-radius:${borderRadius};border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.25);margin:0 auto;"></div>
-    <div class="marker-text">${name}</div>
-  </div>`
-  const label = new BMapGL.Label(html, {
-    position: point,
-    offset: new BMapGL.Size(-size / 2 - 3, -size - 8)
-  })
-  label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
-  try {
-    label.addEventListener('click', (e) => {
-      overlayClicked = true
-      e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
-      onClick && onClick()
-    })
-  } catch (_) { /* marker click listener failed */ }
-  mapInstance.addOverlay(label)
-}
-
-function showPopup(lng, lat, html) {
-  try {
-    closeInfoWindow()
-    const point = new BMapGL.Point(lng, lat)
-    const infoWindow = new BMapGL.InfoWindow(html, {
-      width: 310,
-      title: '',
-      enableMessage: false
-    })
-    activeInfoWindow = infoWindow
-    mapInstance.openInfoWindow(infoWindow, point)
-  } catch (e) {
-    console.warn('弹出信息窗口失败:', e)
-  }
-}
-
-function closeInfoWindow() {
-  if (activeInfoWindow && mapInstance) {
-    mapInstance.closeInfoWindow()
-    activeInfoWindow = null
-  }
-}
-
-function buildPopupHTML(detail) {
-  const rows = Object.entries(detail.details)
-    .map(([k, v]) => `<div class="popup-row"><span class="popup-label">${k}</span><span class="popup-value">${v}</span></div>`)
-    .join('')
-  return `<div class="info-popup">
-    <div class="popup-header">${detail.name}</div>
-    <div class="popup-tag">${detail.category}</div>
-    <div class="popup-divider"></div>
-    ${rows}
-  </div>`
-}
-
-function buildPipelineDetail(item) {
-  return {
-    name: item.name,
-    category: labelMap[item.level],
-    details: {
-      '管线编号': item.code,
-      '所属道路': item.road,
-      '管线长度': `${item.length} km`,
-      '管径': item.diameter,
-      '材质': item.material,
-      '压力等级': item.pressure,
-      '敷设方式': item.layingMethod,
-      '埋深': item.depth,
-      '投产日期': item.commissioningDate,
-      '运行状态': item.status
-    }
-  }
-}
-
-function getPipelineName(pipelineId) {
-  const found = pipelineData.find(p => p.id === pipelineId)
-  return found ? found.name : '—'
-}
-
-function buildPipePointDetail(item) {
-  return {
-    name: item.name,
-    category: labelMap[item.type],
-    details: {
-      '设备编号': item.code,
-      '所属管线': getPipelineName(item.pipelineId),
-      '所属道路': item.road,
-      '设备型号': item.deviceModel,
-      '口径': item.caliber,
-      '运行状态': item.status,
-      '经度': item.lng.toFixed(5),
-      '纬度': item.lat.toFixed(5)
-    }
-  }
-}
-
-function buildManholeDetail(item) {
-  return {
-    name: item.name,
-    category: labelMap[item.type],
-    details: {
-      '井盖编号': item.code,
-      '所属管线': getPipelineName(item.pipelineId),
-      '所属道路': item.road,
-      '材质': item.material,
-      '规格': item.size,
-      '巡检状态': item.status,
-      '经度': item.lng.toFixed(5),
-      '纬度': item.lat.toFixed(5)
-    }
-  }
-}
-
-function getMidPoint(points) {
-  return points[Math.floor(points.length / 2)]
-}
-
-function handleSearch() {
-  const keyword = searchKeyword.value.trim()
-  if (!keyword) {
-    ElMessage.warning('请输入搜索关键字')
-    return
-  }
-
-  const matched = searchableFeatures.value.find(item =>
-    [item.name, item.code, item.road].some(text => text?.includes(keyword))
-  )
-
-  if (!matched) {
-    ElMessage.info('未找到匹配要素')
-    return
-  }
-
-  const [mlng, mlat] = convertCoord(matched.lng, matched.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(mlng, mlat), 18)
-
-  if (matched.kind === 'pipeline') {
-    selectedFeature.value = buildPipelineDetail(matched.raw)
-    showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
-  } else if (matched.kind === 'pipePoint') {
-    selectedFeature.value = buildPipePointDetail(matched.raw)
-    showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
-  } else {
-    selectedFeature.value = buildManholeDetail(matched.raw)
-    showPopup(mlng, mlat, buildPopupHTML(selectedFeature.value))
-  }
-
-  ElMessage.success(`已定位到 ${matched.name}`)
-}
-
-async function refreshScene() {
-  layerState.pipelines = { high: true, subHigh: true, medium: true, low: true }
-  layerState.pipePoints = { valve: true, regulator: true, flowmeter: true, terminal: true }
-  layerState.manholes = { cover: true, inspection: true }
-  searchKeyword.value = ''
-  selectedFeature.value = null
-  closeInfoWindow()
-  await loadGisData()
-  locateCenter()
-}
-
-function locateCenter() {
-  if (!mapInstance) return
-  const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
-  mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), MAP_DEFAULT_ZOOM)
-}
-
-function zoomIn() {
-  if (!mapInstance) return
-  const z = mapInstance.getZoom()
-  if (z < MAP_MAX_ZOOM) mapInstance.zoomIn()
-  else ElMessage.info('已放大至最大级别')
-}
-
-function zoomOut() {
-  if (!mapInstance) return
-  const z = mapInstance.getZoom()
-  if (z > MAP_MIN_ZOOM) mapInstance.zoomOut()
-  else ElMessage.info('已缩小至最小级别')
-}
-
-function toggleFullscreen() {
-  const target = screenRef.value
-  if (!target) return
-  if (!document.fullscreenElement) {
-    target.requestFullscreen?.()
-    return
-  }
-  document.exitFullscreen?.()
-}
-
-function handleFullscreenChange() {
-  if (!mapInstance) return
-  const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat)
-  setTimeout(() => {
-    mapInstance.centerAndZoom(new BMapGL.Point(clng, clat), mapInstance.getZoom())
-  }, 200)
-}
+import GasPipelineGis from '@/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue'
 </script>
 
-<style scoped>
-.gis-dashboard {
-  height: calc(100vh - 84px);
-  padding: 16px;
-  background: #f2f5f9;
-  display: flex;
-  flex-direction: column;
-  gap: 0;
-}
-
-.dashboard-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 16px;
-  padding: 10px 16px;
-  margin-bottom: 14px;
-  background: #fff;
-  border-radius: 10px;
-  box-shadow: 0 4px 16px rgba(15, 35, 95, 0.05);
-  flex-shrink: 0;
-}
-
-.header-title {
-  display: flex;
-  align-items: center;
-  gap: 20px;
-  flex-wrap: wrap;
-}
-
-.title-main {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  color: #1f2d3d;
-  font-size: 17px;
-  font-weight: 700;
-}
-
-.title-metrics {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  flex-wrap: wrap;
-}
-
-.metric {
-  position: relative;
-  padding-left: 14px;
-  color: #44546a;
-  font-size: 13px;
-}
-
-.metric::before {
-  content: '';
-  position: absolute;
-  left: 0;
-  top: 50%;
-  width: 9px;
-  height: 9px;
-  border-radius: 50%;
-  transform: translateY(-50%);
-}
-
-.metric-danger::before { background: #e63946; }
-.metric-primary::before { background: #457b9d; }
-.metric-warning::before { background: #7f4f24; }
-
-.header-actions {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.search-input {
-  width: 300px;
-}
-
-.dashboard-content {
-  flex: 1;
-  display: flex;
-  gap: 14px;
-  min-height: 0;
-}
-
-.dashboard-sidebar {
-  width: 320px;
-  display: flex;
-  flex-direction: column;
-  gap: 14px;
-  flex-shrink: 0;
-  overflow-y: auto;
-}
-
-.panel-card {
-  border: none;
-  border-radius: 14px;
-  box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
-  flex-shrink: 0;
-}
-
-.layer-group {
-  display: flex;
-  flex-direction: column;
-  gap: 9px;
-  padding-bottom: 16px;
-  margin-bottom: 16px;
-  border-bottom: 1px solid #eef2f8;
-}
-
-.layer-group-last {
-  padding-bottom: 0;
-  margin-bottom: 0;
-  border-bottom: none;
-}
-
-.layer-title {
-  color: #303133;
-  font-size: 14px;
-  font-weight: 600;
-}
-
-.legend-list {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.legend-item {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  color: #4e5969;
-  font-size: 13px;
-}
-
-.legend-line {
-  width: 34px;
-  height: 0;
-  border-top-width: 3px;
-  border-top-style: solid;
-  border-radius: 999px;
-  flex-shrink: 0;
-}
-
-.legend-high { border-top-color: #e63946; }
-.legend-sub-high { border-top-color: #f77f00; }
-.legend-medium { border-top-color: #fcbf49; }
-.legend-low { border-top-color: #2a9d8f; }
-
-.legend-dot,
-.legend-square {
-  width: 12px;
-  height: 12px;
-  flex-shrink: 0;
-}
-
-.legend-dot { border-radius: 50%; }
-.legend-square { border-radius: 2px; }
-.legend-valve { background: #457b9d; }
-.legend-regulator { background: #e76f51; }
-.legend-flowmeter { background: #2d6a4f; }
-.legend-terminal { background: #6c757d; }
-.legend-cover { background: #7f4f24; }
-.legend-inspection { background: #b08968; }
-
-.stats-card {
-  background: linear-gradient(180deg, #f8fbff 0%, #f3f7fb 100%);
-}
-
-.stats-list {
-  display: flex;
-  flex-direction: column;
-  gap: 11px;
-}
-
-.stats-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  color: #4e5969;
-  font-size: 13px;
-}
-
-.stats-row strong {
-  color: #1f2d3d;
-  font-size: 14px;
-}
-
-.detail-card {
-  flex: 1;
-  min-height: 280px;
-}
-
-.detail-body {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.detail-name {
-  color: #1f2d3d;
-  font-size: 16px;
-  font-weight: 700;
-}
-
-.detail-tag {
-  display: inline-flex;
-  align-items: center;
-  width: fit-content;
-  padding: 3px 10px;
-  color: #2d6cdf;
-  background: #edf4ff;
-  border-radius: 999px;
-  font-size: 12px;
-}
-
-.dashboard-map-panel {
-  position: relative;
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  background: #fff;
-  border-radius: 14px;
-  overflow: hidden;
-  box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
-  min-width: 0;
-}
-
-.map-toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 12px;
-  padding: 10px 16px;
-  border-bottom: 1px solid #edf2f7;
-  flex-shrink: 0;
-}
-
-.toolbar-left {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.toolbar-right {
-  color: #7a8699;
-  font-size: 13px;
-}
-
-.gis-map {
-  flex: 1;
-  min-height: 0;
-  background: #e9f0e5;
-}
-
-.map-error {
-  position: absolute;
-  inset: 52px 0 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #909399;
-  background: rgba(255, 255, 255, 0.85);
-  font-size: 15px;
-  z-index: 10;
-}
-</style>
-
-<style>
-.pipe-label {
-  padding: 2px 8px;
-  font-size: 12px;
-  font-weight: 600;
-  line-height: 1.3;
-  white-space: nowrap;
-  text-shadow: 0 0 4px #fff, 0 0 4px #fff;
-  background: rgba(255, 255, 255, 0.7);
-  border-radius: 10px;
-}
-
-.marker-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.marker-text {
-  margin-top: 3px;
-  padding: 1px 7px;
-  color: #2f3a4f;
-  font-size: 11px;
-  line-height: 1.3;
-  white-space: nowrap;
-  background: rgba(255, 255, 255, 0.78);
-  border-radius: 10px;
-  text-shadow: 0 0 3px #fff;
-}
-
-.info-popup {
-  padding: 4px 2px;
-  font-size: 13px;
-  color: #333;
-}
-
-.popup-header {
-  font-size: 15px;
-  font-weight: 700;
-  color: #1a1a2e;
-  margin-bottom: 6px;
-}
-
-.popup-tag {
-  display: inline-block;
-  padding: 2px 10px;
-  font-size: 11px;
-  color: #1d4ed8;
-  background: #eff6ff;
-  border-radius: 999px;
-  margin-bottom: 10px;
-}
-
-.popup-divider {
-  height: 1px;
-  background: #e5e7eb;
-  margin-bottom: 10px;
-}
-
-.popup-row {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 5px 0;
-  border-bottom: 1px dashed #f0f0f0;
-  font-size: 12px;
-  line-height: 1.5;
-}
-
-.popup-row:last-child {
-  border-bottom: none;
-}
-
-.popup-label {
-  color: #6b7280;
-  flex-shrink: 0;
-  margin-right: 12px;
-}
-
-.popup-value {
-  color: #1f2937;
-  font-weight: 500;
-  text-align: right;
-  word-break: break-all;
-}
-
-.BMap_cpyCtrl,
-.anchorBL {
-  display: none;
-}
-</style>
+<template>
+  <GasPipelineGis />
+</template>

+ 139 - 9
src/views/subSystem/drainage/home.vue

@@ -57,6 +57,11 @@
       </div>
       <div class="panel-center">
         <div id="rwMap" class="map-container"></div>
+        <div class="pipeline-map-status">
+          <span class="pipeline-status-dot"></span>
+          排水管网 {{ drainagePipelines.length }} 段
+        </div>
+        <div v-if="pipelineMapError" class="pipeline-map-error">{{ pipelineMapError }}</div>
       </div>
       <div class="panel-right">
         <div class="panel-box">
@@ -291,6 +296,8 @@ import * as echarts from 'echarts'
 import locationIcon from '@/assets/images/location.png'
 import { getEquipmentPage, getWaterloggingPage, getPartitionPage, getEquipmentById, getAlarmDataPage } from '@/api/drainage'
 import { getAlarmPage } from '@/api/pipeNetwork/hazard'
+import { getGisPipeLines } from '@/api/pipeNetwork/basic'
+import { convertCoord } from '@/utils/coordTransform'
 
 const BAIDU_MAP_AK = 'mvSNSN4CPN6WJs2Xi6sQFNmWwCLRgU6T'
 const MAP_STYLE_ID = '7a6c3490bbc3d1e3e9d8602530301e10'
@@ -378,6 +385,8 @@ const rwRealtimeData = ref([])
 const rwWarnSummary = ref({ online: 0, offline: 0 })
 const rwWarnData = ref([])
 const rwMapPoints = ref([])
+const drainagePipelines = ref([])
+const pipelineMapError = ref('')
 
 // ==================== Tab 2: 设备管理 ====================
 const deviceOverview = ref({ total: 0, online: 0, offline: 0 })
@@ -415,28 +424,91 @@ const waterStatType = ref('month')
 
 // ==================== 地图实例 ====================
 const rwMap = ref(null), deviceMap = ref(null), waterMap = ref(null)
+let bmapInitTimer = null
 
 function initBMapGL(containerId, mapRef, points, clickHandler) {
   const container = document.getElementById(containerId)
-  if (!container || typeof BMapGL === 'undefined') { console.warn('BMapGL 未加载或容器不存在:', containerId); return }
+  if (!container || typeof BMapGL === 'undefined') return false
   try {
     const map = new BMapGL.Map(containerId)
     map.enableScrollWheelZoom(true)
     map.setMapStyleV2({ styleId: MAP_STYLE_ID })
     mapRef.value = map
+    // 首页中间的雨水污水地图展示 pipe_network_base 中的排水管网;
+    // 设备、积水页签仍只展示各自的业务点位。
+    const viewPoints = containerId === 'rwMap' ? drawDrainagePipelines(map) : []
     if (points.length > 0) {
       points.forEach(p => addLabelMarker(map, p, clickHandler))
-      const viewPoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
+      viewPoints.push(...points.map(p => {
+        const [lng, lat] = convertCoord(Number(p.lng), Number(p.lat))
+        return new BMapGL.Point(lng, lat)
+      }))
+    }
+    if (viewPoints.length > 0) {
       map.setViewport(viewPoints)
-      if (map.getZoom() < 13) map.setZoom(13)
+      if (containerId !== 'rwMap' && map.getZoom() < 13) map.setZoom(13)
     } else {
       map.centerAndZoom(new BMapGL.Point(110.393, 28.452), 14)
     }
-  } catch (e) { console.error('百度地图初始化失败:', e) }
+    return true
+  } catch (e) {
+    console.error('百度地图初始化失败:', e)
+    if (containerId === 'rwMap') pipelineMapError.value = '排水管网地图初始化失败,请刷新页面重试。'
+    return false
+  }
+}
+
+function escapeMapText(value) {
+  return String(value ?? '')
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;')
+}
+
+function getPipelineColor(level) {
+  const text = String(level || '').toLowerCase()
+  if (text.includes('主') || text.includes('一级') || text.includes('main')) return '#00e5ff'
+  if (text.includes('支') || text.includes('二级') || text.includes('branch')) return '#409eff'
+  return '#67c23a'
+}
+
+function drawDrainagePipelines(map) {
+  const viewportPoints = []
+  drainagePipelines.value.forEach(line => {
+    if (!Array.isArray(line.points) || line.points.length < 2) return
+    const bmapPoints = line.points.map(point => {
+      const [lng, lat] = convertCoord(point.lng, point.lat)
+      return new BMapGL.Point(lng, lat)
+    })
+    const polyline = new BMapGL.Polyline(bmapPoints, {
+      strokeColor: getPipelineColor(line.level),
+      strokeWeight: 5,
+      strokeOpacity: 0.9
+    })
+    polyline.addEventListener('click', event => {
+      event?.domEvent?.stopPropagation?.()
+      const content = `<div style="min-width:220px;color:#303133;line-height:1.8">
+        <div style="font-size:15px;font-weight:700;color:#1677ff;margin-bottom:6px">${escapeMapText(line.name || '排水管网')}</div>
+        <div>设施编号:${escapeMapText(line.code || '-')}</div>
+        <div>管网等级:${escapeMapText(line.level || '-')}</div>
+        <div>管径:${escapeMapText(line.diameter ?? '-')} mm</div>
+        <div>长度:${escapeMapText(line.length ?? '-')} m</div>
+        <div>材质:${escapeMapText(line.material || '-')}</div>
+        <div>位置:${escapeMapText(line.location || '-')}</div>
+      </div>`
+      map.openInfoWindow(new BMapGL.InfoWindow(content, { width: 280, enableMessage: false }), bmapPoints[0])
+    })
+    map.addOverlay(polyline)
+    viewportPoints.push(...bmapPoints)
+  })
+  return viewportPoints
 }
 
 function addLabelMarker(map, point, clickHandler) {
-  const bPoint = new BMapGL.Point(point.lng, point.lat)
+  const [lng, lat] = convertCoord(Number(point.lng), Number(point.lat))
+  const bPoint = new BMapGL.Point(lng, lat)
   const color = point.color || '#409eff'
   const icon = point.icon || locationIcon
   const html = `<div style="text-align:center;cursor:pointer;">
@@ -494,9 +566,45 @@ function getBarOption(xData, yData, name = '数值', color = '#409eff') {
 
 // ==================== 数据加载(mock + API 占位) ====================
 async function loadAllData() {
-  await loadRwTabData()
-  await loadEquipmentData()
-  await loadWaterloggingData()
+  await Promise.all([
+    loadDrainagePipelineData(),
+    loadRwTabData(),
+    loadEquipmentData(),
+    loadWaterloggingData()
+  ])
+}
+
+async function loadDrainagePipelineData() {
+  pipelineMapError.value = ''
+  try {
+    const response = await getGisPipeLines({ networkType: '1' })
+    const features = Array.isArray(response?.data?.features) ? response.data.features : []
+    drainagePipelines.value = features.map(feature => {
+      const properties = feature?.properties || {}
+      const coordinates = feature?.geometry?.type === 'LineString' ? feature.geometry.coordinates : []
+      const points = coordinates
+        .map(item => ({ lng: Number(item?.[0]), lat: Number(item?.[1]) }))
+        .filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
+      return {
+        id: properties.networkId,
+        code: properties.networkCode,
+        name: properties.networkName,
+        level: properties.networkLevel,
+        diameter: properties.diameter,
+        length: properties.length,
+        material: properties.material,
+        location: properties.location,
+        points
+      }
+    }).filter(line => line.points.length >= 2)
+    if (!drainagePipelines.value.length) {
+      pipelineMapError.value = '暂无带完整起止坐标的排水管网数据'
+    }
+  } catch (error) {
+    drainagePipelines.value = []
+    pipelineMapError.value = '排水管网数据加载失败,请检查后端服务。'
+    console.error('加载排水管网 GIS 线路失败:', error)
+  }
 }
 
 async function loadEquipmentData() {
@@ -947,6 +1055,24 @@ function initTabMaps() {
   initBMapGL('waterMap', waterMap, waterMapPoints.value, null)
 }
 
+function waitForBMapAndInit() {
+  let attempts = 0
+  const maxAttempts = 100
+  const tryInit = () => {
+    attempts += 1
+    if (typeof BMapGL !== 'undefined') {
+      initTabMaps()
+      return
+    }
+    if (attempts >= maxAttempts) {
+      pipelineMapError.value = '百度地图加载超时,请检查网络或地图配置。'
+      return
+    }
+    bmapInitTimer = window.setTimeout(tryInit, 200)
+  }
+  tryInit()
+}
+
 function switchTab(tab) {
   activeTab.value = tab
   nextTick(() => {
@@ -967,7 +1093,7 @@ onMounted(async () => {
   updateRefreshTime()
   await Promise.all([loadAllData(), loadWeatherData()])
   setTimeout(() => {
-    initTabMaps()
+    waitForBMapAndInit()
     initTab1Charts()
     initTab2Charts()
     initTab3Charts()
@@ -976,6 +1102,7 @@ onMounted(async () => {
 
 onUnmounted(() => {
   if (timer) clearInterval(timer)
+  if (bmapInitTimer) window.clearTimeout(bmapInitTimer)
   Object.values(chartInstances).forEach(c => c && c.dispose())
 })
 
@@ -1053,6 +1180,9 @@ window.addEventListener('resize', () => {
 
 /* ==================== 地图 ==================== */
 .map-container { flex: 1; border-radius: 6px; overflow: hidden; border: 1px solid rgba(64, 158, 255, 0.2); min-height: 0; }
+.pipeline-map-status { position: absolute; top: 12px; left: 12px; z-index: 5; display: flex; align-items: center; gap: 7px; padding: 7px 12px; color: #d9f4ff; background: rgba(6, 30, 65, 0.86); border: 1px solid rgba(0, 229, 255, 0.42); border-radius: 4px; font-size: 12px; pointer-events: none; }
+.pipeline-status-dot { width: 9px; height: 9px; background: #00e5ff; border-radius: 50%; box-shadow: 0 0 8px rgba(0, 229, 255, 0.9); }
+.pipeline-map-error { position: absolute; left: 50%; bottom: 18px; z-index: 5; max-width: 70%; padding: 8px 14px; color: #ffd591; background: rgba(80, 45, 0, 0.88); border: 1px solid rgba(230, 162, 60, 0.55); border-radius: 4px; font-size: 12px; text-align: center; transform: translateX(-50%); pointer-events: none; }
 .chart-area-sm { width: 100%; flex: 1; min-height: 130px; }
 
 /* ==================== 预警卡片 ==================== */

+ 0 - 546
src/views/subSystem/drainage/jcbj/bjsh.vue

@@ -1,546 +0,0 @@
-<template>
-  <div class="app-container bjsh-page">
-    <!-- ==================== 上半部分:GIS地图 ==================== -->
-    <div class="map-section">
-      <div id="bjshMap" class="map-container"></div>
-    </div>
-
-    <!-- ==================== 下半部分:搜索 + 报警审核表格 ==================== -->
-    <div class="table-section">
-      <!-- 搜索区 -->
-      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-        <el-form-item label="设备名称">
-          <el-input v-model="queryParams.equipmentName" placeholder="请输入设备名称" clearable style="width: 200px" />
-        </el-form-item>
-        <el-form-item label="报警等级">
-          <el-select v-model="queryParams.warningLevel" placeholder="请选择" clearable style="width: 200px">
-            <el-option label="严重" :value="1" />
-            <el-option label="重要" :value="2" />
-            <el-option label="一般" :value="3" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="审核状态">
-          <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 200px">
-            <el-option label="待审核" :value="1" />
-            <el-option label="已审核" :value="2" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleQuery">搜索</el-button>
-          <el-button @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-
-      <!-- 报警审核表格 -->
-      <el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
-        <el-table-column prop="id" label="报警编号" width="80" align="center" />
-        <el-table-column prop="equipmentName" label="设备名称" min-width="140" />
-        <el-table-column label="报警等级" width="100">
-          <template #default="{ row }">
-            <el-tag :type="levelTagType(row.warningLevel)" size="small">{{ levelText(row.warningLevel) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="warningTime" label="报警时间" width="160" />
-        <el-table-column label="监测值" width="100">
-          <template #default="{ row }">
-            <span :style="{ color: parseFloat(row.currentValue) > parseFloat(row.thresholdValue) ? '#f56c6c' : '#303133' }">{{ row.currentValue }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="审核状态" width="100">
-          <template #default="{ row }">
-            <el-tag :type="auditStatusTagType(row.status)" size="small">{{ auditStatusText(row.status) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="160" align="center">
-          <template #default="{ row }">
-            <el-button v-if="row.status === 1" link type="primary" @click="openAudit(row)">审核</el-button>
-            <el-button link type="primary" @click="openView(row)">查看</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-
-    <!-- ==================== 审核弹窗 ==================== -->
-    <el-dialog v-model="auditVisible" title="报警审核" width="700px" :before-close="closeAudit">
-      <!-- 报警信息展示 -->
-      <el-descriptions :column="2" border size="small" style="margin-bottom: 16px">
-        <el-descriptions-item label="设备名称">{{ currentAlarm.equipmentName }}</el-descriptions-item>
-        <el-descriptions-item label="报警时间">{{ currentAlarm.warningTime }}</el-descriptions-item>
-        <el-descriptions-item label="报警等级">
-          <el-tag :type="levelTagType(currentAlarm.warningLevel)" size="small">{{ levelText(currentAlarm.warningLevel) }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="指标值">{{ currentAlarm.warningType || '-' }}:{{ currentAlarm.currentValue }}(阈值 {{ currentAlarm.thresholdValue }})</el-descriptions-item>
-      </el-descriptions>
-
-      <!-- 审核操作 -->
-      <div class="audit-form-section">
-        <div class="section-title">审核操作</div>
-        <el-form :model="auditForm" label-width="90px" style="margin-top: 12px">
-          <el-form-item label="报警类型">
-            <el-tag type="info" size="small">{{ currentAlarm.warningType || '设备报警' }}</el-tag>
-          </el-form-item>
-          <el-form-item label="审核结果" required>
-            <el-radio-group v-model="auditForm.result">
-              <el-radio v-for="opt in auditResultOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="审核/解除意见" required>
-            <el-input v-model="auditForm.opinion" type="textarea" :rows="3" :placeholder="auditForm.result === '解除报警' ? '请输入解除原因及说明' : '请输入审核意见'" />
-          </el-form-item>
-          <el-form-item label="附件上传">
-            <el-upload
-              action="#"
-              :auto-upload="false"
-              :file-list="auditFiles"
-              :limit="5"
-              :on-change="handleFileChange"
-              :on-remove="handleFileRemove"
-              accept=".jpg,.jpeg,.png,.pdf,.doc,.docx"
-            >
-              <el-button type="primary" size="small">选择文件</el-button>
-              <template #tip>
-                <div class="upload-tip">支持jpg/png/pdf/doc格式,最多5个文件,单文件不超过10MB</div>
-              </template>
-            </el-upload>
-          </el-form-item>
-        </el-form>
-      </div>
-
-      <!-- 处置流程时间线 -->
-      <div class="audit-timeline-section">
-        <div class="section-title">处置全流程跟踪</div>
-        <el-timeline style="margin-top: 12px; padding-left: 2px">
-          <el-timeline-item :timestamp="currentAlarm.warningTime || ''" placement="top" color="#f56c6c">
-            <div class="timeline-node">报警触发</div>
-            <div class="timeline-desc">{{ currentAlarm.equipmentName }} {{ currentAlarm.warningType || '' }}超标,当前值:{{ currentAlarm.currentValue || '-' }},阈值:{{ currentAlarm.thresholdValue || '-' }}</div>
-          </el-timeline-item>
-          <el-timeline-item placement="top" color="#e6a23c">
-            <div class="timeline-node">系统通知</div>
-            <div class="timeline-desc">已自动推送至值守人员及相关负责人</div>
-          </el-timeline-item>
-          <el-timeline-item v-if="currentAlarm.firstApproverName" placement="top" color="#409eff">
-            <div class="timeline-node">下派处理</div>
-            <div class="timeline-desc">已下派至:{{ currentAlarm.firstApproverName }}</div>
-          </el-timeline-item>
-          <el-timeline-item timestamp="当前" placement="top" color="#409eff">
-            <div class="timeline-node">人工审核</div>
-            <div class="timeline-desc">审核进行中,等待审核结果</div>
-          </el-timeline-item>
-          <el-timeline-item timestamp="待完成" placement="top" color="#909399">
-            <div class="timeline-node">处置完成</div>
-            <div class="timeline-desc">审核通过后完成闭环</div>
-          </el-timeline-item>
-        </el-timeline>
-      </div>
-
-      <template #footer>
-        <el-button @click="closeAudit">取消</el-button>
-        <el-button type="primary" :loading="submitting" @click="submitAudit">提交审核</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- ==================== 查看详情弹窗 ==================== -->
-    <el-dialog v-model="viewVisible" title="报警处置全流程" width="650px">
-      <el-descriptions :column="2" border size="small">
-        <el-descriptions-item label="报警编号">{{ viewRow.id }}</el-descriptions-item>
-        <el-descriptions-item label="设备名称">{{ viewRow.equipmentName }}</el-descriptions-item>
-        <el-descriptions-item label="报警等级">
-          <el-tag :type="levelTagType(viewRow.warningLevel)" size="small">{{ levelText(viewRow.warningLevel) }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="报警时间">{{ viewRow.warningTime }}</el-descriptions-item>
-        <el-descriptions-item label="监测指标">{{ viewRow.warningType || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="实时值">{{ viewRow.currentValue }}</el-descriptions-item>
-        <el-descriptions-item label="阈值">{{ viewRow.thresholdValue }}</el-descriptions-item>
-        <el-descriptions-item label="审核状态">
-          <el-tag :type="auditStatusTagType(viewRow.status)" size="small">{{ auditStatusText(viewRow.status) }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item v-if="viewRow.auditResult" label="审核结果">{{ viewRow.auditResult }}</el-descriptions-item>
-        <el-descriptions-item v-if="viewRow.auditOpinion" label="审核意见">{{ viewRow.auditOpinion }}</el-descriptions-item>
-        <el-descriptions-item label="位置" :span="2">{{ viewRow.location || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="描述" :span="2">{{ viewRow.content || '-' }}</el-descriptions-item>
-      </el-descriptions>
-      <!-- 处置全流程跟踪 -->
-      <div style="margin-top: 16px">
-        <div class="section-title">处置全流程跟踪</div>
-        <el-timeline style="margin-top: 12px; padding-left: 2px">
-          <el-timeline-item :timestamp="viewRow.warningTime || ''" placement="top" color="#f56c6c">
-            <div class="timeline-node">报警触发</div>
-            <div class="timeline-desc">{{ viewRow.equipmentName }} {{ viewRow.warningType || '' }}超标,当前值:{{ viewRow.currentValue || '-' }}</div>
-          </el-timeline-item>
-          <el-timeline-item placement="top" color="#e6a23c">
-            <div class="timeline-node">系统通知</div>
-            <div class="timeline-desc">已自动推送至值守人员及相关负责人</div>
-          </el-timeline-item>
-          <el-timeline-item v-if="viewRow.firstApproverName" placement="top" color="#409eff">
-            <div class="timeline-node">下派处理</div>
-            <div class="timeline-desc">已下派至:{{ viewRow.firstApproverName }}</div>
-          </el-timeline-item>
-          <el-timeline-item v-if="viewRow.status === 2" placement="top" color="#67c23a">
-            <div class="timeline-node">审核完成</div>
-            <div class="timeline-desc">审核结果:{{ viewRow.auditResult || '-' }};意见:{{ viewRow.auditOpinion || '-' }}</div>
-          </el-timeline-item>
-          <el-timeline-item v-if="viewRow.status === 2" placement="top" color="#67c23a">
-            <div class="timeline-node">处置完成</div>
-            <div class="timeline-desc">报警处置流程已闭环</div>
-          </el-timeline-item>
-          <el-timeline-item v-if="viewRow.status !== 2" timestamp="进行中" placement="top" color="#909399">
-            <div class="timeline-node">待审核</div>
-            <div class="timeline-desc">等待人工审核处理</div>
-          </el-timeline-item>
-        </el-timeline>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="Bjsh">
-import { ref, computed, onMounted, nextTick } from 'vue'
-import { ElMessage } from 'element-plus'
-import locationIcon from '@/assets/images/location.png'
-import { approveAlarmToWorkOrder, getAlarmDataPage, handleAlarm } from '@/api/drainage'
-import useUserStore from '@/store/modules/user'
-
-// ==================== 用户信息 ====================
-const userStore = useUserStore()
-const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
-
-// ==================== 地图变量 ====================
-let mapInstance = null
-let mapInitialized = false
-
-// ==================== 等级/状态映射 ====================
-const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
-const levelColorMap = { 1: '#f56c6c', 2: '#e6a23c', 3: '#409eff' }
-const auditStatusMap = { 1: '待审核', 2: '已审核' }
-const auditStatusColorMap = { 1: '#e6a23c', 2: '#67c23a' }
-
-function levelText(level) {
-  return levelMap[level] || '-'
-}
-function levelTagType(level) {
-  if (level === 1) return 'danger'
-  if (level === 2) return 'warning'
-  return ''
-}
-function auditStatusText(status) {
-  return auditStatusMap[status] || '-'
-}
-function auditStatusTagType(status) {
-  if (status === 2) return 'success'
-  return 'warning'
-}
-
-// ==================== 搜索参数 ====================
-const queryParams = ref({
-  equipmentName: '',
-  warningLevel: '',
-  status: ''
-})
-const queryRef = ref(null)
-
-function handleQuery() {
-  loadData()
-}
-function resetQuery() {
-  queryParams.value = { equipmentName: '', warningLevel: '', status: '' }
-  loadData()
-}
-
-// ==================== 表格数据 ====================
-const tableData = ref([])
-const loading = ref(false)
-
-async function loadData() {
-  loading.value = true
-  try {
-    // 查询排水全部报警,由综合审核状态筛选待审核/已审核。
-    // 不能固定传 alarmStatus=0,否则已审核记录无法在页面显示。
-    const res = await getAlarmDataPage(1, 200, { equipmentType: 'drainage' })
-    const pageData = res.code !== undefined ? res.data : res
-    const records = pageData.records || pageData.rows || []
-    let tableData_local = records.map(row => ({
-      id: row.id,
-      equipmentId: row.deviceCode,
-      equipmentName: row.deviceName || '-',
-      warningTime: row.alarmTime,
-      warningLevel: row.alarmLevel,
-      content: row.warningType || '-',
-      location: '-',
-      longitude: row.longitude ? parseFloat(row.longitude) : null,
-      latitude: row.latitude ? parseFloat(row.latitude) : null,
-      currentValue: row.actualValue,
-      thresholdValue: row.maxValue,
-      warningType: row.warningType,
-      // 以接口返回的综合审核状态为准;已有工单的历史报警不能继续显示为待审核。
-      status: row.auditStatus != null
-        ? Number(row.auditStatus)
-        : (row.hasWorkOrder || row.alarmStatus === 1 ? 2 : 1),
-      source: 'alarm_data'
-    }))
-
-    // 前端筛选(equipmentName/warningLevel/status)
-    let filtered = tableData_local
-    if (queryParams.value.equipmentName) {
-      filtered = filtered.filter(item => item.equipmentName.includes(queryParams.value.equipmentName))
-    }
-    if (queryParams.value.warningLevel) {
-      filtered = filtered.filter(item => item.warningLevel === queryParams.value.warningLevel)
-    }
-    if (queryParams.value.status !== '' && queryParams.value.status !== null) {
-      filtered = filtered.filter(item => item.status === queryParams.value.status)
-    }
-
-    tableData.value = filtered
-
-    // 如果地图已初始化,刷新标记
-    if (mapInitialized) {
-      refreshMapMarkers()
-    }
-  } catch (error) {
-    console.error('加载报警数据失败', error)
-    ElMessage.error('加载报警数据失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-// ==================== 审核弹窗 ====================
-const auditVisible = ref(false)
-const currentAlarm = ref({})
-const auditForm = ref({ result: '', opinion: '' })
-
-// 各类报警预定义的审核结果选项
-const auditResultOptionsMap = {
-  '设备报警': [
-    { label: '确认报警 - 设备异常需维修', value: '确认报警' },
-    { label: '解除报警 - 设备恢复正常', value: '解除报警' },
-    { label: '误报处理 - 传感器误触发', value: '误报处理' },
-    { label: '降级处理 - 下调报警等级', value: '降级处理' }
-  ],
-  '水位报警': [
-    { label: '确认报警 - 水位超标需处置', value: '确认报警' },
-    { label: '解除报警 - 水位已回落', value: '解除报警' },
-    { label: '误报处理 - 监测数据异常', value: '误报处理' }
-  ],
-  '压力报警': [
-    { label: '确认报警 - 管网压力异常', value: '确认报警' },
-    { label: '解除报警 - 压力恢复正常', value: '解除报警' },
-    { label: '误报处理 - 压力波动属正常范围', value: '误报处理' }
-  ],
-  '默认': [
-    { label: '确认报警', value: '确认报警' },
-    { label: '解除报警', value: '解除报警' },
-    { label: '误报处理', value: '误报处理' }
-  ]
-}
-const auditResultOptions = computed(() => {
-  const type = currentAlarm.value.warningType || '默认'
-  return auditResultOptionsMap[type] || auditResultOptionsMap['默认']
-})
-const auditFiles = ref([])
-const submitting = ref(false)
-
-async function openAudit(row) {
-  currentAlarm.value = { ...row }
-  auditForm.value = { result: '', opinion: '' }
-  auditFiles.value = []
-  auditVisible.value = true
-}
-function closeAudit() {
-  auditVisible.value = false
-}
-function handleFileChange(file) {
-  auditFiles.value.push(file)
-}
-function handleFileRemove(file) {
-  const idx = auditFiles.value.findIndex(f => f.uid === file.uid)
-  if (idx !== -1) auditFiles.value.splice(idx, 1)
-}
-async function submitAudit() {
-  if (!auditForm.value.result) {
-    ElMessage.warning('请选择审核结果')
-    return
-  }
-  if (!auditForm.value.opinion) {
-    ElMessage.warning('请输入审核意见')
-    return
-  }
-  submitting.value = true
-  try {
-    // “确认报警/降级处理”审核通过后自动上传为运维工单;误报/解除仍只关闭报警
-    const isApproved = ['确认报警', '降级处理'].includes(auditForm.value.result)
-    const res = isApproved
-      ? await approveAlarmToWorkOrder({
-          alarmId: currentAlarm.value.id,
-          orderLevel: auditForm.value.result === '降级处理' ? 2 : currentAlarm.value.warningLevel,
-          orderDesc: auditForm.value.opinion || `${currentAlarm.value.warningType || '监测'}异常,请现场核实处理`
-        })
-      : await handleAlarm(currentAlarm.value.id, userStore.name || 'admin', auditForm.value.opinion || auditForm.value.result)
-    if (res.code === 200 || res.code === 0) {
-      ElMessage.success(res.msg || (isApproved ? '审核通过,已自动上传至工单' : '审核提交成功'))
-      auditVisible.value = false
-      loadData()
-    } else {
-      ElMessage.error(res.msg || '审核提交失败')
-    }
-  } catch (error) {
-    console.error('审核提交失败', error)
-    ElMessage.error('审核提交失败')
-  } finally {
-    submitting.value = false
-  }
-}
-
-// ==================== 查看详情弹窗 ====================
-const viewVisible = ref(false)
-const viewRow = ref({})
-async function openView(row) {
-  viewRow.value = { ...row }
-  viewVisible.value = true
-}
-
-// ==================== 地图初始化 ====================
-function initMap() {
-  const container = document.getElementById('bjshMap')
-  if (!container || typeof BMapGL === 'undefined') {
-    console.warn('BMapGL 未加载或容器不存在')
-    return
-  }
-  try {
-    mapInstance = new BMapGL.Map('bjshMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    mapInstance.centerAndZoom(centerPoint, 15)
-    mapInstance.enableScrollWheelZoom(true)
-
-    refreshMapMarkers()
-    mapInitialized = true
-  } catch (e) {
-    console.error('地图初始化失败:', e)
-  }
-}
-
-function refreshMapMarkers() {
-  if (!mapInstance) return
-  mapInstance.clearOverlays()
-
-  let firstPoint = null
-  tableData.value.forEach(point => {
-    const lng = parseFloat(point.longitude)
-    const lat = parseFloat(point.latitude)
-    if (!lng || !lat) return
-    if (!firstPoint) firstPoint = { lng, lat }
-    addMapMarker(point, lng, lat)
-  })
-
-  // 默认定位到第一个报警点位
-  if (firstPoint) {
-    mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
-  }
-}
-
-function addMapMarker(point, lng, lat) {
-  const color = levelColorMap[point.warningLevel] || '#409eff'
-  const levelTextStr = levelMap[point.warningLevel] || '一般'
-  const statusTextStr = auditStatusMap[point.status] || '待审核'
-  const statusColor = auditStatusColorMap[point.status] || '#e6a23c'
-  const bPoint = new BMapGL.Point(lng, lat)
-
-  const html = `<div style="text-align:center;cursor:pointer;">
-    <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.equipmentName || '-'}</div>
-    <div style="color:#fff;font-size:10px;background:rgba(0,0,0,0.6);border-radius:3px;padding:1px 4px;margin-bottom:2px;">${levelTextStr} | ${point.warningType || '-'}:${point.currentValue || '-'}</div>
-    <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
-  </div>`
-
-  const label = new BMapGL.Label(html, {
-    position: bPoint,
-    offset: new BMapGL.Size(-30, -55)
-  })
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0'
-  })
-  mapInstance.addOverlay(label)
-
-  // 点击标记弹出信息窗口
-  label.addEventListener('click', function () {
-    const infoContent = `<div style="font-size:13px;line-height:1.8;">
-      <div><b>${point.equipmentName || '-'}</b></div>
-      <div>报警等级:<b style="color:${color}">${levelTextStr}</b></div>
-      <div>报警时间:${point.warningTime || '-'}</div>
-      <div>监测值:<b style="color:${color}">${point.warningType || '-'}:${point.currentValue || '-'}</b></div>
-      <div>阈值:${point.thresholdValue || '-'}</div>
-      <div>审核状态:<b style="color:${statusColor}">${statusTextStr}</b></div>
-    </div>`
-    const infoWindow = new BMapGL.InfoWindow(infoContent, {
-      width: 220,
-      title: ''
-    })
-    mapInstance.openInfoWindow(infoWindow, bPoint)
-  })
-}
-
-// ==================== 生命周期 ====================
-onMounted(async () => {
-  await loadData()
-  nextTick(() => {
-    initMap()
-  })
-})
-</script>
-
-<style scoped>
-.bjsh-page {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.map-section {
-  width: 100%;
-}
-.map-container {
-  width: 100%;
-  height: 350px;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-}
-
-.table-section {
-  width: 100%;
-}
-.table-section .search-form {
-  margin-bottom: 12px;
-}
-
-/* 弹窗内部样式 */
-.section-title {
-  font-size: 15px;
-  font-weight: bold;
-  margin-bottom: 0;
-}
-.upload-tip {
-  font-size: 12px;
-  color: #909399;
-  margin-top: 4px;
-}
-.audit-form-section {
-  margin-top: 12px;
-  padding: 12px;
-  background: #f8f9fb;
-  border-radius: 6px;
-}
-.audit-timeline-section {
-  margin-top: 16px;
-}
-.timeline-node {
-  font-weight: 600;
-  font-size: 14px;
-  color: #303133;
-}
-.timeline-desc {
-  font-size: 12px;
-  color: #606266;
-  margin-top: 2px;
-}
-</style>

+ 160 - 5
src/views/subSystem/drainage/jcsj/gwyxqk.vue

@@ -89,6 +89,11 @@
           <span class="search-icon">🔍</span>
         </div>
         <div id="gwyxqkMap" class="map-container"></div>
+        <div class="pipeline-map-status">
+          <span class="pipeline-status-dot"></span>
+          排水管线 {{ gisPipelines.length }} 段
+        </div>
+        <div v-if="pipelineMapError" class="pipeline-map-error">{{ pipelineMapError }}</div>
       </div>
 
       <!-- ========== 右侧面板 ========== -->
@@ -175,6 +180,8 @@ import { ref, reactive, onMounted, onUnmounted, watch } from 'vue'
 import * as echarts from 'echarts'
 import locationIcon from '@/assets/images/location.png'
 import { getPipeNetworkPage, getEquipmentPage, getAlarmDataPage, getRadarDataByCode, getRadarDataHistory } from '@/api/drainage'
+import { getGisPipeLines } from '@/api/pipeNetwork/basic'
+import { convertCoord } from '@/utils/coordTransform'
 
 // ==================== 实时时间 ====================
 const currentDateTime = ref('')
@@ -228,6 +235,8 @@ const alarmList = ref([])
 // ==================== 地图数据(从API加载)====================
 const mapPoints = ref([])
 const pipeNetworkData = ref([])
+const gisPipelines = ref([])
+const pipelineMapError = ref('')
 
 // ==================== 设备状态映射 ====================
 function getEquipStatusInfo(equipment) {
@@ -251,6 +260,7 @@ function extractPage(res) {
 // ==================== 数据加载 ====================
 async function loadMapData() {
   try {
+    await loadGisPipelineData()
     // 1. 获取所有管网
     const pnRes = await getPipeNetworkPage(1, 999, {})
     const pipeNetworks = extractPage(pnRes)
@@ -381,6 +391,37 @@ async function loadMapData() {
   }
 }
 
+async function loadGisPipelineData() {
+  pipelineMapError.value = ''
+  try {
+    const response = await getGisPipeLines({ networkType: '1' })
+    const features = Array.isArray(response?.data?.features) ? response.data.features : []
+    gisPipelines.value = features.map(feature => {
+      const properties = feature?.properties || {}
+      const coordinates = feature?.geometry?.type === 'LineString' ? feature.geometry.coordinates : []
+      const points = coordinates
+        .map(item => ({ lng: Number(item?.[0]), lat: Number(item?.[1]) }))
+        .filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
+      return {
+        id: properties.networkId,
+        code: properties.networkCode,
+        name: properties.networkName || properties.networkCode || '排水管网',
+        level: properties.networkLevel,
+        diameter: properties.diameter,
+        length: properties.length,
+        material: properties.material,
+        location: properties.location,
+        points
+      }
+    }).filter(line => line.points.length >= 2)
+    if (!gisPipelines.value.length) pipelineMapError.value = '暂无带完整起止坐标的排水管网数据'
+  } catch (error) {
+    gisPipelines.value = []
+    pipelineMapError.value = '排水管网数据加载失败,请检查后端服务。'
+    console.error('[gwyxqk] 排水管网 GIS 线路加载失败:', error)
+  }
+}
+
 // ==================== 实时数据加载(/latest 返回单条 RadarData 对象)====================
 async function loadRealtimeData(equipmentList, pipeNetworks) {
   console.log('[gwyxqk] loadRealtimeData 收到设备:', equipmentList.length, '条', equipmentList.map(e => e.equipmentCode))
@@ -468,6 +509,7 @@ function loadAlarmData(alarmRecords, equipmentList) {
 
 // ==================== 地图初始化 ====================
 let mapInstance = null
+let mapInitTimer = null
 
 function initMap() {
   const container = document.getElementById('gwyxqkMap')
@@ -485,23 +527,76 @@ function initMap() {
       centerLng = mapPoints.value[0].lng
       centerLat = mapPoints.value[0].lat
     }
-    const centerPoint = new BMapGL.Point(centerLng, centerLat)
+    const [mapCenterLng, mapCenterLat] = convertCoord(centerLng, centerLat)
+    const centerPoint = new BMapGL.Point(mapCenterLng, mapCenterLat)
     mapInstance.centerAndZoom(centerPoint, 14)
     mapInstance.enableScrollWheelZoom(true)
 
-    // 地图只显示实时数据中的设备点位,自动定位到第一个设备
-    console.log('[gwyxqk] 初始化地图,设备点数量:', mapPoints.value.length)
+    const viewportPoints = drawGisPipelines()
+    // 叠加实时数据中的设备点位,并让视野覆盖管线和设备。
+    console.log('[gwyxqk] 初始化地图,排水管线数量:', gisPipelines.value.length, '设备点数量:', mapPoints.value.length)
     mapPoints.value.forEach(point => {
       addMapMarker(point)
+      const [lng, lat] = convertCoord(point.lng, point.lat)
+      viewportPoints.push(new BMapGL.Point(lng, lat))
     })
+    if (viewportPoints.length > 0) mapInstance.setViewport(viewportPoints)
   } catch (e) {
     console.error('百度地图初始化失败:', e)
   }
 }
 
+function getGisPipelineColor(level) {
+  const text = String(level || '').toLowerCase()
+  if (text.includes('主') || text.includes('一级') || text.includes('main')) return '#00e5ff'
+  if (text.includes('支') || text.includes('二级') || text.includes('branch')) return '#409eff'
+  return '#67c23a'
+}
+
+function escapeMapText(value) {
+  return String(value ?? '')
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;')
+}
+
+function drawGisPipelines() {
+  const viewportPoints = []
+  gisPipelines.value.forEach(line => {
+    const bmapPoints = line.points.map(point => {
+      const [lng, lat] = convertCoord(point.lng, point.lat)
+      return new BMapGL.Point(lng, lat)
+    })
+    const polyline = new BMapGL.Polyline(bmapPoints, {
+      strokeColor: getGisPipelineColor(line.level),
+      strokeWeight: 5,
+      strokeOpacity: 0.9
+    })
+    polyline.addEventListener('click', event => {
+      event?.domEvent?.stopPropagation?.()
+      const content = `<div style="min-width:220px;color:#303133;line-height:1.8">
+        <div style="font-size:15px;font-weight:700;color:#1677ff;margin-bottom:6px">${escapeMapText(line.name)}</div>
+        <div>设施编号:${escapeMapText(line.code || '-')}</div>
+        <div>管网等级:${escapeMapText(line.level || '-')}</div>
+        <div>管径:${escapeMapText(line.diameter ?? '-')} mm</div>
+        <div>长度:${escapeMapText(line.length ?? '-')} m</div>
+        <div>材质:${escapeMapText(line.material || '-')}</div>
+        <div>位置:${escapeMapText(line.location || '-')}</div>
+      </div>`
+      mapInstance.openInfoWindow(new BMapGL.InfoWindow(content, { width: 280, enableMessage: false }), bmapPoints[0])
+    })
+    mapInstance.addOverlay(polyline)
+    viewportPoints.push(...bmapPoints)
+  })
+  return viewportPoints
+}
+
 function addMapMarker(point) {
   const color = point.color || '#409eff'
-  const bPoint = new BMapGL.Point(point.lng, point.lat)
+  const [lng, lat] = convertCoord(point.lng, point.lat)
+  const bPoint = new BMapGL.Point(lng, lat)
 
   const html = `<div style="text-align:center;cursor:pointer;">
     <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.name}</div>
@@ -899,7 +994,7 @@ onMounted(async () => {
 
   // 延迟初始化确保 DOM 渲染完成
   setTimeout(() => {
-    initMap()
+    waitForBMapAndInit()
     initPieChart()
     // 加载默认设备的曲线历史数据
     if (selectedCurveDevice.value) {
@@ -913,8 +1008,26 @@ onMounted(async () => {
   }, 300)
 })
 
+function waitForBMapAndInit() {
+  let attempts = 0
+  const tryInit = () => {
+    attempts += 1
+    if (typeof BMapGL !== 'undefined') {
+      initMap()
+      return
+    }
+    if (attempts >= 100) {
+      pipelineMapError.value = '百度地图加载超时,请检查网络或地图配置。'
+      return
+    }
+    mapInitTimer = window.setTimeout(tryInit, 200)
+  }
+  tryInit()
+}
+
 onUnmounted(() => {
   if (timer) clearInterval(timer)
+  if (mapInitTimer) clearTimeout(mapInitTimer)
   if (pieChart) pieChart.dispose()
   if (lineChart) lineChart.dispose()
   if (barChart) barChart.dispose()
@@ -1233,6 +1346,48 @@ window.addEventListener('resize', () => {
   align-items: center;
 }
 
+.pipeline-map-status {
+  position: absolute;
+  top: 48px;
+  left: 12px;
+  z-index: 5;
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  padding: 7px 12px;
+  color: #d9f4ff;
+  background: rgba(6, 30, 65, 0.86);
+  border: 1px solid rgba(0, 229, 255, 0.42);
+  border-radius: 4px;
+  font-size: 12px;
+  pointer-events: none;
+}
+
+.pipeline-status-dot {
+  width: 9px;
+  height: 9px;
+  background: #00e5ff;
+  border-radius: 50%;
+  box-shadow: 0 0 8px rgba(0, 229, 255, 0.9);
+}
+
+.pipeline-map-error {
+  position: absolute;
+  left: 50%;
+  bottom: 18px;
+  z-index: 5;
+  max-width: 70%;
+  padding: 8px 14px;
+  color: #ffd591;
+  background: rgba(80, 45, 0, 0.88);
+  border: 1px solid rgba(230, 162, 60, 0.55);
+  border-radius: 4px;
+  font-size: 12px;
+  text-align: center;
+  transform: translateX(-50%);
+  pointer-events: none;
+}
+
 .search-input {
   background: rgba(6, 30, 65, 0.85);
   border: 1px solid rgba(64, 158, 255, 0.3);

+ 0 - 721
src/views/subSystem/drainage/jcsj/sbjcgl.vue

@@ -1,721 +0,0 @@
-<template>
-  <div class="app-container">
-    <!-- 搜索区域 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="设备名称">
-        <el-input v-model="queryParams.equipmentName" placeholder="请输入" clearable style="width: 200px" />
-      </el-form-item>
-      <el-form-item label="设备状态">
-        <el-select v-model="queryParams.currentStatus" placeholder="请选择" clearable style="width: 200px">
-          <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" @click="handleQuery">搜索</el-button>
-      </el-form-item>
-    </el-form>
-
-    <!-- 操作按钮区域 -->
-    <div class="toolbar">
-      <el-button type="primary" @click="handleAdd">新增</el-button>
-      <el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">删除</el-button>
-    </div>
-
-    <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="tableData" border style="width: 100%" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="设备名称" prop="equipmentName" min-width="140" />
-      <el-table-column label="设备编号" prop="equipmentCode" min-width="130" />
-      <el-table-column label="设备类型" prop="equipmentTypeName" min-width="90">
-        <template #default="{ row }">
-          <span>{{ row.equipmentTypeName || '-' }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="安装位置" prop="equipmentLocation" min-width="150" />
-      <el-table-column label="设备状态" prop="currentStatus" min-width="90" align="center">
-        <template #default="{ row }">
-          <el-tag :type="statusTagType(row.currentStatus)">{{ statusText(row.currentStatus) }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="报警状态" prop="alarmStatus" min-width="90" align="center">
-        <template #default="{ row }">
-          <el-tag :type="row.alarmStatus === 1 ? 'danger' : 'success'">{{ row.alarmStatus === 1 ? '报警' : '正常' }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="在线状态" prop="onlineStatus" min-width="90" align="center">
-        <template #default="{ row }">
-          <el-tag :type="row.onlineStatus === 1 ? 'success' : 'info'">{{ row.onlineStatus === 1 ? '在线' : '离线' }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="安装时间" prop="installTime" min-width="120" />
-      <el-table-column label="最近维护时间" prop="lastMaintainDate" min-width="130" />
-      <el-table-column label="下次维护日期" prop="nextMaintainDate" min-width="130" />
-      <el-table-column label="操作" width="180" align="center" fixed="right">
-        <template #default="{ row }">
-          <el-button link type="primary" @click="handleView(row)">查看</el-button>
-          <el-button link type="primary" @click="handleEdit(row)">修改</el-button>
-          <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 分页 -->
-    <el-pagination
-      v-model:current-page="queryParams.pageNum"
-      v-model:page-size="queryParams.pageSize"
-      :page-sizes="[10, 20, 50, 100]"
-      :total="total"
-      layout="total, sizes, prev, pager, next, jumper"
-      @size-change="handleQuery"
-      @current-change="handlePageChange"
-    />
-
-    <!-- 查看详情对话框 -->
-    <el-drawer v-model="dialogVisible" title="设备详情" size="700px" :before-close="handleClose" class="detail-drawer" direction="rtl">
-      <div class="detail-info">
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">设备名称:</span>
-              <span class="info-value">{{ currentRow.equipmentName }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">设备编号:</span>
-              <span class="info-value">{{ currentRow.equipmentCode }}</span>
-            </div>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">设备类型:</span>
-              <span class="info-value">{{ currentRow.equipmentTypeName || '-' }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">权属单位:</span>
-              <span class="info-value">{{ currentRow.ownershipUnit || '-' }}</span>
-            </div>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">安装位置:</span>
-              <span class="info-value">{{ currentRow.equipmentLocation }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">设备状态:</span>
-              <span class="info-value">
-                <el-tag :type="statusTagType(currentRow.currentStatus)">{{ statusText(currentRow.currentStatus) }}</el-tag>
-              </span>
-            </div>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">安装时间:</span>
-              <span class="info-value">{{ currentRow.installTime }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">最近维护时间:</span>
-              <span class="info-value">{{ currentRow.lastMaintainDate }}</span>
-            </div>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <div class="info-item">
-              <span class="info-label">备注:</span>
-              <span class="info-value">{{ currentRow.remark || '-' }}</span>
-            </div>
-          </el-col>
-        </el-row>
-      </div>
-    </el-drawer>
-
-    <!-- 新增/修改对话框 -->
-    <el-dialog v-model="editDialogVisible" :title="editDialogTitle" width="800px" :before-close="handleEditClose">
-      <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="120px">
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="设备名称" prop="equipmentName">
-              <el-input v-model="editForm.equipmentName" placeholder="请输入设备名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备编号" prop="equipmentCode">
-              <el-input v-model="editForm.equipmentCode" placeholder="请输入设备编号" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="设备型号" prop="equipmentModel">
-              <el-input v-model="editForm.equipmentModel" placeholder="请输入设备型号" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备类型" prop="equipmentTypeId">
-              <el-select v-model="editForm.equipmentTypeId" placeholder="请选择设备类型" style="width: 100%">
-                <el-option v-for="item in drainageTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="设备制造商" prop="manufacturer">
-              <el-input v-model="editForm.manufacturer" placeholder="请输入设备制造商" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="生产日期" prop="productionDate">
-              <el-date-picker v-model="editForm.productionDate" type="date" placeholder="请选择生产日期" value-format="YYYY-MM-DD" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <el-form-item label="设备规格参数" prop="equipmentSpec">
-              <el-input v-model="editForm.equipmentSpec" type="textarea" :rows="2" placeholder="请输入设备规格参数" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="资产价值(元)" prop="assetValue">
-              <el-input-number v-model="editForm.assetValue" :precision="2" :min="0" placeholder="请输入" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="使用年限(年)" prop="useLife">
-              <el-input-number v-model="editForm.useLife" :min="0" :precision="0" placeholder="请输入" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="经度" prop="longitude">
-              <el-input-number v-model="editForm.longitude" :precision="8" :min="-180" :max="180" :controls="false" placeholder="请输入经度" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="纬度" prop="latitude">
-              <el-input-number v-model="editForm.latitude" :precision="8" :min="-90" :max="90" :controls="false" placeholder="请输入纬度" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="安装位置" prop="equipmentLocation">
-              <el-input v-model="editForm.equipmentLocation" placeholder="请输入安装位置" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="安装时间" prop="installTime">
-              <el-date-picker v-model="editForm.installTime" type="date" placeholder="请选择安装时间" value-format="YYYY-MM-DD" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="维护人员" prop="maintainer">
-              <el-input v-model="editForm.maintainer" placeholder="请输入维护人员" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="联系方式" prop="maintainerPhone">
-              <el-input v-model="editForm.maintainerPhone" placeholder="请输入联系方式" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="权属单位" prop="ownershipUnit">
-              <el-input v-model="editForm.ownershipUnit" placeholder="请输入权属单位" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备状态" prop="status">
-              <el-select v-model="editForm.status" placeholder="请选择设备状态" style="width: 100%">
-                <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="报警状态" prop="alarmStatus">
-              <el-select v-model="editForm.alarmStatus" placeholder="请选择报警状态" style="width: 100%">
-                <el-option label="正常" :value="0" />
-                <el-option label="报警" :value="1" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="在线状态" prop="onlineStatus">
-              <el-select v-model="editForm.onlineStatus" placeholder="请选择在线状态" style="width: 100%">
-                <el-option label="离线" :value="0" />
-                <el-option label="在线" :value="1" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="最近维护时间" prop="lastMaintenanceTime">
-              <el-date-picker v-model="editForm.lastMaintenanceTime" type="date" placeholder="请选择维护时间" value-format="YYYY-MM-DD" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="下次维护日期" prop="nextMaintenanceTime">
-              <el-date-picker v-model="editForm.nextMaintenanceTime" type="date" placeholder="请选择下次维护日期" value-format="YYYY-MM-DD" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <el-form-item label="备注" prop="remark">
-              <el-input v-model="editForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <template #footer>
-        <el-button @click="handleEditClose">取 消</el-button>
-        <el-button type="primary" @click="handleEditSubmit">确 定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="Sbjcgl">
-import { ref, computed, onMounted } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import {
-  getEquipmentPage,
-  getEquipmentById,
-  addEquipment,
-  updateEquipment,
-  deleteEquipment,
-  batchDeleteEquipment,
-  getEquipmentTypeList,
-  addEquipmentStatus,
-  updateEquipmentStatus
-} from '@/api/drainage'
-
-// ==================== 选项配置 ====================
-const typeList = ref([])
-const typeOptions = computed(() => {
-  return typeList.value.map(item => ({ label: item.typeName, value: item.typeId }))
-})
-
-const drainageTypeOptions = computed(() => {
-  // 找到排水父类记录(typeId为'drainage'且parentTypeId为'0')
-  const drainageParent = typeList.value.find(item => item.typeId === 'drainage' && item.parentTypeId === '0')
-  if (!drainageParent) {
-    // 如果找不到父类,返回所有非顶级的类型
-    return typeList.value.filter(item => item.parentTypeId !== '0' && item.parentTypeId !== null).map(item => ({ label: item.typeName, value: item.id }))
-  }
-  // 返回parentTypeId等于drainage父类id的子类型
-  return typeList.value.filter(item => String(item.parentTypeId) === String(drainageParent.id)).map(item => ({ label: item.typeName, value: item.id }))
-})
-
-const statusOptions = [
-  { label: '在用', value: 1 },
-  { label: '闲置', value: 2 },
-  { label: '维修', value: 3 },
-  { label: '报废', value: 4 },
-  { label: '待入库', value: 5 }
-]
-
-function typeText(val) {
-  const item = typeList.value.find(t => t.typeId === val)
-  return item ? item.typeName : '-'
-}
-
-function statusText(val) {
-  const item = statusOptions.find(s => s.value === val)
-  return item ? item.label : '-'
-}
-
-function statusTagType(val) {
-  const map = { 1: 'success', 2: 'info', 3: 'warning', 4: 'danger', 5: '' }
-  return map[val] || 'info'
-}
-
-// ==================== 搜索参数 ====================
-const queryParams = ref({
-  equipmentName: '',
-  equipmentTypeId: '',
-  currentStatus: '',
-  pageNum: 1,
-  pageSize: 10
-})
-
-// ==================== 表格数据 ====================
-const tableData = ref([])
-const total = ref(0)
-const loading = ref(false)
-
-// 多选
-const selectedIds = ref([])
-function handleSelectionChange(selection) {
-  selectedIds.value = selection.map(item => item.equipmentId)
-}
-
-// 加载分页数据
-async function loadData() {
-  loading.value = true
-  try {
-    const { pageNum, pageSize, ...rest } = queryParams.value
-    const res = await getEquipmentPage(pageNum, pageSize, rest)
-    const pageData = res.code !== undefined ? res.data : res
-    tableData.value = pageData.records || []
-    total.value = pageData.total || 0
-  } catch (error) {
-    console.error('加载设备列表失败', error)
-    ElMessage.error('加载设备列表失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-// 搜索
-function handleQuery() {
-  queryParams.value.pageNum = 1
-  loadData()
-}
-
-function handlePageChange() {
-  loadData()
-}
-
-onMounted(async () => {
-  await loadTypeList()
-  loadData()
-})
-
-async function loadTypeList() {
-  try {
-    const res = await getEquipmentTypeList()
-    typeList.value = (res.code !== undefined ? res.data : res) || []
-    // 分页接口要求传 equipment_type.id,不能传业务编码 drainage。
-    const drainageParent = typeList.value.find(item =>
-      item.typeId === 'drainage' && String(item.parentTypeId) === '0'
-    )
-    queryParams.value.equipmentTypeId = drainageParent ? String(drainageParent.id) : ''
-  } catch (error) {
-    console.error('加载设备类型失败', error)
-    ElMessage.error('加载设备类型失败')
-  }
-}
-
-// ==================== 查看详情对话框 ====================
-const dialogVisible = ref(false)
-const currentRow = ref({})
-
-async function handleView(row) {
-  try {
-    const res = await getEquipmentById(row.equipmentId)
-    const detail = res.data || {}
-    currentRow.value = {
-      ...row,
-      ...detail.equipmentInfo,
-      ...detail.statusInfo,
-      equipmentTypeName: detail.typeInfo ? detail.typeInfo.typeName : (row.equipmentTypeName || '-')
-    }
-    dialogVisible.value = true
-  } catch (error) {
-    console.error('获取设备详情失败', error)
-    ElMessage.error('获取设备详情失败')
-  }
-}
-
-function handleClose() {
-  dialogVisible.value = false
-}
-
-// ==================== 新增/修改对话框 ====================
-const editDialogVisible = ref(false)
-const editDialogTitle = ref('新增设备')
-const editFormRef = ref(null)
-const isEditMode = ref(false)
-const editRowId = ref(null)
-
-const editForm = ref({
-  equipmentName: '',
-  equipmentCode: '',
-  equipmentModel: '',
-  equipmentSpec: '',
-  manufacturer: '',
-  productionDate: '',
-  equipmentTypeId: '',
-  assetValue: null,
-  useLife: null,
-  longitude: null,
-  latitude: null,
-  equipmentLocation: '',
-  installTime: '',
-  maintainer: '',
-  maintainerPhone: '',
-  ownershipUnit: '',
-  remark: '',
-  // 状态表字段
-  status: '',
-  alarmStatus: null,
-  onlineStatus: null,
-  lastMaintenanceTime: '',
-  nextMaintenanceTime: ''
-})
-
-const editRules = {
-  equipmentName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
-  equipmentCode: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
-  equipmentTypeId: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
-  equipmentLocation: [{ required: true, message: '请输入安装位置', trigger: 'blur' }],
-  status: [{ required: true, message: '请选择设备状态', trigger: 'change' }]
-}
-
-function handleAdd() {
-  isEditMode.value = false
-  editDialogTitle.value = '新增设备'
-  editRowId.value = null
-  editForm.value = {
-    equipmentName: '',
-    equipmentCode: '',
-    equipmentModel: '',
-    equipmentSpec: '',
-    manufacturer: '',
-    productionDate: '',
-    equipmentTypeId: '',
-    assetValue: null,
-    useLife: null,
-    longitude: null,
-    latitude: null,
-    equipmentLocation: '',
-    installTime: '',
-    maintainer: '',
-    maintainerPhone: '',
-    ownershipUnit: '',
-    remark: '',
-    status: '',
-    alarmStatus: null,
-    onlineStatus: null,
-    lastMaintenanceTime: '',
-    nextMaintenanceTime: ''
-  }
-  editDialogVisible.value = true
-}
-
-function handleEdit(row) {
-  isEditMode.value = true
-  editDialogTitle.value = '修改设备'
-  editRowId.value = row.equipmentId
-  editForm.value = {
-    equipmentName: row.equipmentName || '',
-    equipmentCode: row.equipmentCode || '',
-    equipmentModel: row.equipmentModel || '',
-    equipmentSpec: row.equipmentSpec || '',
-    manufacturer: row.manufacturer || '',
-    productionDate: row.productionDate || '',
-    equipmentTypeId: row.equipmentTypeId || '',
-    assetValue: row.assetValue || null,
-    useLife: row.useLife || null,
-    longitude: row.longitude || null,
-    latitude: row.latitude || null,
-    equipmentLocation: row.equipmentLocation || '',
-    installTime: row.installTime ? row.installTime.split(' ')[0] : '',
-    maintainer: row.maintainer || '',
-    maintainerPhone: row.maintainerPhone || '',
-    ownershipUnit: row.ownershipUnit || '',
-    remark: row.remark || '',
-    status: row.currentStatus || '',
-    alarmStatus: row.alarmStatus != null ? row.alarmStatus : null,
-    onlineStatus: row.onlineStatus != null ? row.onlineStatus : null,
-    lastMaintenanceTime: row.lastMaintainDate || '',
-    nextMaintenanceTime: row.nextMaintainDate || ''
-  }
-  editDialogVisible.value = true
-}
-
-function handleEditClose() {
-  editDialogVisible.value = false
-}
-
-function handleEditSubmit() {
-  editFormRef.value.validate(async valid => {
-    if (!valid) return
-
-    const formData = {
-      equipmentName: editForm.value.equipmentName,
-      equipmentCode: editForm.value.equipmentCode,
-      equipmentModel: editForm.value.equipmentModel,
-      equipmentSpec: editForm.value.equipmentSpec,
-      manufacturer: editForm.value.manufacturer,
-      productionDate: editForm.value.productionDate || null,
-      equipmentTypeId: editForm.value.equipmentTypeId,
-      equipmentType: 'drainage',
-      assetValue: editForm.value.assetValue,
-      useLife: editForm.value.useLife,
-      longitude: editForm.value.longitude,
-      latitude: editForm.value.latitude,
-      equipmentLocation: editForm.value.equipmentLocation,
-      installTime: editForm.value.installTime ? editForm.value.installTime + ' 00:00:00' : null,
-      maintainer: editForm.value.maintainer,
-      maintainerPhone: editForm.value.maintainerPhone,
-      ownershipUnit: editForm.value.ownershipUnit,
-      remark: editForm.value.remark
-    }
-
-    try {
-      if (isEditMode.value) {
-        formData.equipmentId = editRowId.value
-        const res = await updateEquipment(formData)
-        if (res.code === 0 || res.code === 200) {
-          // 更新设备状态
-          if (editForm.value.status) {
-            try {
-              await updateEquipmentStatus({
-                equipmentId: editRowId.value,
-                currentStatus: editForm.value.status,
-                alarmStatus: editForm.value.alarmStatus,
-                onlineStatus: editForm.value.onlineStatus,
-                installationAddr: editForm.value.equipmentLocation,
-                lastMaintainDate: editForm.value.lastMaintenanceTime || null,
-                nextMaintainDate: editForm.value.nextMaintenanceTime || null
-              })
-            } catch (e) {
-              console.warn('更新设备状态失败', e)
-            }
-          }
-          ElMessage.success('修改成功')
-        } else {
-          ElMessage.error(res.msg || '修改失败')
-          return
-        }
-      } else {
-        const res = await addEquipment(formData)
-        if (res.code === 0 || res.code === 200) {
-          // 新增成功后,创建设备状态记录
-          const newEquipmentId = res.data ? (res.data.equipmentId || null) : null
-          if (newEquipmentId && editForm.value.status) {
-            try {
-              await addEquipmentStatus({
-                equipmentId: newEquipmentId,
-                currentStatus: editForm.value.status,
-                alarmStatus: editForm.value.alarmStatus,
-                onlineStatus: editForm.value.onlineStatus,
-                installationAddr: editForm.value.equipmentLocation,
-                lastMaintainDate: editForm.value.lastMaintenanceTime || null,
-                nextMaintainDate: editForm.value.nextMaintenanceTime || null
-              })
-            } catch (e) {
-              console.warn('创建设备状态失败', e)
-            }
-          }
-          ElMessage.success('新增成功')
-          // 新增记录按创建时间倒序显示,回到第一页后才能立即看到。
-          queryParams.value.pageNum = 1
-        } else {
-          ElMessage.error(res.msg || '新增失败')
-          return
-        }
-      }
-      editDialogVisible.value = false
-      loadData()
-    } catch (error) {
-      console.error('提交失败', error)
-      ElMessage.error('操作失败')
-    }
-  })
-}
-
-// ==================== 删除功能 ====================
-function handleDelete(row) {
-  ElMessageBox.confirm(`确认删除设备"${row.equipmentName}"吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(async () => {
-    try {
-      const res = await deleteEquipment(row.equipmentId)
-      if (res.code === 0 || res.code === 200) {
-        ElMessage.success('删除成功')
-        loadData()
-      } else {
-        ElMessage.error(res.msg || '删除失败')
-      }
-    } catch (error) {
-      ElMessage.error('删除失败')
-    }
-  }).catch(() => { })
-}
-
-function handleBatchDelete() {
-  if (selectedIds.value.length === 0) return
-  ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条数据吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(async () => {
-    try {
-      const res = await batchDeleteEquipment(selectedIds.value)
-      if (res.code === 0 || res.code === 200) {
-        selectedIds.value = []
-        ElMessage.success('删除成功')
-        loadData()
-      } else {
-        ElMessage.error(res.msg || '删除失败')
-      }
-    } catch (error) {
-      ElMessage.error('删除失败')
-    }
-  }).catch(() => { })
-}
-</script>
-
-<style scoped>
-.app-container {
-  padding: 20px;
-}
-
-.search-form {
-  margin-bottom: 16px;
-}
-
-.toolbar {
-  margin-bottom: 16px;
-}
-
-.detail-info {
-  margin-bottom: 16px;
-}
-
-.info-item {
-  display: flex;
-  align-items: center;
-  margin-bottom: 12px;
-  line-height: 32px;
-}
-
-.info-label {
-  color: #909399;
-  font-size: 14px;
-  white-space: nowrap;
-}
-
-.info-value {
-  color: #303133;
-  font-size: 14px;
-}
-</style>

+ 0 - 311
src/views/subSystem/drainage/jcyj/DrainageThreshold.vue

@@ -1,311 +0,0 @@
-<template>
-  <div class="app-container">
-    <!-- 搜索区 -->
-    <el-card class="search-card">
-      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-        <el-form-item label="设备编码">
-          <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width: 180px" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleQuery">查询</el-button>
-          <el-button @click="resetQuery">重置</el-button>
-          <el-button type="success" @click="handleAdd">按设备类型批量修改阈值</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <!-- 表格 -->
-    <el-card class="table-card">
-      <el-table :data="tableData" v-loading="loading" border style="width: 100%">
-        <el-table-column label="设备编码" prop="deviceCode" min-width="140" show-overflow-tooltip />
-        <el-table-column label="设备名称" min-width="140" show-overflow-tooltip>
-          <template #default="{ row }">
-            {{ row.equipmentName || getDeviceName(row.deviceCode) }}
-          </template>
-        </el-table-column>
-        <el-table-column label="设备类型" min-width="120" show-overflow-tooltip>
-          <template #default="{ row }">
-            {{ row.equipmentTypeName || getDeviceType(row.deviceCode) }}
-          </template>
-        </el-table-column>
-        <el-table-column label="最小值" prop="minValue" width="90" align="center" />
-        <el-table-column label="最大值" prop="maxValue" width="90" align="center" />
-        <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
-        <el-table-column label="创建时间" prop="createTime" width="170" />
-        <el-table-column label="操作" width="160" align="center" fixed="right">
-          <template #default="{ row }">
-            <el-button v-if="row.id" type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-            <span v-else class="muted">未配置</span>
-            <el-button v-if="row.id" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
-        layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
-        @size-change="handleSizeChange" @current-change="handlePageChange"
-        style="margin-top: 16px; justify-content: flex-end"
-      />
-    </el-card>
-
-    <!-- 新增/编辑对话框 -->
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
-      <el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="设备类型" prop="deviceType">
-          <el-select v-model="formData.deviceType" placeholder="请选择设备类型" filterable clearable style="width: 100%" @change="onDeviceTypeChange">
-            <el-option v-for="type in deviceTypeOptions" :key="type" :label="type" :value="type" />
-          </el-select>
-        </el-form-item>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="最小值" prop="minValue">
-              <el-input-number v-model="formData.minValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最小值" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="最大值" prop="maxValue">
-              <el-input-number v-model="formData.maxValue" :precision="2" :min="0" style="width: 100%" placeholder="阈值最大值" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-form-item label="备注">
-          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注信息" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSave" :loading="submitLoading">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="DrainageThreshold">
-import { ref, reactive, onMounted } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import {
-  getThresholdModulePage, saveThreshold, updateThreshold, deleteThreshold
-} from '@/api/drainage'
-import { getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
-
-const DRAINAGE_MODULE_TYPE = '排水'
-const deviceTypeOptions = ref([])
-const filteredDeviceOptions = ref([])
-
-// ==================== 搜索参数 ====================
-const queryParams = reactive({
-  deviceCode: ''
-})
-const queryRef = ref(null)
-
-function handleQuery() {
-  pageNum.value = 1
-  loadData()
-}
-function resetQuery() {
-  queryParams.deviceCode = ''
-  pageNum.value = 1
-  loadData()
-}
-
-// ==================== 表格数据 ====================
-const tableData = ref([])
-const total = ref(0)
-const loading = ref(false)
-const pageNum = ref(1)
-const pageSize = ref(10)
-
-async function loadData() {
-  loading.value = true
-  try {
-    const params = {}
-    if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
-    const res = await getThresholdModulePage(pageNum.value, pageSize.value, DRAINAGE_MODULE_TYPE, params)
-    const pageData = res.data || res
-    tableData.value = pageData.records || []
-    total.value = pageData.total || 0
-  } catch (e) {
-    console.error('加载阈值列表失败', e)
-    ElMessage.error('加载阈值列表失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-function handlePageChange() { loadData() }
-function handleSizeChange() { pageNum.value = 1; loadData() }
-
-// ==================== 设备名称/类型映射 ====================
-const deviceInfoMap = ref({})
-
-async function ensureDeviceMap() {
-  if (Object.keys(deviceInfoMap.value).length > 0) return
-  try {
-    const res = await getEquipmentByTopLevelType(DRAINAGE_MODULE_TYPE)
-    const list = res.data || []
-    const map = {}
-    list.forEach(d => { map[d.equipmentCode] = d })
-    deviceInfoMap.value = map
-    deviceTypeOptions.value = [...new Set(list.map(d => d.equipmentTypeName).filter(Boolean))]
-    filteredDeviceOptions.value = list
-  } catch (e) { console.error('加载排水设备列表失败', e) }
-}
-
-function getDeviceName(deviceCode) {
-  return String(deviceCode || '').split(',').map(code => {
-    const d = deviceInfoMap.value[code.trim()]
-    return d ? (d.equipmentName || code.trim()) : code.trim()
-  }).filter(Boolean).join('、') || '-'
-}
-
-function getDeviceType(deviceCode) {
-  const types = String(deviceCode || '').split(',').map(code => deviceInfoMap.value[code.trim()])
-    .filter(Boolean).map(d => d.equipmentTypeName).filter(Boolean)
-  return [...new Set(types)].join('、') || '-'
-}
-
-// ==================== 新增/编辑 ====================
-const dialogVisible = ref(false)
-const dialogTitle = ref('按设备类型批量修改阈值')
-const formRef = ref(null)
-const submitLoading = ref(false)
-const isEdit = ref(false)
-
-const rules = {
-  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
-}
-
-const formData = reactive({
-  id: '',
-  deviceType: '',
-  deviceCodes: [],
-  deviceCode: '',
-  minValue: null,
-  maxValue: null,
-  remark: ''
-})
-
-const deviceOptions = ref([])
-
-async function loadDrainageDeviceOptions() {
-  if (deviceOptions.value.length > 0) return
-  try {
-    const res = await getEquipmentByTopLevelType(DRAINAGE_MODULE_TYPE)
-    deviceOptions.value = res.data || []
-    deviceTypeOptions.value = [...new Set(deviceOptions.value.map(d => d.equipmentTypeName).filter(Boolean))]
-    filteredDeviceOptions.value = deviceOptions.value
-  } catch (e) {
-    console.error('加载排水设备列表失败', e)
-  }
-}
-
-function resetForm() {
-  formData.id = ''
-  formData.deviceType = ''
-  formData.deviceCodes = []
-  formData.deviceCode = ''
-  formData.minValue = null
-  formData.maxValue = null
-  formData.remark = ''
-  isEdit.value = false
-  dialogTitle.value = '按设备类型批量修改阈值'
-}
-
-function onDeviceTypeChange(type) {
-  filteredDeviceOptions.value = deviceOptions.value.filter(d => d.equipmentTypeName === type)
-  formData.deviceCodes = filteredDeviceOptions.value.map(d => d.equipmentCode).filter(Boolean)
-}
-
-async function handleAdd() {
-  resetForm()
-  await loadDrainageDeviceOptions()
-  dialogVisible.value = true
-}
-
-async function handleEdit(row) {
-  await loadDrainageDeviceOptions()
-  isEdit.value = true
-  dialogTitle.value = '编辑阈值'
-  formData.id = row.id || ''
-  formData.deviceCode = row.deviceCode
-  formData.deviceCodes = String(row.deviceCode || '').split(',').map(v => v.trim()).filter(Boolean)
-  const firstDevice = deviceOptions.value.find(d => formData.deviceCodes.includes(d.equipmentCode))
-  formData.deviceType = firstDevice?.equipmentTypeName || ''
-  onDeviceTypeChange(formData.deviceType)
-  formData.minValue = row.minValue
-  formData.maxValue = row.maxValue
-  formData.remark = row.remark || ''
-  dialogVisible.value = true
-}
-
-async function handleSave() {
-  const valid = await formRef.value.validate().catch(() => false)
-  if (!valid) return
-  if (!formData.deviceCodes.length) {
-    ElMessage.warning('该设备类型下没有可配置的设备')
-    return
-  }
-  submitLoading.value = true
-  try {
-    const data = {
-      deviceCode: formData.deviceCodes.join(','),
-      minValue: formData.minValue,
-      maxValue: formData.maxValue,
-      remark: formData.remark || undefined
-    }
-    if (isEdit.value) {
-      data.id = formData.id
-      await updateThreshold(data, DRAINAGE_MODULE_TYPE)
-      ElMessage.success('设备类型阈值批量修改成功')
-    } else {
-      await saveThreshold(data, DRAINAGE_MODULE_TYPE)
-      ElMessage.success('设备类型阈值批量配置成功')
-    }
-    dialogVisible.value = false
-    loadData()
-    // 刷新设备映射
-    deviceInfoMap.value = {}
-    await ensureDeviceMap()
-  } catch (e) {
-    const msg = e?.response?.data?.msg || e?.message || '操作失败'
-    ElMessage.error(msg)
-  } finally {
-    submitLoading.value = false
-  }
-}
-
-// ==================== 删除 ====================
-function handleDelete(row) {
-  ElMessageBox.confirm('确认删除该阈值配置?', '提示', { type: 'warning' }).then(async () => {
-    try {
-      await deleteThreshold([row.id])
-      ElMessage.success('删除成功')
-      loadData()
-    } catch (e) {
-      ElMessage.error(e?.response?.data?.msg || '删除失败')
-    }
-  }).catch(() => {})
-}
-
-// ==================== 生命周期 ====================
-onMounted(async () => {
-  await ensureDeviceMap()
-  loadData()
-})
-</script>
-
-<style scoped>
-.app-container {
-  padding: 16px;
-}
-.search-card {
-  margin-bottom: 16px;
-}
-.search-form {
-  display: flex;
-  flex-wrap: wrap;
-}
-.table-card {
-  min-height: 400px;
-}
-</style>

+ 1 - 1
src/views/subSystem/drainage/jcyj/dqyjgl.vue

@@ -237,7 +237,7 @@ async function loadEquipmentTypes() {
     const map = new Map()
     if (Array.isArray(list)) {
       list.forEach(item => {
-        if (item.typeId) map.set(String(item.typeId), item.typeName)
+        if (item.id !== undefined && item.id !== null) map.set(String(item.id), item.typeName)
       })
     }
     equipmentTypeMap.value = map

+ 1 - 1
src/views/subSystem/drainage/jcyj/lsyjgl.vue

@@ -158,7 +158,7 @@ async function loadEquipmentTypes() {
     const map = new Map()
     if (Array.isArray(list)) {
       list.forEach(item => {
-        if (item.typeId) map.set(String(item.typeId), item.typeName)
+        if (item.id !== undefined && item.id !== null) map.set(String(item.id), item.typeName)
       })
     }
     equipmentTypeMap.value = map

+ 121 - 505
src/views/subSystem/gas/gasBasicadata/gasPipelintNet.vue

@@ -1,130 +1,75 @@
-<!--燃气管网基础设施gis一张图-->
+<!-- 燃气管网基础设施 GIS 一张图:线路数据来自 pipe_network_base 的燃气设施(network_type=0)。 -->
 <template>
   <div class="gas-gis-dashboard">
-    <!-- 页面头部 -->
     <div class="page-header">
       <div class="header-left">
         <el-icon><MapLocation /></el-icon>
         <span class="title">燃气管网GIS一张图</span>
         <div class="stats-badge">
-          <span class="stat-item"><i class="dot pipeline-dot"></i>管线 {{ pipelineCount }}km</span>
-          <span class="stat-item"><i class="dot point-dot"></i>燃气管点 {{ gasPointCount }}</span>
-          <span class="stat-item"><i class="dot manhole-dot"></i>窨井 {{ manholeCount }}</span>
+          <span class="stat-item"><i class="dot pipeline-dot"></i>管线 {{ pipelineCount }} 段</span>
+          <span class="stat-item"><i class="dot point-dot"></i>总长度 {{ pipelineTotalLength }} km</span>
         </div>
       </div>
       <div class="header-right">
-        <el-input
-            v-model="searchKeyword"
-            placeholder="搜索管点/井盖编号/位置"
-            clearable
-            :prefix-icon="Search"
-            style="width: 240px"
-            size="default"
-        />
-        <el-button type="primary" :icon="Refresh" @click="refreshMap">刷新</el-button>
+        <el-input v-model="searchKeyword" placeholder="搜索管线名称/编号" clearable :prefix-icon="Search" style="width: 240px" />
+        <el-button type="primary" :icon="Refresh" :loading="loading" @click="loadPipelines">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>
 
-    <!-- 主内容区:左侧图层控制 + 右侧GIS地图 -->
     <div class="main-layout">
-      <!-- 左侧图层控制面板 -->
       <div class="layer-panel">
-        <div class="layer-header">
-          <el-icon></el-icon> 图层控制
-        </div>
-        <div class="layer-tree">
-          <el-checkbox-group v-model="visibleLayers">
-            <div class="layer-group">
-              <div class="group-title">燃气管线</div>
-              <el-checkbox value="highPressure">高压管线</el-checkbox>
-              <el-checkbox value="mediumPressure">中压管线</el-checkbox>
-              <el-checkbox value="lowPressure">低压管线</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">燃气管点</div>
-              <el-checkbox value="valve">阀门</el-checkbox>
-              <el-checkbox value="regulator">调压器</el-checkbox>
-              <el-checkbox value="meter">流量计</el-checkbox>
-              <el-checkbox value="terminal">终端节点</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">窨井设施</div>
-              <el-checkbox value="manhole">窨井盖</el-checkbox>
-              <el-checkbox value="inspection">检查井</el-checkbox>
-            </div>
-          </el-checkbox-group>
-        </div>
+        <div class="layer-header"><el-icon><Grid /></el-icon> 图层控制</div>
+        <el-checkbox-group v-model="visibleLayers">
+          <div class="layer-group">
+            <div class="group-title">燃气管线</div>
+            <el-checkbox value="highPressure">高压管线</el-checkbox>
+            <el-checkbox value="mediumPressure">中压管线</el-checkbox>
+            <el-checkbox value="lowPressure">低压管线</el-checkbox>
+            <el-checkbox value="other">其他/未分级管线</el-checkbox>
+          </div>
+        </el-checkbox-group>
         <div class="legend-panel">
           <div class="legend-title">图例说明</div>
           <div><span class="legend-line high"></span> 高压管线</div>
           <div><span class="legend-line medium"></span> 中压管线</div>
           <div><span class="legend-line low"></span> 低压管线</div>
-          <div><span class="legend-point valve"></span> 阀门</div>
-          <div><span class="legend-point regulator"></span> 调压器</div>
-          <div><span class="legend-point meter"></span> 流量计</div>
-          <div><span class="legend-point manhole"></span> 窨井</div>
+          <div><span class="legend-line other"></span> 其他/未分级</div>
         </div>
         <div class="info-stats">
-          <div class="info-title">测绘数据统计</div>
-          <div>管线总长: {{ pipelineTotalLength }} km</div>
-          <div>管点总数: {{ gasPointCount }}</div>
-          <div>窨井总数: {{ manholeCount }}</div>
-          <div>覆盖面积: 156.8 km²</div>
+          <div class="info-title">燃气管网数据统计</div>
+          <div>管线总数:{{ pipelineCount }} 段</div>
+          <div>管线总长:{{ pipelineTotalLength }} km</div>
+          <div>数据来源:pipe_network_base</div>
+          <div>设施类型:0-燃气设施</div>
         </div>
+        <el-alert v-if="errorMessage" :title="errorMessage" type="warning" :closable="false" show-icon />
       </div>
 
-      <!-- 右侧GIS地图区域 -->
       <div class="gis-container">
         <div class="map-toolbar">
           <el-button-group>
             <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
             <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
-            <el-button size="small" :icon="Location" @click="locateCurrent">定位</el-button>
+            <el-button size="small" :icon="Location" @click="fitBounds">定位</el-button>
           </el-button-group>
-          <div class="coordinate-info">
-            当前坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}
-          </div>
+          <div class="coordinate-info">燃气管线实时地图</div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="gasMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="updateCoordinates"></canvas>
-          <!-- 悬浮信息卡 -->
-          <div v-if="hoverFeature" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoverFeature.name }}</div>
-            <div>类型: {{ hoverFeature.typeName }}</div>
-            <div v-if="hoverFeature.pressure">压力等级: {{ hoverFeature.pressure }}</div>
-            <div v-if="hoverFeature.diameter">管径: {{ hoverFeature.diameter }}mm</div>
-            <div v-if="hoverFeature.material">材质: {{ hoverFeature.material }}</div>
-            <div>位置: {{ hoverFeature.address || hoverFeature.location }}</div>
-          </div>
+        <div class="map-wrapper">
+          <GisMapCanvas ref="mapRef" :lines="visibleLines" :center="mapCenter" :zoom="14" :show-marker-name="false" :enable-clustering="false" @ready="handleMapReady" @line-click="showPipeline" @error="errorMessage = $event" />
+          <div v-if="!loading && !visibleLines.length" class="empty-map">暂无带起止坐标的燃气管线数据</div>
         </div>
-
-        <!-- 右侧详情抽屉 -->
-        <el-drawer v-model="detailDrawerVisible" :title="`设施详情 - ${currentFeature?.name}`" size="420px" direction="rtl" class="detail-drawer">
-          <div v-if="currentFeature" class="feature-detail">
-            <el-descriptions :column="1" border>
-              <el-descriptions-item label="设施名称">{{ currentFeature.name }}</el-descriptions-item>
-              <el-descriptions-item label="设施类型">{{ currentFeature.typeName }}</el-descriptions-item>
-              <el-descriptions-item label="设施编号">{{ currentFeature.code || '—' }}</el-descriptions-item>
-              <el-descriptions-item label="详细地址">{{ currentFeature.address || currentFeature.location }}</el-descriptions-item>
-              <el-descriptions-item label="经纬度">{{ currentFeature.lng }}, {{ currentFeature.lat }}</el-descriptions-item>
-              <el-descriptions-item label="所属区域">{{ currentFeature.district || '浦东新区' }}</el-descriptions-item>
-              <el-descriptions-item label="权属单位">{{ currentFeature.ownerUnit || '燃气集团' }}</el-descriptions-item>
-              <el-descriptions-item label="建设年份">{{ currentFeature.buildYear || '2020' }}</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.pressure" label="压力等级">{{ currentFeature.pressure }}</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.diameter" label="管径">{{ currentFeature.diameter }}mm</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.material" label="材质">{{ currentFeature.material }}</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.manholeType" label="井盖类型">{{ currentFeature.manholeType }}</el-descriptions-item>
-            </el-descriptions>
-            <div class="history-section" v-if="currentFeature.records?.length">
-              <div class="sub-title">运维记录</div>
-              <el-timeline>
-                <el-timeline-item v-for="record in currentFeature.records" :key="record.id" :timestamp="record.time" :type="record.type">
-                  {{ record.content }}
-                </el-timeline-item>
-              </el-timeline>
-            </div>
-          </div>
+        <el-drawer v-model="detailDrawerVisible" :title="`管线详情 - ${currentPipeline?.name || ''}`" size="420px" direction="rtl">
+          <el-descriptions v-if="currentPipeline" :column="1" border size="small">
+            <el-descriptions-item label="设施名称">{{ currentPipeline.name || '—' }}</el-descriptions-item>
+            <el-descriptions-item label="设施编号">{{ currentPipeline.code || '—' }}</el-descriptions-item>
+            <el-descriptions-item label="设施类型">{{ currentPipeline.networkTypeName }}</el-descriptions-item>
+            <el-descriptions-item label="管网等级">{{ currentPipeline.level || '—' }}</el-descriptions-item>
+            <el-descriptions-item label="管径">{{ currentPipeline.diameter || '—' }} mm</el-descriptions-item>
+            <el-descriptions-item label="材质">{{ currentPipeline.material || '—' }}</el-descriptions-item>
+            <el-descriptions-item label="长度">{{ currentPipeline.length || '—' }} m</el-descriptions-item>
+            <el-descriptions-item label="位置描述">{{ currentPipeline.location || '—' }}</el-descriptions-item>
+          </el-descriptions>
         </el-drawer>
       </div>
     </div>
@@ -132,427 +77,98 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
-import { MapLocation, Search, Refresh, FullScreen, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
-
-// 可见图层
-const visibleLayers = ref(['highPressure', 'mediumPressure', 'lowPressure', 'valve', 'regulator', 'meter', 'manhole'])
-
-// --- 模拟燃气管线数据 (线段)---
-const pipelines = ref([
-  { id: 'P1', name: '张杨路高压管线', type: 'highPressure', pressure: '高压', diameter: 500, material: '钢管', color: '#f56c6c', lineWidth: 4,
-    coordinates: [[121.48, 31.24], [121.50, 31.245], [121.52, 31.24], [121.54, 31.235]] },
-  { id: 'P2', name: '世纪大道中压管线', type: 'mediumPressure', pressure: '中压', diameter: 300, material: 'PE管', color: '#e6a23c', lineWidth: 3,
-    coordinates: [[121.49, 31.23], [121.51, 31.228], [121.53, 31.225], [121.55, 31.22]] },
-  { id: 'P3', name: '浦东南路低压管线', type: 'lowPressure', pressure: '低压', diameter: 150, material: 'PE管', color: '#67c23a', lineWidth: 2,
-    coordinates: [[121.485, 31.255], [121.50, 31.25], [121.515, 31.248], [121.53, 31.245]] },
-  { id: 'P4', name: '龙阳路中压管线', type: 'mediumPressure', pressure: '中压', diameter: 250, material: '钢管', color: '#e6a23c', lineWidth: 3,
-    coordinates: [[121.47, 31.22], [121.49, 31.218], [121.51, 31.215], [121.53, 31.212]] }
-])
-
-// 模拟燃气管点数据
-const gasPoints = ref([
-  { id: 'V1', name: '世纪大道阀门井', type: 'valve', typeName: '阀门', code: 'VAL-001', lng: 121.505, lat: 31.235, address: '世纪大道与浦电路口', district: '浦东新区', ownerUnit: '燃气集团输配分公司', buildYear: '2019', records: [] },
-  { id: 'V2', name: '张杨路调压站', type: 'regulator', typeName: '调压器', code: 'REG-012', lng: 121.52, lat: 31.242, address: '张杨路628号', district: '浦东新区', ownerUnit: '燃气集团', buildYear: '2018', records: [] },
-  { id: 'V3', name: '龙阳路流量计', type: 'meter', typeName: '流量计', code: 'MET-005', lng: 121.51, lat: 31.215, address: '龙阳路2000号', district: '浦东新区', ownerUnit: '燃气集团', buildYear: '2020', records: [] },
-  { id: 'V4', name: '浦电路终端节点', type: 'terminal', typeName: '终端节点', code: 'TER-023', lng: 121.49, lat: 31.23, address: '浦电路400号', district: '浦东新区', ownerUnit: '燃气集团', buildYear: '2021', records: [] },
-  { id: 'V5', name: '东方路阀门', type: 'valve', typeName: '阀门', code: 'VAL-045', lng: 121.53, lat: 31.248, address: '东方路与浦建路口', district: '浦东新区', ownerUnit: '燃气集团', buildYear: '2019', records: [] }
-])
-
-// 模拟窨井数据
-const manholes = ref([
-  { id: 'M1', name: '世纪大道窨井', type: 'manhole', typeName: '窨井盖', code: 'MH-1001', lng: 121.50, lat: 31.238, address: '世纪大道福山路口', manholeType: '燃气检查井', depth: 2.5, diameter: 0.8, ownerUnit: '燃气集团', records: [] },
-  { id: 'M2', name: '张杨路检查井', type: 'inspection', typeName: '检查井', code: 'IN-023', lng: 121.515, lat: 31.244, address: '张杨路松林路口', manholeType: '阀门井', depth: 3.0, diameter: 1.2, ownerUnit: '燃气集团', records: [] },
-  { id: 'M3', name: '浦电路窨井', type: 'manhole', typeName: '窨井盖', code: 'MH-1005', lng: 121.49, lat: 31.228, address: '浦电路向城路口', manholeType: '燃气检查井', depth: 2.2, diameter: 0.7, ownerUnit: '燃气集团', records: [] },
-  { id: 'M4', name: '龙阳路检查井', type: 'inspection', typeName: '检查井', code: 'IN-045', lng: 121.525, lat: 31.218, address: '龙阳路白杨路口', manholeType: '调压箱井', depth: 2.8, diameter: 1.0, ownerUnit: '燃气集团', records: [] },
-  { id: 'M5', name: '东方路窨井', type: 'manhole', typeName: '窨井盖', code: 'MH-1012', lng: 121.535, lat: 31.25, address: '东方路蓝村路口', manholeType: '燃气检查井', depth: 2.3, diameter: 0.8, ownerUnit: '燃气集团', records: [] }
-])
-
-// 统计数据
-const pipelineTotalLength = computed(() => {
-  // 模拟计算管线总长
-  return 48.6
-})
-const pipelineCount = computed(() => 48.6)
-const gasPointCount = computed(() => gasPoints.value.length)
-const manholeCount = computed(() => manholes.value.length)
-
-// GIS画布相关
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
-const currentLng = ref(121.505)
-const currentLat = ref(31.235)
-const hoverFeature = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
-const currentFeature = ref(null)
-const detailDrawerVisible = ref(false)
+import { computed, nextTick, onMounted, ref } from 'vue'
+import { MapLocation, Grid, Search, Refresh, FullScreen, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
+import { getGisPipeLines } from '@/api/pipeNetwork/basic'
+
+const mapRef = ref(null)
+const loading = ref(false)
+const errorMessage = ref('')
 const searchKeyword = ref('')
-
-// 坐标转换
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
-}
-
-function canvasToWorld(x, y) {
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  const px = (x - cx - offsetX) / mapZoom + cx
-  const py = (y - cy - offsetY) / mapZoom + cy
-  const lng = bounds.minLng + (px / mapWidth) * (bounds.maxLng - bounds.minLng)
-  const lat = bounds.maxLat - (py / mapHeight) * (bounds.maxLat - bounds.minLat)
-  return { lng, lat }
-}
-
-// 绘制底图
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-
-  // 绘制网格
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0)
-    ctx.lineTo(x, mapHeight)
-    ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y)
-    ctx.lineTo(mapWidth, y)
-    ctx.stroke()
-  }
-
-  // 绘制道路示意
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('世纪大道', 350, 120)
-  ctx.fillText('张杨路', 450, 280)
-  ctx.fillText('龙阳路', 200, 370)
-  ctx.fillText('东方路', 580, 200)
-}
-
-// 绘制管线
-function drawPipelines() {
-  pipelines.value.forEach(pipeline => {
-    if (!visibleLayers.value.includes(pipeline.type)) return
-
-    const points = pipeline.coordinates.map(coord => worldToCanvas(coord[0], coord[1]))
-    if (points.length < 2) return
-
-    ctx.beginPath()
-    ctx.moveTo(points[0].x, points[0].y)
-    for (let i = 1; i < points.length; i++) {
-      ctx.lineTo(points[i].x, points[i].y)
-    }
-    ctx.strokeStyle = pipeline.color
-    ctx.lineWidth = pipeline.lineWidth * mapZoom
-    ctx.stroke()
-
-    // 绘制管线名称标签
-    const midIdx = Math.floor(points.length / 2)
-    if (points[midIdx]) {
-      ctx.fillStyle = '#333'
-      ctx.font = '10px "Microsoft YaHei"'
-      ctx.shadowBlur = 0
-      ctx.fillText(pipeline.name.slice(0, 8), points[midIdx].x - 20, points[midIdx].y - 5)
-    }
-  })
-}
-
-// 绘制点要素
-function drawPoints(points, type, iconColor, size = 8) {
-  points.forEach(point => {
-    if (!visibleLayers.value.includes(point.type)) return
-
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    if (x < -20 || x > mapWidth + 20 || y < -20 || y > mapHeight + 20) return
-
-    ctx.beginPath()
-    ctx.arc(x, y, size, 0, 2 * Math.PI)
-    ctx.fillStyle = iconColor
-    ctx.fill()
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 2
-    ctx.stroke()
-
-    // 绘制小标签
-    ctx.fillStyle = iconColor
-    ctx.font = 'bold 10px sans-serif'
-    ctx.fillText(point.name.slice(0, 4), x - 10, y - 6)
-  })
-}
-
-// 绘制窨井(方形)
-function drawManholesPoints() {
-  manholes.value.forEach(manhole => {
-    if (!visibleLayers.value.includes(manhole.type)) return
-
-    const { x, y } = worldToCanvas(manhole.lng, manhole.lat)
-    if (x < -20 || x > mapWidth + 20 || y < -20 || y > mapHeight + 20) return
-
-    ctx.fillStyle = '#8B4513'
-    ctx.fillRect(x - 6, y - 6, 12, 12)
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 1.5
-    ctx.strokeRect(x - 6, y - 6, 12, 12)
-    ctx.fillStyle = '#8B4513'
-    ctx.font = '9px sans-serif'
-    ctx.fillText('井', x - 3, y + 3)
-  })
-}
-
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-
-  drawBaseMap()
-  drawPipelines()
-  drawPoints(gasPoints.value, 'point', '#409eff', 7)
-  drawManholesPoints()
-}
-
-// 地图操作
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-function locateCurrent() { fitBounds() }
-function refreshMap() { renderMap() }
-
-// 坐标显示更新
-function updateCoordinates(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  const { lng, lat } = canvasToWorld(mouseX, mouseY)
-  currentLng.value = lng
-  currentLat.value = lat
-}
-
-// 点击事件 - 查询最近的管点或窨井
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-
-  let minDist = 25
-  let hit = null
-
-  // 检查燃气管点
-  gasPoints.value.forEach(point => {
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      hit = point
-    }
-  })
-
-  // 检查窨井
-  manholes.value.forEach(manhole => {
-    const { x, y } = worldToCanvas(manhole.lng, manhole.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      hit = { ...manhole, isManhole: true }
-    }
-  })
-
-  if (hit) {
-    currentFeature.value = hit
-    detailDrawerVisible.value = true
+const visibleLayers = ref(['highPressure', 'mediumPressure', 'lowPressure', 'other'])
+const pipelines = ref([])
+const currentPipeline = ref(null)
+const detailDrawerVisible = ref(false)
+const mapCenter = ref({ lng: 110.3965, lat: 28.4638 })
+
+function normalizeLevel(value) {
+  const text = String(value || '').trim().toLowerCase()
+  if (text.includes('高') || text.includes('high')) return { key: 'highPressure', name: '高压' }
+  if (text.includes('中') || text.includes('medium')) return { key: 'mediumPressure', name: '中压' }
+  if (text.includes('低') || text.includes('low')) return { key: 'lowPressure', name: '低压' }
+  return { key: 'other', name: value || '未分级' }
+}
+
+function normalizeFeature(feature) {
+  const p = feature?.properties || {}
+  const geometry = feature?.geometry
+  const coordinates = geometry?.type === 'LineString' ? geometry.coordinates : []
+  const start = coordinates[0] || [p.startLongitude, p.startLatitude]
+  const end = coordinates[coordinates.length - 1] || [p.endLongitude, p.endLatitude]
+  const points = [start, end].map(item => ({ lng: Number(item?.[0]), lat: Number(item?.[1]) })).filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
+  if (points.length < 2) return null
+  const level = normalizeLevel(p.networkLevel)
+  return {
+    id: p.networkId,
+    code: p.networkCode,
+    name: p.networkName || p.networkCode || p.networkId,
+    networkTypeName: '燃气设施(0)',
+    level: level.name,
+    layer: level.key,
+    material: p.material,
+    diameter: p.diameter,
+    length: p.length,
+    location: p.location,
+    points,
+    color: level.key === 'highPressure' ? '#e34d59' : level.key === 'mediumPressure' ? '#e6a23c' : level.key === 'lowPressure' ? '#32a852' : '#409eff',
+    strokeWeight: level.key === 'highPressure' ? 6 : 5,
+    strokeOpacity: 0.9
   }
 }
 
-// 鼠标悬浮检测
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-
-  let minDist = 20
-  let hit = null
-
-  gasPoints.value.forEach(point => {
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      hit = point
-    }
-  })
-
-  manholes.value.forEach(manhole => {
-    const { x, y } = worldToCanvas(manhole.lng, manhole.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) {
-      minDist = dist
-      hit = { ...manhole, isManhole: true }
-    }
-  })
-
-  if (hit) {
-    hoverFeature.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else {
-    hoverFeature.value = null
+async function loadPipelines() {
+  loading.value = true
+  errorMessage.value = ''
+  try {
+    const response = await getGisPipeLines({ networkType: '0' })
+    const features = Array.isArray(response?.data?.features) ? response.data.features : []
+    pipelines.value = features.map(normalizeFeature).filter(Boolean)
+    const firstPoint = pipelines.value[0]?.points?.[0]
+    if (firstPoint) mapCenter.value = firstPoint
+    await nextTick()
+    mapRef.value?.fitBounds?.()
+  } catch (error) {
+    pipelines.value = []
+    errorMessage.value = '燃气管网数据加载失败,请检查服务或网络。'
+    ElMessage.error(errorMessage.value)
+  } finally {
+    loading.value = false
   }
 }
 
-// 拖动地图
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
-  })
-
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x
-    const dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx
-    offsetY = dragStart.offsetY + dy
-    renderMap()
-  })
-
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
-  })
-}
-
-// 监听图层变化重新渲染
-watch(() => visibleLayers.value, () => renderMap(), { deep: true })
-
-onMounted(() => {
-  nextTick(() => {
-    renderMap()
-    initDragEvents()
-    window.addEventListener('resize', renderMap)
+const visibleLines = computed(() => {
+  const keyword = searchKeyword.value.trim().toLowerCase()
+  return pipelines.value.filter(item => {
+    const matched = !keyword || [item.name, item.code, item.location].some(value => String(value || '').toLowerCase().includes(keyword))
+    return matched && visibleLayers.value.includes(item.layer)
   })
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
+const pipelineCount = computed(() => pipelines.value.length)
+const pipelineTotalLength = computed(() => (pipelines.value.reduce((sum, item) => sum + (Number(item.length) || 0), 0) / 1000).toFixed(2))
+function showPipeline(line) { currentPipeline.value = line; detailDrawerVisible.value = true }
+function handleMapReady() { mapRef.value?.fitBounds?.() }
+function fitBounds() { mapRef.value?.fitBounds?.() }
+function zoomIn() { mapRef.value?.zoomIn?.() }
+function zoomOut() { mapRef.value?.zoomOut?.() }
+onMounted(loadPipelines)
 </script>
 
 <style scoped>
-.gas-gis-dashboard {
-  background: #f0f2f6;
-  height: 100vh;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.page-header {
-  background: white;
-  padding: 12px 24px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  border-bottom: 1px solid #e9ecef;
-  flex-shrink: 0;
-}
-.header-left { display: flex; align-items: center; gap: 20px; }
-.header-left .title { font-size: 1.3rem; font-weight: 600; color: #1f2d3d; }
-.stats-badge { display: flex; gap: 16px; }
-.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
-.dot { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
-.pipeline-dot { background: #f56c6c; }
-.point-dot { background: #409eff; border-radius: 50%; }
-.manhole-dot { background: #8B4513; }
-.header-right { display: flex; gap: 12px; }
-
-.main-layout {
-  display: flex;
-  flex: 1;
-  overflow: hidden;
-  padding: 12px;
-  gap: 12px;
-}
-.layer-panel {
-  width: 260px;
-  background: white;
-  border-radius: 12px;
-  padding: 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  overflow-y: auto;
-  flex-shrink: 0;
-}
-.layer-header { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
-.layer-group { margin-bottom: 12px; }
-.group-title { font-weight: 500; margin-bottom: 8px; font-size: 13px; color: #606266; }
-.layer-group .el-checkbox { display: block; margin-left: 8px; margin-bottom: 6px; }
-.legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; }
-.legend-title { font-weight: 500; margin-bottom: 8px; font-size: 13px; }
-.legend-line { display: inline-block; width: 30px; height: 3px; margin-right: 8px; vertical-align: middle; }
-.legend-line.high { background: #f56c6c; }
-.legend-line.medium { background: #e6a23c; }
-.legend-line.low { background: #67c23a; }
-.legend-point { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; }
-.legend-point.valve { background: #409eff; }
-.legend-point.regulator { background: #e6a23c; }
-.legend-point.meter { background: #67c23a; }
-.legend-point.manhole { background: #8B4513; border-radius: 2px; }
-.info-stats { background: #f5f7fa; border-radius: 8px; padding: 12px; font-size: 13px; }
-.info-stats .info-title { font-weight: 600; margin-bottom: 8px; }
-
-.gis-container {
-  flex: 1;
-  background: white;
-  border-radius: 12px;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.map-toolbar {
-  padding: 10px 16px;
-  border-bottom: 1px solid #e9ecef;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.coordinate-info { font-size: 12px; color: #909399; }
-.map-wrapper {
-  flex: 1;
-  position: relative;
-  cursor: crosshair;
-}
-canvas { width: 100%; height: 100%; display: block; cursor: crosshair; }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
-  border-radius: 10px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
-  backdrop-filter: blur(4px);
-}
-.feature-detail { padding: 8px; }
-.sub-title { font-weight: 600; margin: 16px 0 8px 0; }
-</style>
+.gas-gis-dashboard { background: #f0f2f6; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
+.page-header { background: white; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e9ecef; flex-shrink: 0; }
+.header-left, .header-right, .stats-badge { display: flex; align-items: center; gap: 16px; }.header-left .title { font-size: 1.3rem; font-weight: 600; color: #1f2d3d; }.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }.dot { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }.pipeline-dot { background: #e34d59; }.point-dot { background: #409eff; border-radius: 50%; }
+.main-layout { display: flex; flex: 1; overflow: hidden; padding: 12px; gap: 12px; }.layer-panel { width: 260px; background: white; border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; flex-shrink: 0; }.layer-header { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }.group-title, .legend-title, .info-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; color: #606266; }.layer-group .el-checkbox { display: block; margin-left: 8px; margin-bottom: 8px; }.legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; line-height: 26px; font-size: 13px; }.legend-line { display: inline-block; width: 30px; height: 4px; margin-right: 8px; vertical-align: middle; }.legend-line.high { background: #e34d59; }.legend-line.medium { background: #e6a23c; }.legend-line.low { background: #32a852; }.legend-line.other { background: #409eff; }.info-stats { background: #f5f7fa; border-radius: 8px; padding: 12px; font-size: 13px; line-height: 26px; }
+.gis-container { flex: 1; min-width: 0; background: white; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; }.map-toolbar { padding: 10px 16px; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; }.coordinate-info { font-size: 12px; color: #909399; }.map-wrapper { flex: 1; min-height: 420px; position: relative; }.empty-map { position: absolute; inset: 0; display: grid; place-items: center; color: #909399; pointer-events: none; }
+</style>

+ 0 - 438
src/views/subSystem/gas/gasOperamon/krqildjc.vue

@@ -1,438 +0,0 @@
-<!--场站可燃气体浓度监测-->
-<template>
-  <div class="gas-station-monitor">
-    <!-- 页面头部 -->
-    <div class="page-header">
-      <div class="header-left">
-        <el-icon><Warning /></el-icon>
-        <span class="title">燃气场站安全监测中心</span>
-        <div class="stats-badge">
-          <span class="stat-item" :class="{ alert: alertCount > 0 }">
-            <i class="dot"></i>告警场站 {{ alertCount }}
-          </span>
-          <span class="stat-item">正常场站 {{ normalCount }}</span>
-          <span class="stat-item">接入场站 {{ totalStations }}</span>
-        </div>
-      </div>
-      <div class="header-right">
-        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
-      </div>
-    </div>
-
-    <!-- KPI指标卡片 -->
-    <div class="kpi-cards">
-      <div class="kpi-card critical">
-        <div class="kpi-value">{{ maxMethane }}<span class="unit">ppm</span></div>
-        <div class="kpi-label">最高甲烷浓度</div>
-        <div class="kpi-trend">场站: {{ maxMethaneStation }}</div>
-      </div>
-      <div class="kpi-card">
-        <div class="kpi-value">{{ riskScore }}<span class="unit">分</span></div>
-        <div class="kpi-label">综合风险评分</div>
-        <div class="kpi-trend trend-down">越低越安全</div>
-      </div>
-    </div>
-
-    <!-- 主内容区:左侧场站列表 + 右侧详情曲线 -->
-    <div class="main-layout">
-      <!-- 左侧场站列表 -->
-      <div class="station-list-panel">
-        <div class="list-header">
-          <span>燃气场站列表</span>
-          <el-input
-              v-model="searchKeyword"
-              placeholder="搜索场站名称"
-              clearable
-              :prefix-icon="Search"
-              style="width: 160px"
-              size="small"
-          />
-        </div>
-        <div class="station-list">
-          <div
-              v-for="station in paginatedStations"
-              :key="station.pointId"
-              class="station-item"
-              :class="{
-              selected: selectedStation?.pointId === station.pointId,
-              alert: station.alertStatus
-            }"
-              @click="selectStation(station)"
-          >
-            <div class="station-header">
-              <span class="station-name">{{ station.pointName }}</span>
-              <el-tag :type="station.alertStatus ? 'danger' : 'success'" size="small">
-                {{ station.alertStatus ? '告警' : '正常' }}
-              </el-tag>
-            </div>
-            <div class="station-address">
-              <el-icon><Location /></el-icon> {{ station.location }}
-            </div>
-            <div class="station-metrics">
-              <div class="metric">
-                <span class="label">甲烷浓度</span>
-                <span class="value" :class="{ 'alert-value': station.alertStatus }">
-                  {{ station.methaneValue }} ppm
-                </span>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="pagination-area">
-          <el-pagination
-              small
-              layout="prev, pager, next"
-              :total="filteredStations.length"
-              :page-size="pageSize"
-              v-model:current-page="currentPage"
-          />
-        </div>
-      </div>
-
-      <!-- 右侧场站详情 -->
-      <div class="detail-panel" v-if="selectedStation">
-        <div class="detail-header">
-          <div class="title-section">
-            <h3>{{ selectedStation.pointName }}</h3>
-            <el-tag :type="selectedStation.alertStatus ? 'danger' : 'success'" size="large">
-              {{ selectedStation.alertStatus ? '实时告警中' : '运行正常' }}
-            </el-tag>
-          </div>
-          <div class="address-info">
-            <el-icon><Location /></el-icon> {{ selectedStation.location }}
-          </div>
-        </div>
-
-        <!-- 实时监测数据卡片 -->
-        <div class="realtime-cards">
-          <div class="realtime-card" :class="{ warning: selectedStation.alertStatus }">
-            <div class="card-icon"><el-icon><WindPower /></el-icon></div>
-            <div class="card-content">
-              <div class="card-label">甲烷浓度 (CH₄)</div>
-              <div class="card-value">{{ selectedStation.methaneValue }} <span class="unit">ppm</span></div>
-              <div class="card-threshold">阈值: {{ selectedStation.methaneThreshold }} ppm</div>
-            </div>
-          </div>
-          <div class="realtime-card">
-            <div class="card-icon"><el-icon><Odometer /></el-icon></div>
-            <div class="card-content">
-              <div class="card-label">环境温度</div>
-              <div class="card-value">{{ selectedStation.temperature }} <span class="unit">°C</span></div>
-              <div class="card-threshold">正常范围</div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 监测数据趋势图 -->
-        <div class="chart-section">
-          <div class="section-header">
-            <span><el-icon><TrendCharts /></el-icon> 甲烷浓度趋势(最近24小时)</span>
-          </div>
-          <div ref="trendChartRef" class="chart-container"></div>
-        </div>
-
-        <!-- SCADA系统接入信息 -->
-        <div class="scada-section">
-          <div class="section-header">
-            <span><el-icon><Monitor /></el-icon> SCADA系统接入信息</span>
-          </div>
-          <el-descriptions :column="2" border size="small">
-            <el-descriptions-item label="数据来源">燃气公司SCADA系统</el-descriptions-item>
-            <el-descriptions-item label="最后通讯时间">{{ selectedStation.lastUpdateTime || '--' }}</el-descriptions-item>
-            <el-descriptions-item label="数据采集频率">10秒/次</el-descriptions-item>
-            <el-descriptions-item label="通讯状态">
-              <el-tag type="success">在线</el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="场站编号">{{ selectedStation.stationCode }}</el-descriptions-item>
-            <el-descriptions-item label="关联设备">{{ selectedStation.deviceCount }}台</el-descriptions-item>
-          </el-descriptions>
-        </div>
-      </div>
-
-      <!-- 未选中状态 -->
-      <div v-else class="empty-panel">
-        <el-empty description="请从左侧选择燃气场站查看详情" :image-size="140" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
-import { Warning, Refresh, Location, WindPower, Odometer, TrendCharts, Monitor, Search } from '@element-plus/icons-vue'
-import * as echarts from 'echarts'
-import { ElMessage } from 'element-plus'
-import { getGasMonitorPoints, getGasMonitorTrend } from '@/api/pipeNetwork/gasMonitor'
-
-// ECharts实例
-const trendChartRef = ref(null)
-let trendChartInstance = null
-
-// 加载数据
-const stations = ref([])
-const loading = ref(false)
-
-async function loadStations() {
-  loading.value = true
-  try {
-    const res = await getGasMonitorPoints()
-    stations.value = res.data || []
-    if (stations.value.length > 0) {
-      selectStation(stations.value[0])
-    }
-  } finally {
-    loading.value = false
-  }
-}
-
-// 统计数据
-const totalStations = computed(() => stations.value.length)
-const alertCount = computed(() => stations.value.filter(s => s.alertStatus).length)
-const normalCount = computed(() => totalStations.value - alertCount.value)
-const maxMethane = computed(() => {
-  if (!stations.value.length) return 0
-  return Math.max(...stations.value.map(s => s.methaneValue))
-})
-const maxMethaneStation = computed(() => {
-  const max = maxMethane.value
-  return stations.value.find(s => s.methaneValue === max)?.pointName || '—'
-})
-const riskScore = computed(() => {
-  if (!stations.value.length) return 0
-  const alertRatio = alertCount.value / totalStations.value
-  const avgMethane = stations.value.reduce((sum, s) => sum + s.methaneValue, 0) / totalStations.value
-  let score = 100 - (alertRatio * 50 + (avgMethane / 40) * 30)
-  return Math.max(0, Math.min(100, Math.round(score)))
-})
-
-// 筛选
-const searchKeyword = ref('')
-const currentPage = ref(1)
-const pageSize = 5
-const selectedStation = ref(null)
-
-const filteredStations = computed(() => {
-  let list = stations.value
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(s => s.pointName.toLowerCase().includes(kw))
-  }
-  return list
-})
-const paginatedStations = computed(() => {
-  const start = (currentPage.value - 1) * pageSize
-  return filteredStations.value.slice(start, start + pageSize)
-})
-
-// 趋势图
-function renderTrendChart(trendRes) {
-  if (!trendChartRef.value || !selectedStation.value) return
-  if (!trendChartInstance) {
-    trendChartInstance = echarts.init(trendChartRef.value)
-  }
-  const data = trendRes || {}
-  const times = data.times || []
-  const values = data.methaneValues || []
-  const threshold = data.threshold || 20
-
-  trendChartInstance.setOption({
-    tooltip: { trigger: 'axis' },
-    legend: { data: ['甲烷浓度', '报警阈值'], bottom: 0 },
-    grid: { left: 50, right: 30, top: 20, bottom: 30 },
-    xAxis: {
-      type: 'category', data: times,
-      axisLabel: { rotate: 45, interval: 3, fontSize: 10 }
-    },
-    yAxis: { type: 'value', name: 'ppm', axisLabel: { fontSize: 10 } },
-    series: [
-      {
-        name: '甲烷浓度', type: 'line', data: values,
-        smooth: true,
-        lineStyle: { color: '#f56c6c', width: 2 },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(245,108,108,0.3)' },
-            { offset: 1, color: 'rgba(255,255,255,0.05)' }
-          ])
-        },
-        symbol: 'none'
-      },
-      {
-        name: '报警阈值', type: 'line',
-        data: Array(times.length || 24).fill(threshold),
-        lineStyle: { color: '#f56c6c', width: 1.5, type: 'dashed' },
-        symbol: 'none'
-      }
-    ]
-  })
-}
-
-async function selectStation(station) {
-  selectedStation.value = station
-  currentPage.value = 1
-  try {
-    const res = await getGasMonitorTrend(station.pointId, 24)
-    await nextTick()
-    renderTrendChart(res.data || {})
-  } catch {
-    await nextTick()
-    renderTrendChart({})
-  }
-}
-
-function refreshData() {
-  ElMessage.success('数据已刷新')
-  loadStations()
-}
-
-onMounted(() => {
-  loadStations()
-})
-
-onBeforeUnmount(() => {
-  if (trendChartInstance) {
-    trendChartInstance.dispose()
-    trendChartInstance = null
-  }
-})
-</script>
-
-<style scoped>
-.gas-station-monitor {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-.page-header {
-  background: white;
-  border-radius: 16px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-}
-.header-left { display: flex; align-items: center; gap: 20px; }
-.header-left .title { font-size: 1.3rem; font-weight: 600; color: #1f2d3d; }
-.stats-badge { display: flex; gap: 20px; }
-.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
-.stat-item.alert { color: #f56c6c; }
-.dot { width: 10px; height: 10px; border-radius: 50%; background: #67c23a; display: inline-block; }
-.stat-item.alert .dot { background: #f56c6c; }
-.header-right { display: flex; gap: 12px; }
-
-.kpi-cards {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16px;
-  margin-bottom: 20px;
-}
-.kpi-card {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
-  text-align: center;
-}
-.kpi-card.critical .kpi-value { color: #f56c6c; }
-.kpi-value { font-size: 32px; font-weight: bold; color: #1f2d3d; }
-.kpi-value .unit { font-size: 14px; font-weight: normal; margin-left: 4px; }
-.kpi-label { font-size: 13px; color: #909399; margin-top: 6px; }
-.kpi-trend { font-size: 11px; color: #67c23a; margin-top: 4px; }
-.kpi-trend.trend-down { color: #f56c6c; }
-
-.main-layout {
-  display: flex;
-  gap: 20px;
-}
-.station-list-panel {
-  width: 360px;
-  background: white;
-  border-radius: 16px;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.list-header {
-  padding: 16px;
-  border-bottom: 1px solid #e9ecef;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-weight: 600;
-}
-.station-list {
-  flex: 1;
-  overflow-y: auto;
-  padding: 12px;
-  max-height: calc(100vh - 320px);
-}
-.station-item {
-  padding: 14px;
-  margin-bottom: 10px;
-  border-radius: 12px;
-  background: #fafbfc;
-  border: 1px solid #e9ecef;
-  cursor: pointer;
-  transition: all 0.2s;
-}
-.station-item.selected { background: #ecf5ff; border-color: #409eff; }
-.station-item.alert { border-left: 3px solid #f56c6c; background: #fff8f8; }
-.station-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
-.station-name { font-weight: 600; }
-.station-address { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
-.station-metrics { display: flex; gap: 16px; font-size: 12px; }
-.metric .label { color: #909399; }
-.metric .value { font-weight: 500; }
-.metric .value.alert-value { color: #f56c6c; font-weight: bold; }
-
-.detail-panel {
-  flex: 1;
-  background: white;
-  border-radius: 16px;
-  padding: 20px;
-  overflow-y: auto;
-  max-height: calc(100vh - 180px);
-}
-.detail-header { margin-bottom: 20px; }
-.title-section { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
-.title-section h3 { margin: 0; font-size: 20px; }
-.address-info { color: #6c757d; font-size: 13px; display: flex; align-items: center; gap: 6px; }
-
-.realtime-cards {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16px;
-  margin-bottom: 24px;
-}
-.realtime-card {
-  background: #f8f9fc;
-  border-radius: 12px;
-  padding: 14px;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.realtime-card.warning { background: #fff8f0; border: 1px solid #f56c6c; }
-.card-icon .el-icon { font-size: 32px; color: #409eff; }
-.realtime-card.warning .card-icon .el-icon { color: #f56c6c; }
-.card-label { font-size: 12px; color: #909399; }
-.card-value { font-size: 22px; font-weight: bold; color: #1f2d3d; }
-.card-value .unit { font-size: 12px; font-weight: normal; }
-.card-threshold { font-size: 11px; color: #aaa; margin-top: 4px; }
-
-.chart-section, .scada-section {
-  margin-bottom: 24px;
-}
-.section-header {
-  font-weight: 600;
-  margin-bottom: 16px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.chart-container { height: 260px; }
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 16px; }
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>

+ 43 - 12
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -84,15 +84,15 @@
               <el-button size="small" type="primary" @click.stop="handleAlert(alert)">处理</el-button>
             </div>
           </div>
-          <div v-if="alarmStats.realtimeCount === 0" class="empty-state">
+          <div v-if="realtimeEquipments.length === 0" class="empty-state">
             <el-empty description="暂无实时异常" :image-size="80" />
           </div>
         </div>
-        <div class="pagination-area" v-if="alarmStats.realtimeCount > realtimePageSize">
+        <div class="pagination-area" v-if="realtimeEquipments.length > realtimePageSize">
           <el-pagination
             small
             layout="prev, pager, next"
-            :total="alarmStats.realtimeCount"
+            :total="realtimeEquipments.length"
             :page-size="realtimePageSize"
             v-model:current-page="realtimePage"
             @update:current-page="handleRealtimePageChange"
@@ -181,9 +181,14 @@
         </div>
 
         <!-- 处理操作 -->
-        <div class="action-section" v-if="currentAlert.status !== 'resolved'">
+        <div class="action-section" v-if="currentAlert.status === 'pending'">
           <el-button type="primary" @click="resolveAlert">标记已处理</el-button>
-          <el-button type="warning" @click="dispatchWorkOrder">派发工单</el-button>
+          <el-button
+            type="warning"
+            :loading="dispatching"
+            :disabled="currentAlert.hasWorkOrder"
+            @click="dispatchWorkOrder"
+          >{{ currentAlert.hasWorkOrder ? `已关联工单${currentAlert.workOrderStatusText ? `(${currentAlert.workOrderStatusText})` : ''}` : '派发工单' }}</el-button>
         </div>
       </div>
     </el-drawer>
@@ -207,16 +212,22 @@ const historyPage = ref(1)
 const historyPageSize = 8
 const historyTotal = ref(0)
 const alarmStats = ref({ realtimeCount: 0, pendingCount: 0, maxPressure: 0, totalPoints: 0 })
+const dispatching = ref(false)
 
 async function loadEquipment() {
   try {
     const res = await getPressureAlarmEquipment()
     equipments.value = (res.data || []).map(e => ({ ...e,
-      pointName: e.equipmentName, address: e.location,
+      id: e.latestAlarmId || null,
+      pointCode: e.equipmentCode, pointName: e.equipmentName, address: e.location,
       pressure: e.latestPressure, threshold: e.latestThreshold || 250,
       alertTime: e.latestAlarmTime,
       level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
-      status: e.hasAlarm ? 'pending' : 'normal'
+      status: e.hasAlarm ? 'pending' : 'normal',
+      hasWorkOrder: Boolean(e.latestHasWorkOrder),
+      workOrderId: e.latestWorkOrderId || null,
+      workOrderStatus: e.latestWorkOrderStatus ?? null,
+      workOrderStatusText: e.latestWorkOrderStatusText || ''
     }))
   } catch (e) { console.error(e) }
 }
@@ -227,8 +238,10 @@ async function loadHistory() {
     const res = await getPressureAlarmHistory(historyPage.value, historyPageSize, d?.[0] || '', d?.[1] || '', currentDeviceCode.value || '')
     historyAlerts.value = (res.data?.records || []).map(a => ({
       id: a.id, pointCode: a.deviceCode, pointName: a.pointName, address: a.address,
-      pressure: a.pressure, threshold: a.threshold || 250, alertTime: a.alarmTime, status: a.alarmStatus === 0 ? 'pending' : 'resolved',
-      resolveTime: a.resolveTime, handler: a.handler, remark: a.remark, flow: 0
+       pressure: a.pressure, threshold: a.threshold || 250, alertTime: a.alarmTime, status: a.alarmStatus === 0 ? 'pending' : 'resolved',
+       resolveTime: a.resolveTime, handler: a.handler, remark: a.remark, flow: 0,
+       hasWorkOrder: Boolean(a.hasWorkOrder), workOrderId: a.workOrderId || null,
+       workOrderStatus: a.workOrderStatus ?? null, workOrderStatusText: a.workOrderStatusText || ''
     }))
     historyTotal.value = res.data?.total || 0
   } catch (e) { console.error(e) }
@@ -258,9 +271,10 @@ const dateRange = ref([])
 const historySearch = ref('')
 
 // 实时异常分页
+const realtimeEquipments = computed(() => equipments.value.filter(item => item.hasAlarm))
 const paginatedRealtime = computed(() => {
   const start = (realtimePage.value - 1) * realtimePageSize
-  return equipments.value.slice(start, start + realtimePageSize)
+  return realtimeEquipments.value.slice(start, start + realtimePageSize)
 })
 
 // 历史异常筛选和分页
@@ -312,7 +326,24 @@ const resolveAlert = async () => {
   }
 }
 const dispatchWorkOrder = async () => {
-  if (currentAlert.value?.id) { try { await dispatchPressureAlarm(currentAlert.value.id); ElMessage.success('已派发工单(伪)') } catch (e) { ElMessage.error('派发失败') } }
+  if (!currentAlert.value?.id || currentAlert.value.hasWorkOrder || dispatching.value) return
+  dispatching.value = true
+  try {
+    const res = await dispatchPressureAlarm(currentAlert.value.id, {
+      orderDesc: `${currentAlert.value.pointName || '管线'}压力异常,请现场核实处理`
+    })
+    currentAlert.value.hasWorkOrder = true
+    currentAlert.value.workOrderId = res.data || null
+    currentAlert.value.workOrderStatusText = '待派单'
+    currentAlert.value.status = 'resolved'
+    currentAlert.value.resolveTime = new Date().toLocaleString()
+    ElMessage.success(res.msg || '报警已转为待派单工单')
+    await Promise.all([loadEquipment(), loadHistory(), loadStats()])
+  } catch (e) {
+    ElMessage.error(e?.message || '工单派发失败')
+  } finally {
+    dispatching.value = false
+  }
 }
 async function loadStats() {
   try { const res = await getPressureAlarmStats(); if (res.data) alarmStats.value = res.data } catch (e) { console.error(e) }
@@ -434,4 +465,4 @@ onBeforeUnmount(() => { try { chartInstance?.dispose() } catch {}; chartInstance
 .section-title { font-weight: 600; margin: 16px 0 12px 0; }
 .trend-chart { height: 200px; }
 .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
-</style>
+</style>

+ 36 - 11
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -190,9 +190,14 @@
         </div>
 
         <!-- 处理操作 -->
-        <div class="action-section" v-if="currentAlert.status !== 'resolved'">
+        <div class="action-section" v-if="currentAlert.status === 'pending'">
           <el-button type="primary" @click="resolveAlert">标记已处理</el-button>
-          <el-button type="warning" @click="dispatchWorkOrder">派发现场工单</el-button>
+          <el-button
+            type="warning"
+            :loading="dispatching"
+            :disabled="currentAlert.hasWorkOrder"
+            @click="dispatchWorkOrder"
+          >{{ currentAlert.hasWorkOrder ? `已关联工单${currentAlert.workOrderStatusText ? `(${currentAlert.workOrderStatusText})` : ''}` : '派发现场工单' }}</el-button>
         </div>
       </div>
     </el-drawer>
@@ -215,18 +220,25 @@ const historyPage = ref(1)
 const historyPageSize = 8
 const historyTotal = ref(0)
 const alarmStats = ref({ realtimeCount: 0, pendingCount: 0, maxConcentration: 0, totalStations: 0 })
+const dispatching = ref(false)
 
 async function loadEquipment() {
   try {
     const res = await getGasAlarmEquipment()
     equipments.value = (res.data || []).map(e => ({
       ...e,
+      id: e.latestAlarmId || null,
+      stationCode: e.equipmentCode,
       stationName: e.equipmentName,
       concentration: e.latestConcentration,
       threshold: e.latestThreshold || 20,
       alertTime: e.latestAlarmTime,
       level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
-      status: e.hasAlarm ? 'pending' : 'normal'
+      status: e.hasAlarm ? 'pending' : 'normal',
+      hasWorkOrder: Boolean(e.latestHasWorkOrder),
+      workOrderId: e.latestWorkOrderId || null,
+      workOrderStatus: e.latestWorkOrderStatus ?? null,
+      workOrderStatusText: e.latestWorkOrderStatusText || ''
     }))
   } catch (e) { console.error(e) }
 }
@@ -258,7 +270,9 @@ function mapAlarm(a) {
     level: lv, alertTime: a.alarmTime || '', status,
     resolveTime: a.resolveTime || a.handleTime || null,
     handler: a.handler || a.handleUser || null,
-    remark: a.remark || a.handleRemark || null
+       remark: a.remark || a.handleRemark || null,
+       hasWorkOrder: Boolean(a.hasWorkOrder), workOrderId: a.workOrderId || null,
+       workOrderStatus: a.workOrderStatus ?? null, workOrderStatusText: a.workOrderStatusText || ''
   }
 }
 
@@ -327,11 +341,23 @@ const resolveAlert = async () => {
 }
 
 const dispatchWorkOrder = async () => {
-  if (currentAlert.value?.id) {
-    try {
-      await dispatchGasAlarm(currentAlert.value.id)
-      ElMessage.success('已派发现场处置工单(伪)')
-    } catch (e) { ElMessage.error('派发失败') }
+  if (!currentAlert.value?.id || currentAlert.value.hasWorkOrder || dispatching.value) return
+  dispatching.value = true
+  try {
+    const res = await dispatchGasAlarm(currentAlert.value.id, {
+      orderDesc: `${currentAlert.value.stationName || '可燃气体'}浓度报警,请现场核实处理`
+    })
+    currentAlert.value.hasWorkOrder = true
+    currentAlert.value.workOrderId = res.data || null
+    currentAlert.value.workOrderStatusText = '待派单'
+    currentAlert.value.status = 'resolved'
+    currentAlert.value.resolveTime = new Date().toLocaleString()
+    ElMessage.success(res.msg || '报警已转为待派单工单')
+    await Promise.all([loadEquipment(), loadHistory(), loadStats()])
+  } catch (e) {
+    ElMessage.error(e?.message || '工单派发失败')
+  } finally {
+    dispatching.value = false
   }
 }
 
@@ -340,7 +366,6 @@ async function loadStats() {
     const res = await getGasAlarmStats()
     if (res.data) {
       alarmStats.value = res.data
-      totalStations.value = res.data.totalStations || 0
     }
   } catch (e) { console.error(e) }
 }
@@ -478,4 +503,4 @@ onBeforeUnmount(() => {
 .section-title { font-weight: 600; margin: 16px 0 12px 0; }
 .trend-chart { height: 200px; }
 .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
-</style>
+</style>

+ 161 - 101
src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue

@@ -62,7 +62,7 @@
             <el-radio-button label="warning">预警</el-radio-button>
           </el-radio-group>
         </div>
-        <div class="alert-list">
+        <div class="alert-list" v-loading="loading">
           <div
               v-for="alert in paginatedAlerts"
               :key="alert.id"
@@ -165,9 +165,24 @@
         <div class="action-section">
           <div class="section-title">研判处置</div>
           <div class="action-buttons">
-            <el-button type="primary" @click="confirmTrueAlarm">确认为真报警</el-button>
-            <el-button type="info" @click="confirmFalseAlarm">排除误报警</el-button>
-            <el-button type="warning" @click="dispatchWorkOrder">派发工单</el-button>
+            <el-button
+              type="primary"
+              :loading="resolving"
+              :disabled="resolving || selectedAlert.status === 'resolved'"
+              @click="confirmTrueAlarm"
+            >确认为真报警</el-button>
+            <el-button
+              type="info"
+              :loading="resolving"
+              :disabled="resolving || selectedAlert.status === 'resolved'"
+              @click="confirmFalseAlarm"
+            >排除误报警</el-button>
+            <el-button
+              type="warning"
+              :loading="dispatching"
+              :disabled="dispatching || resolving || selectedAlert.status === 'resolved' || selectedAlert.hasWorkOrder"
+              @click="dispatchWorkOrder"
+            >{{ workOrderButtonText }}</el-button>
           </div>
           <el-input
               v-model="judgmentRemark"
@@ -201,59 +216,83 @@
 import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Search, Location, WarningFilled, InfoFilled, DataAnalysis, Clock, WindPower, Document } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
-import { ElMessage, ElNotification } from 'element-plus'
-
-// 沅陵县相邻地下空间可燃气体实时报警数据
-const realtimeAlerts = ref([
-  { id: 1, pointCode: 'CH4-YL-001', pointName: '辰州北路综合管廊监测点', level: 'critical',
-    address: '沅陵县太常片区辰州北路综合管廊B2层', concentration: 15.8, threshold: 10,
-    alertTime: '2026-06-13 15:23:45', status: 'pending',
-    trendData: [2.1, 2.5, 3.2, 4.0, 5.1, 6.5, 8.0, 9.8, 11.2, 12.5, 13.8, 14.5, 15.2, 15.8, 15.8, 15.5, 15.0, 14.2, 13.5, 12.8, 12.0, 11.2, 10.5, 9.8],
-    historyRecords: [] },
-  { id: 2, pointCode: 'CH4-YL-002', pointName: '古城中路地下管沟监测点', level: 'critical',
-    address: '沅陵县太常片区古城中路电力管沟检查井', concentration: 12.3, threshold: 10,
-    alertTime: '2026-06-13 14:58:12', status: 'pending',
-    trendData: [1.5, 2.2, 3.0, 4.1, 5.5, 7.0, 8.5, 9.2, 10.0, 10.8, 11.5, 12.0, 12.3, 12.3, 12.0, 11.5, 11.0, 10.2, 9.5, 8.8, 8.0, 7.2, 6.5, 5.8],
-    historyRecords: [] },
-  { id: 3, pointCode: 'CH4-YL-003', pointName: '建设路雨水管廊监测点', level: 'warning',
-    address: '沅陵县太常片区建设路雨水管廊检查井', concentration: 6.8, threshold: 10,
-    alertTime: '2026-06-13 14:32:05', status: 'pending',
-    trendData: [2.0, 2.5, 3.2, 4.0, 4.8, 5.5, 6.2, 6.8, 6.8, 6.5, 6.2, 5.8, 5.5, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2, 3.0, 2.8],
-    historyRecords: [] },
-  { id: 4, pointCode: 'CH4-YL-004', pointName: '天宁西路地下车库监测点', level: 'warning',
-    address: '沅陵县太常片区天宁西路商业地下停车场', concentration: 7.5, threshold: 10,
-    alertTime: '2026-06-13 14:15:30', status: 'pending',
-    trendData: [3.0, 3.5, 4.2, 5.0, 5.8, 6.5, 7.0, 7.5, 7.5, 7.2, 6.8, 6.5, 6.2, 6.0, 5.8, 5.5, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5],
-    historyRecords: [] },
-  { id: 5, pointCode: 'CH4-YL-005', pointName: '滨江东路河堤管廊监测点', level: 'critical',
-    address: '沅陵县太常片区滨江东路河堤综合管廊', concentration: 11.5, threshold: 10,
-    alertTime: '2026-06-13 13:50:00', status: 'pending',
-    trendData: [4.0, 4.5, 5.2, 6.0, 7.0, 8.0, 9.0, 9.8, 10.5, 11.0, 11.5, 11.5, 11.2, 10.8, 10.5, 10.0, 9.5, 9.0, 8.5, 8.0, 7.5, 7.0, 6.5, 6.0],
-    historyRecords: [] },
-  { id: 6, pointCode: 'CH4-YL-006', pointName: '迎宾东路地下通道监测点', level: 'warning',
-    address: '沅陵县太常片区迎宾东路地下人行通道', concentration: 5.2, threshold: 10,
-    alertTime: '2026-06-13 13:30:00', status: 'pending',
-    trendData: [1.2, 1.8, 2.5, 3.2, 4.0, 4.5, 5.0, 5.2, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2, 3.0, 2.8, 2.5, 2.2, 2.0, 1.8, 1.5],
-    historyRecords: [] },
-  { id: 7, pointCode: 'CH4-YL-007', pointName: '古城北路综合管沟监测点', level: 'warning',
-    address: '沅陵县太常片区古城北路综合管沟入口', concentration: 8.2, threshold: 10,
-    alertTime: '2026-06-13 12:10:00', status: 'pending',
-    trendData: [3.5, 4.0, 4.8, 5.5, 6.2, 7.0, 7.5, 8.0, 8.2, 8.2, 8.0, 7.5, 7.0, 6.5, 6.0, 5.5, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2],
-    historyRecords: [] },
-  { id: 8, pointCode: 'CH4-YL-008', pointName: '建设东路工业园管廊监测点', level: 'critical',
-    address: '沅陵县太常片区建设东路工业园综合管廊', concentration: 14.2, threshold: 10,
-    alertTime: '2026-06-13 11:55:00', status: 'pending',
-    trendData: [5.0, 5.8, 6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.2, 13.8, 14.2, 14.2, 13.8, 13.2, 12.5, 11.8, 11.0, 10.2, 9.5, 8.8, 8.0, 7.2, 6.5],
-    historyRecords: [] }
-])
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import { getGasAlarmHistory, resolveGasAlarm, dispatchGasAlarm } from '@/api/pipeNetwork/basic'
+
+// 相邻地下空间报警和场站探测器统一来自 alarm_data;按位置描述筛出地下空间点位。
+const realtimeAlerts = ref([])
+const loading = ref(false)
+const resolving = ref(false)
+const dispatching = ref(false)
+const adjacentKeywords = ['地下', '管廊', '管沟', '车库', '通道', '综合管线', '检查井', '地下空间', '隧道']
+
+function isAdjacentAlarm(alarm) {
+  const text = `${alarm.stationName || ''} ${alarm.address || ''} ${alarm.deviceName || ''} ${alarm.deviceCode || ''}`
+  return adjacentKeywords.some(keyword => text.includes(keyword))
+}
+
+function mapAlarm(alarm) {
+  const concentration = Number(alarm.concentration ?? alarm.actualValue ?? 0)
+  const threshold = Number(alarm.threshold ?? alarm.maxValue ?? 20)
+  const level = Number(alarm.alarmLevel) === 1 ? 'critical' : 'warning'
+  return {
+    id: alarm.id,
+    pointCode: alarm.deviceCode,
+    pointName: alarm.stationName || alarm.deviceName || alarm.deviceCode || '地下空间监测点',
+    address: alarm.address || '',
+    level,
+    concentration,
+    threshold,
+    alertTime: alarm.alarmTime || '',
+    status: Number(alarm.alarmStatus) === 0 ? 'pending' : 'resolved',
+    resolveTime: alarm.resolveTime || null,
+    handler: alarm.handler || null,
+    remark: alarm.remark || alarm.handleRemark || null,
+    trendData: Array(24).fill(concentration),
+    historyRecords: [],
+    hasWorkOrder: Boolean(alarm.hasWorkOrder),
+    workOrderId: alarm.workOrderId || null,
+    workOrderStatus: alarm.workOrderStatus ?? null,
+    workOrderStatusText: alarm.workOrderStatusText || ''
+  }
+}
+
+async function loadAlerts() {
+  loading.value = true
+  const selectedId = selectedAlert.value?.id
+  try {
+    const res = await getGasAlarmHistory(1, 200, '', '', '')
+    const records = res.data?.records || []
+    const pending = records.filter(item => Number(item.alarmStatus) === 0)
+    const adjacent = pending.filter(isAdjacentAlarm)
+    // 设备基础资料尚未补充位置描述时,保留待处理报警,避免页面无数据且无法派单。
+    realtimeAlerts.value = (adjacent.length ? adjacent : pending).map(mapAlarm)
+    selectedAlert.value = realtimeAlerts.value.find(item => item.id === selectedId)
+      || realtimeAlerts.value[0]
+      || null
+    return true
+  } catch (error) {
+    console.error('加载相邻地下空间报警失败', error)
+    realtimeAlerts.value = []
+    selectedAlert.value = null
+    return false
+  } finally {
+    loading.value = false
+  }
+}
 
 // 统计数据
-const totalPoints = computed(() => 12)
+const totalPoints = computed(() => new Set(realtimeAlerts.value.map(alert => alert.pointCode).filter(Boolean)).size)
 const warningCount = computed(() => realtimeAlerts.value.filter(a => a.level === 'warning').length)
 const todayNewCount = 2
-const maxConcentration = computed(() => Math.max(...realtimeAlerts.value.map(a => a.concentration)).toFixed(1))
-const maxConcentrationPoint = computed(() => realtimeAlerts.value.find(a => a.concentration === parseFloat(maxConcentration.value))?.pointName || '—')
-const avgConcentration = computed(() => (realtimeAlerts.value.reduce((sum, a) => sum + a.concentration, 0) / realtimeAlerts.value.length).toFixed(1))
+const maxConcentration = computed(() => realtimeAlerts.value.length ? Math.max(...realtimeAlerts.value.map(a => a.concentration)).toFixed(1) : '0.0')
+const maxConcentrationPoint = computed(() => {
+  const max = Number(maxConcentration.value)
+  return realtimeAlerts.value.find(a => a.concentration === max)?.pointName || '—'
+})
+const avgConcentration = computed(() => realtimeAlerts.value.length
+  ? (realtimeAlerts.value.reduce((sum, a) => sum + a.concentration, 0) / realtimeAlerts.value.length).toFixed(1)
+  : '0.0')
 const avgTrend = 0.6
 const falseAlarmRate = computed(() => 3.8)
 
@@ -262,8 +301,12 @@ const searchKeyword = ref('')
 const alertFilter = ref('all')
 const currentPage = ref(1)
 const pageSize = 3
-const selectedAlert = ref(realtimeAlerts.value[0])
+const selectedAlert = ref(null)
 const judgmentRemark = ref('')
+const workOrderButtonText = computed(() => {
+  if (!selectedAlert.value?.hasWorkOrder) return '派发工单'
+  return `已关联工单${selectedAlert.value.workOrderStatusText ? `(${selectedAlert.value.workOrderStatusText})` : ''}`
+})
 
 const filteredAlerts = computed(() => {
   let list = realtimeAlerts.value
@@ -336,67 +379,84 @@ const selectAlert = (alert) => {
   selectedAlert.value = alert
 }
 
-const confirmTrueAlarm = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  const index = realtimeAlerts.value.findIndex(a => a.id === selectedAlert.value.id)
-  if (index !== -1) {
-    const resolvedAlert = { ...selectedAlert.value, status: 'resolved' }
-    realtimeAlerts.value.splice(index, 1)
-    resolvedAlert.historyRecords = resolvedAlert.historyRecords || []
-    resolvedAlert.historyRecords.unshift({
-      id: Date.now(),
-      time: new Date().toLocaleString(),
-      type: 'danger',
-      content: `综合研判:确认为真报警,处置说明:${judgmentRemark.value || '已安排人员现场排查'}`
-    })
-    ElNotification({
-      title: '报警确认',
-      message: `已确认真报警,工单已派发至${selectedAlert.value.pointName}所属维护单位`,
-      type: 'warning'
-    })
-    ElMessage.success('已确认真报警,处置流程已启动')
-  }
-  judgmentRemark.value = ''
-  if (realtimeAlerts.value.length > 0) {
-    selectedAlert.value = realtimeAlerts.value[0]
-  } else {
-    selectedAlert.value = null
+const resolveSelectedAlarm = async (kind) => {
+  const alert = selectedAlert.value
+  if (!alert?.id || alert.status === 'resolved' || resolving.value) return
+  const isFalseAlarm = kind === 'false'
+  try {
+    await ElMessageBox.confirm(
+      isFalseAlarm ? '确认将该报警标记为误报警吗?' : '确认该报警为真报警并标记为已处理吗?',
+      isFalseAlarm ? '排除误报警' : '报警确认',
+      { type: isFalseAlarm ? 'info' : 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }
+    )
+  } catch {
+    return
   }
-}
 
-const confirmFalseAlarm = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  const index = realtimeAlerts.value.findIndex(a => a.id === selectedAlert.value.id)
-  if (index !== -1) {
-    realtimeAlerts.value.splice(index, 1)
+  resolving.value = true
+  try {
+    const remark = judgmentRemark.value.trim()
+    await resolveGasAlarm(alert.id, remark || (isFalseAlarm ? '经核实为误报警' : '确认为真报警'))
     ElNotification({
-      title: '误报警排除',
-      message: `已标记为误报警,排除说明:${judgmentRemark.value || '经核实为传感器干扰导致'}`,
-      type: 'info'
+      title: isFalseAlarm ? '误报警排除' : '报警确认',
+      message: isFalseAlarm ? '报警已标记为误报警并从实时列表移除' : '真报警已确认并标记为已处理',
+      type: isFalseAlarm ? 'info' : 'warning'
     })
-    ElMessage.info('已排除误报警')
-  }
-  judgmentRemark.value = ''
-  if (realtimeAlerts.value.length > 0) {
-    selectedAlert.value = realtimeAlerts.value[0]
-  } else {
-    selectedAlert.value = null
+    ElMessage.success(isFalseAlarm ? '已排除误报警' : '已确认真报警')
+    await loadAlerts()
+  } catch (error) {
+    ElMessage.error(error?.message || (isFalseAlarm ? '排除误报警失败' : '确认报警失败'))
+  } finally {
+    resolving.value = false
+    judgmentRemark.value = ''
   }
 }
 
-const dispatchWorkOrder = () => {
-  ElMessage.success(`已向${selectedAlert.value.pointName}所属单位派发处置工单`)
+const confirmTrueAlarm = () => resolveSelectedAlarm('true')
+const confirmFalseAlarm = () => resolveSelectedAlarm('false')
+
+const dispatchWorkOrder = async () => {
+  const alert = selectedAlert.value
+  if (!alert?.id || alert.hasWorkOrder || alert.status === 'resolved' || dispatching.value) return
+  try {
+    await ElMessageBox.confirm(
+      `确认将“${alert.pointName}”的相邻地下空间可燃气体报警转为工单吗?`,
+      '派发工单',
+      { type: 'warning', confirmButtonText: '确认派发', cancelButtonText: '取消' }
+    )
+  } catch {
+    return
+  }
+
+  dispatching.value = true
+  try {
+    const res = await dispatchGasAlarm(alert.id, {
+      orderLevel: alert.level === 'critical' ? 0 : 1,
+      orderDesc: `${alert.pointName || '相邻地下空间'}可燃气体浓度报警,请现场核实处理`
+    })
+    alert.hasWorkOrder = true
+    alert.workOrderId = res.data || null
+    alert.workOrderStatusText = '待派单'
+    ElMessage.success(res.msg || '报警已转为待派单工单')
+    await loadAlerts()
+  } catch (error) {
+    ElMessage.error(error?.message || '工单派发失败')
+  } finally {
+    dispatching.value = false
+  }
 }
 
-const refreshData = () => {
-  ElMessage.success('数据已刷新')
+const refreshData = async () => {
+  const ok = await loadAlerts()
+  if (ok) ElMessage.success('数据已刷新')
 }
 
 const exportReport = () => {
   ElMessage.success('导出报警报表(演示)')
 }
 
-onMounted(() => {
+onMounted(async () => {
+  await loadAlerts()
   nextTick(() => renderChart())
 })
 
@@ -548,4 +608,4 @@ onBeforeUnmount(() => {
 
 .action-buttons { display: flex; gap: 12px; margin-top: 8px; }
 .history-section { margin-top: 20px; }
-</style>
+</style>

+ 152 - 83
src/views/subSystem/hidden/HazardList.vue

@@ -24,14 +24,10 @@
           </el-select>
         </el-form-item>
         <el-form-item label="关联管网">
-          <el-select v-model="searchForm.networkId" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="n in networkOptions" :key="n.networkId" :label="n.networkName" :value="n.networkId" />
-          </el-select>
+          <el-select-v2 v-model="searchForm.networkId" placeholder="请选择" clearable filterable style="width:160px" :options="networkSelectOptions" @focus="loadNetworkOptions" />
         </el-form-item>
         <el-form-item label="关联设备">
-          <el-select v-model="searchForm.equipmentId" placeholder="请选择" clearable style="width:160px">
-            <el-option v-for="e in equipmentOptions" :key="e.equipmentId" :label="e.equipmentName" :value="e.equipmentId" />
-          </el-select>
+          <el-select-v2 v-model="searchForm.equipmentId" placeholder="请选择" clearable filterable style="width:160px" :options="equipmentSelectOptions" @focus="loadEquipmentOptions" />
         </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleSearch">查询</el-button>
@@ -63,11 +59,12 @@
         </el-table-column>
         <el-table-column label="隐患图片" width="90">
           <template #default="{ row }">
-            <template v-if="parseImageUrls(row.hazardImages).length">
+            <template v-if="getRowImageUrls(row).length">
               <el-image
-                :src="parseImageUrls(row.hazardImages)[0]"
-                fit="cover"
-                :preview-src-list="parseImageUrls(row.hazardImages)"
+                :src="getRowImageUrls(row)[0]"
+                fit="contain"
+                lazy
+                :preview-src-list="getRowImageUrls(row)"
                 preview-teleported
                 hide-on-click-modal
                 class="table-image"
@@ -84,13 +81,10 @@
         </el-table-column>
         <el-table-column prop="reportUnit" label="上报单位" width="120" />
         <el-table-column prop="reportTime" label="上报时间" width="160" />
-        <el-table-column label="操作" width="350" fixed="right">
+        <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
             <el-button type="info" size="small" @click="handleDetail(row)">详情</el-button>
-            <el-button type="success" size="small" @click="handleSubmitHazard(row)" :disabled="row.isSubmitted === 1">
-              {{ row.isSubmitted === 1 ? '已报送' : '报送并生成工单' }}
-            </el-button>
             <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
@@ -134,16 +128,12 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="关联管网">
-              <el-select v-model="formData.networkId" placeholder="请选择" clearable style="width: 100%">
-                <el-option v-for="n in networkOptions" :key="n.networkId" :label="n.networkName" :value="n.networkId" />
-              </el-select>
+              <el-select-v2 v-model="formData.networkId" placeholder="请选择" clearable filterable style="width: 100%" :options="networkSelectOptions" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="关联设备" prop="equipmentId">
-              <el-select v-model="formData.equipmentId" placeholder="请选择" clearable style="width: 100%">
-                <el-option v-for="e in equipmentOptions" :key="e.equipmentId" :label="e.equipmentName" :value="e.equipmentId" />
-              </el-select>
+              <el-select-v2 v-model="formData.equipmentId" placeholder="请选择" clearable filterable style="width: 100%" :options="equipmentSelectOptions" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -187,27 +177,29 @@
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="隐患图片">
-              <el-upload
-                action="#"
-                :auto-upload="false"
-                :show-file-list="false"
-                :http-request="() => {}"
-                :on-change="handleImageChange"
-                accept="image/jpeg,image/png,image/jpg"
-                multiple
-              >
-                <el-button type="primary" plain :loading="imageUploading">
-                  {{ imageUploading ? '上传中...' : '选择图片' }}
-                </el-button>
-                <template #tip>
-                  <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB,可多选</div>
-                </template>
-              </el-upload>
-              <div v-if="imageList.length" class="image-list">
-                <div v-for="(img, index) in imageList" :key="index" class="image-item">
-                  <img :src="img" class="image-thumb" @click="previewImage(index)" />
-                  <el-icon class="image-remove" @click="removeImage(index)"><Close /></el-icon>
+            <el-form-item label="隐患图片" class="image-form-item">
+              <div class="image-upload-content">
+                <el-upload
+                  action="#"
+                  :auto-upload="false"
+                  :show-file-list="false"
+                  :http-request="() => {}"
+                  :on-change="handleImageChange"
+                  accept="image/jpeg,image/png,image/jpg"
+                  multiple
+                >
+                  <el-button type="primary" plain :loading="imageUploading">
+                    {{ imageUploading ? '上传中...' : '选择图片' }}
+                  </el-button>
+                  <template #tip>
+                    <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB,可多选</div>
+                  </template>
+                </el-upload>
+                <div v-if="imageList.length" class="image-list">
+                  <div v-for="(img, index) in imageList" :key="index" class="image-item">
+                    <img :src="img" class="image-thumb" @click="previewImage(index)" />
+                    <el-icon class="image-remove" @click="removeImage(index)"><Close /></el-icon>
+                  </div>
                 </div>
               </div>
             </el-form-item>
@@ -246,13 +238,14 @@
         <el-descriptions-item label="经度">{{ detailData.longitude || '-' }}</el-descriptions-item>
         <el-descriptions-item label="纬度">{{ detailData.latitude || '-' }}</el-descriptions-item>
         <el-descriptions-item label="隐患图片" :span="2">
-          <div v-if="parseImageUrls(detailData.hazardImages).length" class="detail-image-list">
+          <div v-if="detailImageUrls.length" class="detail-image-list">
             <el-image
-              v-for="(img, index) in parseImageUrls(detailData.hazardImages)"
+              v-for="(img, index) in detailImageUrls"
               :key="index"
               :src="img"
-              fit="cover"
-              :preview-src-list="parseImageUrls(detailData.hazardImages)"
+              fit="contain"
+              lazy
+              :preview-src-list="detailImageUrls"
               :initial-index="index"
               preview-teleported
               hide-on-click-modal
@@ -277,11 +270,11 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
+import { computed, ref, reactive, shallowRef, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Close } from '@element-plus/icons-vue'
 import request from '@/utils/request'
-import { getHazardPage, getHazardById, addHazard, updateHazard, deleteHazard, submitHazard } from '@/api/pipeNetwork/hazard'
+import { getHazardPage, getHazardById, addHazard, updateHazard, deleteHazard } from '@/api/pipeNetwork/hazard'
 import { getDicts } from '@/api/system/dict/data'
 import { getPipeNetworkOptions, getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
 
@@ -317,12 +310,21 @@ const hazardTypeDict = ref([])
 const hazardLevelDict = ref([])
 const hazardStatusDict = ref([])
 const hazardSourceDict = ref([])
-const networkOptions = ref([])
-const equipmentOptions = ref([])
+// 选项列表可能较大,使用浅层响应式并缓存请求,避免每次编辑都触发整棵数组深度更新。
+const networkOptions = shallowRef([])
+const equipmentOptions = shallowRef([])
+const networkNameCache = shallowRef(new Map())
+const equipmentNameCache = shallowRef(new Map())
+const rowImageCache = new WeakMap()
+const networkSelectOptions = computed(() => networkOptions.value.map(item => ({ value: item.networkId, label: item.networkName })))
+const equipmentSelectOptions = computed(() => equipmentOptions.value.map(item => ({ value: item.equipmentId, label: item.equipmentName })))
+let networkOptionsRequest = null
+let equipmentOptionsRequest = null
 
 // 详情相关
 const detailVisible = ref(false)
 const detailData = ref(null)
+const detailImageUrls = computed(() => parseImageUrls(detailData.value?.hazardImages))
 
 // 图片相关
 const imageList = ref([])
@@ -349,7 +351,46 @@ function statusTag(value) {
 /** 将存储的 hazardImages 字符串转为 URL 数组 */
 function parseImageUrls(hazardImages) {
   if (!hazardImages) return []
-  return hazardImages.split(',').map(s => s.trim()).filter(Boolean)
+  let values = hazardImages
+  if (typeof values === 'string') {
+    const text = values.trim()
+    if (text.startsWith('[') || text.startsWith('{')) {
+      try {
+        values = JSON.parse(text)
+      } catch (e) {
+        values = text.split(',')
+      }
+    } else {
+      values = text.split(/[,;\n]/)
+    }
+  }
+  if (!Array.isArray(values)) values = [values]
+  return values
+    .map(item => {
+      const source = typeof item === 'object' ? (item.url || item.fileName || item.name) : item
+      return normalizeImageUrl(source)
+    })
+    .filter(Boolean)
+}
+
+function getRowImageUrls(row) {
+  if (!row || typeof row !== 'object') return []
+  if (!rowImageCache.has(row)) rowImageCache.set(row, parseImageUrls(row.hazardImages))
+  return rowImageCache.get(row)
+}
+
+/** 兼容 MinIO 完整地址、upload 相对路径和历史仅文件名数据 */
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/i.test(url)) return url
+  const minioBase = String(import.meta.env.VITE_APP_MINIO_BASE_URL || '').trim().replace(/\/+$/, '')
+  if (minioBase) {
+    if (url.startsWith('/')) return `${minioBase}${url}`
+    if (url.startsWith('upload/')) return `${minioBase}/${url}`
+    if (!url.includes('/')) return `${minioBase}/upload/${url}`
+  }
+  const baseApi = String(import.meta.env.VITE_APP_BASE_API || '').trim().replace(/\/+$/, '')
+  return url.startsWith('/') ? `${baseApi}${url}` : `${baseApi}/${url}`
 }
 
 /** 图片选择后立即上传到 MinIO */
@@ -369,7 +410,8 @@ async function handleImageChange(file) {
       method: 'post',
       data: uploadFormData
     })
-    const url = uploadResponse.url || uploadResponse.fileName
+    const url = normalizeImageUrl(uploadResponse.url || uploadResponse.fileName)
+    if (!url) throw new Error('上传接口未返回图片地址')
     imageUrls.value.push(url)
     imageList.value.push(url)
   } catch (e) {
@@ -419,20 +461,36 @@ function handleReset() {
   handleSearch()
 }
 async function loadNetworkOptions() {
-  if (networkOptions.value.length > 0) return
-  try { const res = await getPipeNetworkOptions(); networkOptions.value = res.data || [] } catch (e) { console.error(e) }
+  if (Array.isArray(networkOptions.value) && networkOptions.value.length > 0) return
+  if (networkOptionsRequest) return networkOptionsRequest
+  networkOptionsRequest = getPipeNetworkOptions()
+    .then(res => {
+      networkOptions.value = Array.isArray(res.data) ? res.data : []
+      networkNameCache.value = new Map(networkOptions.value.map(item => [String(item.networkId), item.networkName]))
+      return networkOptions.value
+    })
+    .catch(e => { console.error(e); return [] })
+    .finally(() => { networkOptionsRequest = null })
+  return networkOptionsRequest
 }
 async function loadEquipmentOptions() {
-  if (equipmentOptions.value.length > 0) return
-  try { const res = await getEquipmentByTopLevelType('燃气'); equipmentOptions.value = res.data || [] } catch (e) { console.error(e) }
+  if (Array.isArray(equipmentOptions.value) && equipmentOptions.value.length > 0) return
+  if (equipmentOptionsRequest) return equipmentOptionsRequest
+  equipmentOptionsRequest = getEquipmentByTopLevelType('燃气')
+    .then(res => {
+      equipmentOptions.value = Array.isArray(res.data) ? res.data : []
+      equipmentNameCache.value = new Map(equipmentOptions.value.map(item => [String(item.equipmentId), item.equipmentName]))
+      return equipmentOptions.value
+    })
+    .catch(e => { console.error(e); return [] })
+    .finally(() => { equipmentOptionsRequest = null })
+  return equipmentOptionsRequest
 }
 function getNetworkName(id) {
-  const found = networkOptions.value.find(n => n.networkId === id)
-  return found ? found.networkName : id || '-'
+  return networkNameCache.value.get(String(id)) || id || '-'
 }
 function getEquipmentName(id) {
-  const found = equipmentOptions.value.find(e => e.equipmentId === id)
-  return found ? found.equipmentName : id || '-'
+  return equipmentNameCache.value.get(String(id)) || id || '-'
 }
 /** 自动生成隐患编号:YH-YYYYMMDDHHmmss */
 function generateHazardNo() {
@@ -452,13 +510,31 @@ function handleAdd() {
 }
 function handleEdit(row) {
   dialogTitle.value = '编辑'
-  Object.assign(formData, row)
+  // 只回填表单字段,避免把接口返回的分页/审计字段直接写入响应式表单。
+  // 直接 Object.assign(row) 在部分数据返回结构下会导致弹窗渲染持续触发更新。
+  const editableFields = [
+    'hazardId', 'hazardNo', 'hazardType', 'hazardLevel', 'hazardDesc',
+    'hazardSource', 'hazardStatus', 'reportUnit', 'reportTime',
+    'rectifyPlan', 'rectifyDeadline', 'rectifyPerson', 'networkId',
+    'equipmentId', 'longitude', 'latitude', 'hazardImages'
+  ]
+  editableFields.forEach(key => {
+    const value = row?.[key]
+    if (key === 'longitude' || key === 'latitude') {
+      formData[key] = value === null || value === undefined || value === '' ? null : Number(value)
+    } else if (key === 'hazardImages') {
+      formData[key] = parseImageUrls(value).join(',')
+    } else {
+      formData[key] = value ?? ''
+    }
+  })
   // 回显已有图片
-  const urls = parseImageUrls(row.hazardImages)
+  const urls = getRowImageUrls(row)
   imageUrls.value = [...urls]
   imageList.value = [...urls]
   dialogVisible.value = true
-  loadNetworkOptions(); loadEquipmentOptions()
+  // 选项加载不阻塞弹窗打开,且函数内部会避免重复请求。
+  void Promise.allSettled([loadNetworkOptions(), loadEquipmentOptions()])
 }
 async function handleSave() {
   if (!formRef.value) return
@@ -480,24 +556,6 @@ function handleDelete(row) {
     await deleteHazard(row.hazardId); ElMessage.success('删除成功'); loadData()
   }).catch(() => {})
 }
-/** 报送(同时自动生成工单) */
-async function handleSubmitHazard(row) {
-  ElMessageBox.confirm('确认报送该隐患?报送后将自动创建工单进入待派单状态。', '提示', { type: 'warning' }).then(async () => {
-    try {
-      const res = await submitHazard(row.hazardId)
-      if (res.code === 200) {
-        const msg = res.data ? '报送成功,已自动创建工单' : '报送成功'
-        ElMessage.success(msg)
-        loadData()
-      } else {
-        ElMessage.error(res.msg || '报送失败')
-      }
-    } catch (e) {
-      ElMessage.error(e?.response?.data?.msg || e?.message || '报送失败')
-    }
-  }).catch(() => {})
-}
-
 async function handleDetail(row) {
   try {
     const res = await getHazardById(row.hazardId)
@@ -510,7 +568,10 @@ async function handleDetail(row) {
   }
 }
 
-onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loadEquipmentOptions() })
+onMounted(async () => {
+  await loadDicts()
+  void loadData()
+})
 </script>
 
 <style scoped>
@@ -532,6 +593,12 @@ onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loa
   gap: 8px;
   margin-top: 8px;
 }
+.image-form-item :deep(.el-form-item__content) {
+  display: block;
+}
+.image-upload-content {
+  width: 100%;
+}
 .image-item {
   position: relative;
   width: 80px;
@@ -542,7 +609,8 @@ onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loa
 .image-thumb {
   width: 80px;
   height: 80px;
-  object-fit: cover;
+  object-fit: contain;
+  background: #f5f7fa;
   border-radius: 4px;
   border: 1px solid #dcdfe6;
   cursor: pointer;
@@ -581,6 +649,7 @@ onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loa
   height: 120px;
   border-radius: 6px;
   cursor: pointer;
-  object-fit: cover;
+  object-fit: contain;
+  background: #f5f7fa;
 }
 </style>

+ 578 - 0
src/views/subSystem/hidden/HazardOperationPage.vue

@@ -0,0 +1,578 @@
+<template>
+  <div class="workflow-page">
+    <el-card class="search-card" shadow="never">
+      <el-form :inline="true" :model="queryParams" class="search-form">
+        <el-form-item label="关键词">
+          <el-input
+            v-model="queryParams.keyword"
+            clearable
+            placeholder="隐患编号/描述/上报单位"
+            style="width: 220px"
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item label="隐患类型">
+          <el-select v-model="queryParams.hazardType" clearable placeholder="全部" style="width: 140px">
+            <el-option v-for="item in hazardTypeDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="隐患等级">
+          <el-select v-model="queryParams.hazardLevel" clearable placeholder="全部" style="width: 130px">
+            <el-option v-for="item in hazardLevelDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="隐患来源">
+          <el-select v-model="queryParams.hazardSource" clearable placeholder="全部" style="width: 150px">
+            <el-option v-for="item in hazardSourceDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="报送状态">
+          <el-select v-model="queryParams.isSubmitted" clearable placeholder="全部" style="width: 120px">
+            <el-option label="未报送" :value="0" />
+            <el-option label="已报送" :value="1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="mode !== 'submission'" label="治理状态">
+          <el-select v-model="queryParams.hazardStatus" clearable placeholder="全部" style="width: 130px">
+            <el-option v-for="item in hazardStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="mode === 'submission'" label="时间类型">
+          <el-select v-model="queryParams.timeType" style="width: 140px">
+            <el-option label="隐患上报时间" value="report" />
+            <el-option label="报送时间" value="submit" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            value-format="YYYY-MM-DD"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            style="width: 250px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card class="table-card" shadow="never">
+      <el-table v-loading="loading" :data="tableData" border stripe>
+        <el-table-column prop="hazardNo" label="隐患编号" width="155" fixed="left" />
+        <el-table-column label="隐患类型" width="115">
+          <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
+        </el-table-column>
+        <el-table-column label="隐患等级" width="105">
+          <template #default="{ row }">
+            <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="hazardDesc" label="隐患描述" min-width="220" show-overflow-tooltip />
+        <el-table-column label="隐患来源" width="125">
+          <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="reportUnit" label="上报单位" width="150" show-overflow-tooltip />
+        <el-table-column prop="reportTime" label="上报时间" width="165" />
+
+        <template v-if="mode === 'submission'">
+          <el-table-column label="报送状态" width="100">
+            <template #default="{ row }">
+              <el-tag :type="isSubmitted(row) ? 'success' : 'info'">{{ isSubmitted(row) ? '已报送' : '未报送' }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="submitTime" label="报送时间" width="165">
+            <template #default="{ row }">{{ row.submitTime || '-' }}</template>
+          </el-table-column>
+        </template>
+
+        <template v-else>
+          <el-table-column label="治理状态" width="110">
+            <template #default="{ row }">
+              <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="rectifyPerson" label="整改责任人" width="120">
+            <template #default="{ row }">{{ row.rectifyPerson || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="rectifyDeadline" label="整改期限" width="120">
+            <template #default="{ row }">{{ row.rectifyDeadline || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="rectifyFeedback" label="治理反馈" min-width="180" show-overflow-tooltip>
+            <template #default="{ row }">{{ row.rectifyFeedback || '-' }}</template>
+          </el-table-column>
+        </template>
+
+        <el-table-column label="操作" :width="mode === 'submission' ? 190 : 180" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleDetail(row)">详情</el-button>
+            <el-button
+              v-if="mode === 'submission'"
+              type="success"
+              link
+              :disabled="isSubmitted(row)"
+              @click="handleSubmit(row)"
+            >
+              {{ isSubmitted(row) ? '已报送' : '报送' }}
+            </el-button>
+            <el-button v-else type="warning" link @click="handleFeedback(row)">治理反馈</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        v-model:current-page="pageNum"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="pagination"
+        @current-change="loadData"
+        @size-change="handleSizeChange"
+      />
+    </el-card>
+
+    <el-drawer v-model="detailVisible" title="隐患详情" size="760px" direction="rtl">
+      <el-descriptions v-if="detailData" :column="2" border>
+        <el-descriptions-item label="隐患编号">{{ detailData.hazardNo }}</el-descriptions-item>
+        <el-descriptions-item label="隐患类型">{{ getDictLabel(hazardTypeDict, detailData.hazardType) }}</el-descriptions-item>
+        <el-descriptions-item label="隐患等级">
+          <el-tag :type="levelTag(detailData.hazardLevel)">{{ getDictLabel(hazardLevelDict, detailData.hazardLevel) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="隐患来源">{{ getDictLabel(hazardSourceDict, detailData.hazardSource) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="治理状态">
+          <el-tag :type="statusTag(detailData.hazardStatus)">{{ getDictLabel(hazardStatusDict, detailData.hazardStatus) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="报送状态">
+          <el-tag :type="isSubmitted(detailData) ? 'success' : 'info'">{{ isSubmitted(detailData) ? '已报送' : '未报送' }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="上报单位">{{ detailData.reportUnit || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="上报时间">{{ detailData.reportTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="报送时间">{{ detailData.submitTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="核查人">{{ detailData.verifyPerson || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="核查时间">{{ detailData.verifyTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改责任人">{{ detailData.rectifyPerson || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改期限">{{ detailData.rectifyDeadline || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="隐患描述" :span="2">{{ detailData.hazardDesc || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改方案" :span="2">{{ detailData.rectifyPlan || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="治理反馈" :span="2">{{ detailData.rectifyFeedback || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="隐患图片" :span="2">
+          <div v-if="detailImageUrls.length" class="detail-images">
+            <el-image
+              v-for="(image, index) in detailImageUrls"
+              :key="image"
+              :src="image"
+              :preview-src-list="detailImageUrls"
+              :initial-index="index"
+              preview-teleported
+              fit="contain"
+              lazy
+            />
+          </div>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-drawer>
+
+    <el-dialog v-model="feedbackVisible" title="隐患治理反馈" width="720px" destroy-on-close>
+      <el-form ref="feedbackFormRef" :model="feedbackForm" :rules="feedbackRules" label-width="105px">
+        <el-alert
+          :title="`${feedbackForm.hazardNo || ''} ${feedbackForm.hazardDesc || ''}`"
+          type="info"
+          :closable="false"
+          class="feedback-alert"
+        />
+        <el-row :gutter="18">
+          <el-col :span="12">
+            <el-form-item label="核查人" prop="verifyPerson">
+              <el-input v-model="feedbackForm.verifyPerson" placeholder="请输入现场核查人" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="核查时间" prop="verifyTime">
+              <el-date-picker
+                v-model="feedbackForm.verifyTime"
+                type="datetime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                placeholder="请选择核查时间"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="整改责任人" prop="rectifyPerson">
+              <el-input v-model="feedbackForm.rectifyPerson" placeholder="请输入整改责任人" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="整改期限" prop="rectifyDeadline">
+              <el-date-picker
+                v-model="feedbackForm.rectifyDeadline"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择整改期限"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="治理状态" prop="hazardStatus">
+              <el-select v-model="feedbackForm.hazardStatus" style="width: 100%">
+                <el-option v-for="item in hazardStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="整改方案" prop="rectifyPlan">
+              <el-input v-model="feedbackForm.rectifyPlan" type="textarea" :rows="3" placeholder="请输入现场核查后的整改方案" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="治理反馈" prop="rectifyFeedback">
+              <el-input v-model="feedbackForm.rectifyFeedback" type="textarea" :rows="4" placeholder="请输入治理过程、处理结果及现场复核情况" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="feedbackForm.remark" type="textarea" :rows="2" placeholder="可补充填写其他说明" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <el-button @click="feedbackVisible = false">取消</el-button>
+        <el-button type="primary" :loading="saving" @click="saveFeedback">保存反馈</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getDicts } from '@/api/system/dict/data'
+import { getHazardById, getHazardPage, submitHazard, updateHazard } from '@/api/pipeNetwork/hazard'
+
+const props = defineProps({
+  mode: {
+    type: String,
+    required: true,
+    validator: value => ['submission', 'feedback'].includes(value)
+  }
+})
+
+const loading = ref(false)
+const saving = ref(false)
+const tableData = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const dateRange = ref([])
+const queryParams = reactive({
+  keyword: '',
+  hazardType: '',
+  hazardLevel: '',
+  hazardSource: '',
+  hazardStatus: '',
+  // 治理反馈默认展示全部隐患,避免历史数据未补录报送状态时出现空表。
+  // 需要只看已报送数据时,可通过上方筛选项选择“已报送”。
+  isSubmitted: null,
+  timeType: 'report'
+})
+
+const hazardTypeDict = ref([])
+const hazardLevelDict = ref([])
+const hazardStatusDict = ref([])
+const hazardSourceDict = ref([])
+const detailVisible = ref(false)
+const detailData = ref(null)
+const detailImageUrls = computed(() => parseImageUrls(detailData.value?.hazardImages))
+const feedbackVisible = ref(false)
+const feedbackFormRef = ref(null)
+const feedbackRecord = ref(null)
+
+const feedbackForm = reactive({
+  hazardNo: '',
+  hazardDesc: '',
+  verifyPerson: '',
+  verifyTime: '',
+  rectifyPerson: '',
+  rectifyDeadline: '',
+  hazardStatus: '',
+  rectifyPlan: '',
+  rectifyFeedback: '',
+  remark: ''
+})
+
+const feedbackRules = {
+  verifyPerson: [{ required: true, message: '请输入现场核查人', trigger: 'blur' }],
+  verifyTime: [{ required: true, message: '请选择核查时间', trigger: 'change' }],
+  rectifyPerson: [{ required: true, message: '请输入整改责任人', trigger: 'blur' }],
+  rectifyDeadline: [{ required: true, message: '请选择整改期限', trigger: 'change' }],
+  hazardStatus: [{ required: true, message: '请选择治理状态', trigger: 'change' }],
+  rectifyPlan: [{ required: true, message: '请输入整改方案', trigger: 'blur' }],
+  rectifyFeedback: [{ required: true, message: '请输入治理反馈', trigger: 'blur' }]
+}
+
+function getDictLabel(dict, value) {
+  const item = dict.find(entry => String(entry.dictValue) === String(value))
+  return item ? item.dictLabel : value
+}
+
+function isSubmitted(row) {
+  return Number(row?.isSubmitted) === 1
+}
+
+function levelTag(value) {
+  const label = String(getDictLabel(hazardLevelDict.value, value) || '')
+  if (label.includes('重大') || String(value) === '0') return 'danger'
+  if (label.includes('较大') || String(value) === '1') return 'warning'
+  if (label.includes('一般') || String(value) === '2') return 'primary'
+  return 'info'
+}
+
+function statusTag(value) {
+  const label = String(getDictLabel(hazardStatusDict.value, value) || '')
+  if (label.includes('已整改') || label.includes('闭环')) return 'success'
+  if (label.includes('整改中') || label.includes('治理中')) return 'warning'
+  return 'info'
+}
+
+function parseImageUrls(value) {
+  if (!value) return []
+  let values = value
+  if (typeof values === 'string') {
+    const text = values.trim()
+    if (text.startsWith('[') || text.startsWith('{')) {
+      try {
+        values = JSON.parse(text)
+      } catch (error) {
+        values = text.split(/[,;\n]/)
+      }
+    } else {
+      values = text.split(/[,;\n]/)
+    }
+  }
+  if (!Array.isArray(values)) values = [values]
+  return values.map(item => {
+    const source = typeof item === 'object' ? (item.url || item.fileName || item.name) : item
+    return normalizeImageUrl(source)
+  }).filter(Boolean)
+}
+
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/i.test(url)) return url
+  const minioBase = String(import.meta.env.VITE_APP_MINIO_BASE_URL || '').trim().replace(/\/+$/, '')
+  if (minioBase) {
+    if (url.startsWith('/')) return `${minioBase}${url}`
+    if (url.startsWith('upload/')) return `${minioBase}/${url}`
+    if (!url.includes('/')) return `${minioBase}/upload/${url}`
+  }
+  const baseApi = String(import.meta.env.VITE_APP_BASE_API || '').trim().replace(/\/+$/, '')
+  return url.startsWith('/') ? `${baseApi}${url}` : `${baseApi}/${url}`
+}
+
+function buildQuery() {
+  const params = {}
+  Object.entries(queryParams).forEach(([key, value]) => {
+    if (value !== '' && value !== null && value !== undefined) params[key] = value
+  })
+  if (dateRange.value?.length === 2) {
+    params.startDate = dateRange.value[0]
+    params.endDate = dateRange.value[1]
+  }
+  if (props.mode !== 'submission') {
+    params.timeType = 'report'
+  }
+  return params
+}
+
+async function loadDicts() {
+  try {
+    const [typeRes, levelRes, statusRes, sourceRes] = await Promise.all([
+      getDicts('hazard_type'),
+      getDicts('hazard_level'),
+      getDicts('hazard_status'),
+      getDicts('hazard_source')
+    ])
+    hazardTypeDict.value = typeRes.data || []
+    hazardLevelDict.value = levelRes.data || []
+    hazardStatusDict.value = statusRes.data || []
+    hazardSourceDict.value = sourceRes.data || []
+  } catch (error) {
+    ElMessage.error(error?.message || '隐患字典加载失败')
+  }
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const response = await getHazardPage(pageNum.value, pageSize.value, buildQuery())
+    tableData.value = response.data?.records || []
+    total.value = Number(response.data?.total || 0)
+  } catch (error) {
+    ElMessage.error(error?.response?.data?.msg || error?.message || '隐患数据加载失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  pageNum.value = 1
+  loadData()
+}
+
+function handleReset() {
+  Object.assign(queryParams, {
+    keyword: '',
+    hazardType: '',
+    hazardLevel: '',
+    hazardSource: '',
+    hazardStatus: '',
+    isSubmitted: null,
+    timeType: 'report'
+  })
+  dateRange.value = []
+  handleSearch()
+}
+
+function handleSizeChange() {
+  pageNum.value = 1
+  loadData()
+}
+
+async function fetchDetail(row) {
+  try {
+    const response = await getHazardById(row.hazardId)
+    return response.data || row
+  } catch (error) {
+    return row
+  }
+}
+
+async function handleDetail(row) {
+  detailData.value = await fetchDetail(row)
+  detailVisible.value = true
+}
+
+async function handleSubmit(row) {
+  try {
+    await ElMessageBox.confirm(
+      `确认将隐患“${row.hazardNo}”报送到上级主管单位?报送后将按现有逻辑自动创建工单。`,
+      '隐患报送确认',
+      { type: 'warning', confirmButtonText: '确认报送', cancelButtonText: '取消' }
+    )
+    const response = await submitHazard(row.hazardId)
+    ElMessage.success(response.data ? '报送成功,已自动创建工单' : '报送成功')
+    loadData()
+  } catch (error) {
+    if (error === 'cancel' || error === 'close') return
+    ElMessage.error(error?.response?.data?.msg || error?.message || '报送失败')
+  }
+}
+
+function formatNow() {
+  const now = new Date()
+  const pad = value => String(value).padStart(2, '0')
+  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+}
+
+async function handleFeedback(row) {
+  const record = await fetchDetail(row)
+  feedbackRecord.value = record
+  Object.assign(feedbackForm, {
+    hazardNo: record.hazardNo || '',
+    hazardDesc: record.hazardDesc || '',
+    verifyPerson: record.verifyPerson || '',
+    verifyTime: record.verifyTime || formatNow(),
+    rectifyPerson: record.rectifyPerson || '',
+    rectifyDeadline: record.rectifyDeadline || '',
+    hazardStatus: record.hazardStatus || '',
+    rectifyPlan: record.rectifyPlan || '',
+    rectifyFeedback: record.rectifyFeedback || '',
+    remark: record.remark || ''
+  })
+  feedbackVisible.value = true
+}
+
+async function saveFeedback() {
+  if (!feedbackFormRef.value || !feedbackRecord.value) return
+  try {
+    await feedbackFormRef.value.validate()
+    saving.value = true
+    await updateHazard({
+      ...feedbackRecord.value,
+      verifyPerson: feedbackForm.verifyPerson,
+      verifyTime: feedbackForm.verifyTime,
+      rectifyPerson: feedbackForm.rectifyPerson,
+      rectifyDeadline: feedbackForm.rectifyDeadline,
+      hazardStatus: feedbackForm.hazardStatus,
+      rectifyPlan: feedbackForm.rectifyPlan,
+      rectifyFeedback: feedbackForm.rectifyFeedback,
+      remark: feedbackForm.remark
+    })
+    ElMessage.success('治理反馈保存成功')
+    feedbackVisible.value = false
+    loadData()
+  } catch (error) {
+    if (error?.fields) return
+    ElMessage.error(error?.response?.data?.msg || error?.message || '治理反馈保存失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+onMounted(async () => {
+  await loadDicts()
+  loadData()
+})
+</script>
+
+<style scoped>
+.workflow-page {
+  min-height: 100%;
+  padding: 16px;
+  background: #f5f7fa;
+}
+
+.search-card {
+  margin-bottom: 14px;
+  border: 1px solid #e1e8ef;
+}
+
+.search-form :deep(.el-form-item) {
+  margin-bottom: 10px;
+}
+
+.table-card {
+  border: 1px solid #e1e8ef;
+}
+
+.pagination {
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+.detail-images {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.detail-images .el-image {
+  width: 110px;
+  height: 110px;
+  border-radius: 6px;
+}
+
+.feedback-alert {
+  margin-bottom: 20px;
+}
+
+</style>

+ 7 - 0
src/views/subSystem/hidden/yhbs.vue

@@ -0,0 +1,7 @@
+<template>
+  <HazardOperationPage mode="submission" />
+</template>
+
+<script setup>
+import HazardOperationPage from './HazardOperationPage.vue'
+</script>

+ 20 - 0
src/views/subSystem/hidden/yhpc.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="inspection-page">
+    <HazardList />
+  </div>
+</template>
+
+<script setup>
+import HazardList from './HazardList.vue'
+</script>
+
+<style scoped>
+.inspection-page {
+  min-height: 100%;
+  background: #f5f7fa;
+}
+
+:deep(.hazard-container) {
+  padding-top: 12px;
+}
+</style>

+ 7 - 0
src/views/subSystem/hidden/yhzlfk.vue

@@ -0,0 +1,7 @@
+<template>
+  <HazardOperationPage mode="feedback" />
+</template>
+
+<script setup>
+import HazardOperationPage from './HazardOperationPage.vue'
+</script>

File diff ditekan karena terlalu besar
+ 33 - 0
src/views/subSystem/lifeCompany/warningThreshold/index.vue


+ 0 - 769
src/views/subSystem/manholeCover/alarm/alarmList.vue

@@ -1,769 +0,0 @@
-<!--状态监测报警-->
-<!--监测报警一张图-->
-<template>
-  <div class="alert-dashboard">
-    <!-- 头部统计告警栏 -->
-    <div class="alert-header">
-
-      <div class="stats-cards">
-        <div class="stat-item" :class="{ active: filterType === 'all' }" @click="filterAlerts('all')">
-          <div class="stat-num">{{ totalAlerts }}</div>
-          <div class="stat-label">全部报警</div>
-        </div>
-        <div class="stat-item critical" :class="{ active: filterType === 'critical' }" @click="filterAlerts('critical')">
-          <div class="stat-num">{{ criticalCount }}</div>
-          <div class="stat-label">严重报警</div>
-        </div>
-        <div class="stat-item warning" :class="{ active: filterType === 'warning' }" @click="filterAlerts('warning')">
-          <div class="stat-num">{{ warningCount }}</div>
-          <div class="stat-label">一般报警</div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 主内容区域:左侧报警列表 + 右侧详情研判 -->
-    <div class="main-content">
-      <!-- 左侧报警列表 -->
-      <div class="alert-list-panel" v-loading="listLoading">
-        <div class="panel-header">
-          <span>报警事件列表 <el-badge :value="unhandledCount" class="badge" /></span>
-          <el-input v-model="searchText" placeholder="搜索位置/设备编号" prefix-icon="Search" clearable size="small" style="width: 180px" @input="handleSearch" />
-        </div>
-        <div class="alert-list">
-          <div
-              v-for="alert in alerts"
-              :key="alert.id"
-              class="alert-item"
-              :class="{
-              selected: selectedAlert?.id === alert.id,
-              critical: alert.level === 'critical',
-              warning: alert.level === 'warning'
-            }"
-              @click="selectAlert(alert)"
-          >
-            <div class="alert-level-icon">
-              <el-icon v-if="alert.level === 'critical'"><WarningFilled /></el-icon>
-              <el-icon v-else-if="alert.level === 'warning'"><InfoFilled /></el-icon>
-              <el-icon v-else><CircleCheck /></el-icon>
-            </div>
-            <div class="alert-content">
-              <div class="alert-title">
-                <span>{{ alert.deviceName }}</span>
-                <span class="alert-time">{{ alert.time }}</span>
-              </div>
-              <div class="alert-location">
-                <el-icon><Location /></el-icon> {{ alert.location }}
-              </div>
-              <div class="alert-type">{{ alert.alertType }}</div>
-              <div class="alert-value" v-if="alert.exceedValue">实时值: <strong class="exceed">{{ alert.exceedValue }}</strong> (阈值: {{ alert.threshold }})</div>
-            </div>
-            <div class="alert-status" v-if="alert.status === 'resolved'">
-              <el-tag type="success" size="small">已处置</el-tag>
-            </div>
-            <div class="alert-status" v-else-if="alert.status === 'processing'">
-              <el-tag type="warning" size="small">处理中</el-tag>
-            </div>
-            <div class="alert-status" v-else>
-              <el-tag type="danger" size="small">待处理</el-tag>
-            </div>
-          </div>
-          <el-empty v-if="!alerts.length && !listLoading" description="暂无报警事件" :image-size="100" />
-        </div>
-        <div class="pagination-area">
-          <el-pagination
-            small
-            background
-            layout="total, prev, pager, next"
-            :total="total"
-            :page-size="pageSize"
-            v-model:current-page="currentPage"
-            @current-change="loadAlerts"
-          />
-        </div>
-      </div>
-
-      <!-- 右侧报警详情与研判面板 -->
-      <div class="detail-panel" v-if="selectedAlert" v-loading="detailLoading">
-        <!-- 报警基本信息 -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon><Warning /></el-icon> 报警详情 - 辅助研判</span>
-          </div>
-          <el-descriptions :column="2" border class="detail-desc">
-            <el-descriptions-item label="报警位置">
-              <strong>{{ selectedAlert.location }}</strong> <el-button link type="primary" size="small" @click="viewOnMap">查看地图</el-button>
-            </el-descriptions-item>
-            <el-descriptions-item label="报警级别">
-              <el-tag :type="selectedAlert.level === 'critical' ? 'danger' : 'warning'" size="default">
-                {{ selectedAlert.level === 'critical' ? '严重报警' : '一般报警' }}
-              </el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="设备编号">{{ selectedAlert.deviceCode || '—' }}</el-descriptions-item>
-            <el-descriptions-item label="设备类型">{{ selectedAlert.deviceType }}</el-descriptions-item>
-            <el-descriptions-item label="报警类型">{{ selectedAlert.alertType }}</el-descriptions-item>
-            <el-descriptions-item label="报警时间">{{ selectedAlert.time }}</el-descriptions-item>
-            <el-descriptions-item label="实时数据" :span="2">
-              <div class="real-time-data">
-                <span v-if="selectedAlert.realData">
-                  当前 {{ selectedAlert.alertParam }} = <strong class="warning-value">{{ selectedAlert.realData }}</strong>
-                  (阈值: {{ selectedAlert.threshold }})
-                </span>
-                <span v-else>{{ selectedAlert.exceedValue || '超出阈值' }}</span>
-              </div>
-            </el-descriptions-item>
-          </el-descriptions>
-        </div>
-      </div>
-
-      <!-- 未选中报警时的空状态 -->
-      <div v-else class="empty-detail">
-        <el-empty description="请从左侧选择一个报警事件进行研判" :image-size="140" />
-      </div>
-    </div>
-
-    <!-- 地图弹窗 -->
-    <el-drawer v-model="mapDialogVisible" title="报警位置地图" size="80%" class="map-dialog detail-drawer" @opened="handleMapDialogOpened" direction="rtl">
-      <div class="dialog-map-wrapper">
-        <div ref="dialogMapRef" class="dialog-baidu-map"></div>
-        <div v-if="dialogMapError" class="dialog-map-error">{{ dialogMapError }}</div>
-        <div v-if="!hasAlertCoordinate" class="dialog-map-empty">
-          当前报警缺少真实坐标,已展示默认地图视野
-        </div>
-        <div class="dialog-map-footer">
-          <div><strong>{{ selectedAlert?.location || '暂无位置' }}</strong></div>
-          <div>经度: {{ selectedAlert?.lng || '暂无数据' }}, 纬度: {{ selectedAlert?.lat || '暂无数据' }}</div>
-        </div>
-      </div>
-    </el-drawer>
-  </div>
-</template>
-
-<script setup>
-import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
-import {CircleCheck, InfoFilled, Location, Warning, WarningFilled} from '@element-plus/icons-vue'
-import {ElMessage} from 'element-plus'
-import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
-
-const alerts = ref([])
-const allAlerts = ref([])
-const searchText = ref('')
-const filterType = ref('all')
-const currentPage = ref(1)
-const pageSize = 6
-const total = ref(0)
-const selectedAlert = ref(null)
-const mapDialogVisible = ref(false)
-const listLoading = ref(false)
-const detailLoading = ref(false)
-const dialogMapRef = ref(null)
-const dialogMapError = ref('')
-
-// 搜索防抖
-let searchTimer = null
-function handleSearch() {
-  clearTimeout(searchTimer)
-  searchTimer = setTimeout(() => {
-    currentPage.value = 1
-    loadAlerts()
-    loadStats()
-  }, 300)
-}
-
-const BAIDU_MAP_AK = import.meta.env.VITE_BAIDU_MAP_AK || ''
-const DEFAULT_MAP_CENTER = { lng: 110.393, lat: 28.452 }
-
-let dialogMapInstance = null
-let dialogMapOverlays = []
-let baiduMapScriptPromise = null
-
-function toNumber(value) {
-  const num = Number(value)
-  return Number.isFinite(num) ? num : null
-}
-
-function isValidCoordinate(lng, lat) {
-  return lng !== null && lat !== null && lng >= 70 && lng <= 140 && lat >= 3 && lat <= 55
-}
-
-function formatDateTime(value) {
-  if (!value) return '—'
-  return String(value).replace('T', ' ')
-}
-
-function getThresholdTextByCode(raw, warningCode) {
-  const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
-  const target = list.find(item => item.warningCode === warningCode)
-  if (!target) return '—'
-  const { minValue, maxValue } = target
-  if (minValue !== null && minValue !== undefined && maxValue !== null && maxValue !== undefined) {
-    return `${minValue} ~ ${maxValue}`
-  }
-  if (minValue !== null && minValue !== undefined) return `<= ${minValue}`
-  if (maxValue !== null && maxValue !== undefined) return `>= ${maxValue}`
-  return '—'
-}
-
-function buildAlertInfo(raw) {
-  const status = raw?.equipmentStatus || {}
-  const manholeData = raw?.manholeData || {}
-  const battery = toNumber(manholeData.batteryLevel)
-  const signal = toNumber(manholeData.signalStrength)
-  const tiltAngle = toNumber(manholeData.tiltAngle)
-
-  if (`${manholeData.waterLevelAlarmStatus}` === '1') {
-    return {
-      hasAlert: true,
-      level: 'critical',
-      levelText: '严重报警',
-      alertType: '水位报警',
-      alertParam: '水位',
-      realData: manholeData.waterLevel !== undefined && manholeData.waterLevel !== null ? `${manholeData.waterLevel}cm` : '报警',
-      threshold: getThresholdTextByCode(raw, 'WARN-WATER-LEVEL')
-    }
-  }
-
-  if (`${manholeData.waterInfiltrationAlarmStatus}` === '1') {
-    return {
-      hasAlert: true,
-      level: 'critical',
-      levelText: '严重报警',
-      alertType: '水浸报警',
-      alertParam: '水浸状态',
-      realData: '报警',
-      threshold: getThresholdTextByCode(raw, 'WARN-WATER-INFILTRATION')
-    }
-  }
-
-  if (Number(status.alarmStatus) === 1) {
-    return {
-      hasAlert: true,
-      level: 'critical',
-      levelText: '严重报警',
-      alertType: tiltAngle !== null ? '倾斜报警' : '设备报警',
-      alertParam: tiltAngle !== null ? '倾斜角' : '设备状态',
-      realData: tiltAngle !== null ? `${tiltAngle}°` : '报警',
-      threshold: tiltAngle !== null ? getThresholdTextByCode(raw, 'WARN-TILT') : '—'
-    }
-  }
-
-  if (battery !== null && battery <= 20) {
-    return {
-      hasAlert: true,
-      level: 'warning',
-      levelText: '一般报警',
-      alertType: '低电量预警',
-      alertParam: '电池电量',
-      realData: `${battery}%`,
-      threshold: '20%'
-    }
-  }
-
-  if (signal !== null && signal <= 20) {
-    return {
-      hasAlert: true,
-      level: 'warning',
-      levelText: '一般报警',
-      alertType: '弱信号预警',
-      alertParam: '信号强度',
-      realData: `${signal}%`,
-      threshold: '20%'
-    }
-  }
-
-  return {
-    hasAlert: false,
-    level: '',
-    levelText: '',
-    alertType: '',
-    alertParam: '',
-    realData: '',
-    threshold: '—'
-  }
-}
-
-function normalizeAlert(raw, baseAlert = null) {
-  const alertInfo = buildAlertInfo(raw)
-  if (!alertInfo.hasAlert) return null
-  return {
-    id: raw?.equipmentId || raw?.equipmentCode,
-    equipmentId: raw?.equipmentId || '',
-    deviceCode: raw?.equipmentCode || '',
-    deviceName: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
-    deviceType: raw?.equipmentTypeName || '井盖监测设备',
-    location: raw?.equipmentLocation || '暂无位置信息',
-    lng: raw?.longitude || baseAlert?.lng || '',
-    lat: raw?.latitude || baseAlert?.lat || '',
-    alertType: alertInfo.alertType,
-    alertParam: alertInfo.alertParam,
-    realData: alertInfo.realData,
-    threshold: alertInfo.threshold,
-    exceedValue: alertInfo.realData || '报警',
-    level: alertInfo.level,
-    levelText: alertInfo.levelText,
-    time: formatDateTime(raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime),
-    status: raw?.equipmentStatus?.alarmStatus === 1 ? 'pending' : 'resolved',
-    statusText: raw?.equipmentStatus?.alarmStatus === 1 ? '待处理' : '已恢复',
-    raw
-  }
-}
-
-// 报警级别映射:全部=0,严重=1,一般=2
-const ALARM_LEVEL_MAP = { all: 0, critical: 1, warning: 2 }
-
-async function loadAlerts() {
-  listLoading.value = true
-  try {
-    const res = await getManholeAlarmDeviceList({
-      keyword: searchText.value.trim() || undefined,
-      alarmLevel: ALARM_LEVEL_MAP[filterType.value],
-      isQueryManholeData: true,
-      pageNum: currentPage.value,
-      pageSize
-    })
-    const list = res.data?.records || []
-    total.value = res.data?.total || list.length
-    alerts.value = list
-      .map(item => normalizeAlert(item))
-      .filter(Boolean)
-    if (!alerts.value.length) {
-      selectedAlert.value = null
-      return
-    }
-    const currentId = selectedAlert.value?.id
-    selectedAlert.value = alerts.value.find(item => item.id === currentId) || alerts.value[0]
-    if (selectedAlert.value?.equipmentId) {
-      await loadAlertDetail(selectedAlert.value)
-    }
-  } catch (error) {
-    alerts.value = []
-    selectedAlert.value = null
-    ElMessage.error('报警设备加载失败')
-  } finally {
-    listLoading.value = false
-  }
-}
-
-// 加载全量报警数据(用于顶部统计卡片,不受分页影响)
-async function loadStats() {
-  try {
-    const res = await getManholeAlarmDeviceList({
-      keyword: searchText.value.trim() || undefined,
-      alarmLevel: 0,
-      isQueryManholeData: true,
-      pageNum: 1,
-      pageSize: 1000
-    })
-    const list = res.data?.records || []
-    allAlerts.value = list
-      .map(item => normalizeAlert(item))
-      .filter(Boolean)
-  } catch (error) {
-    allAlerts.value = []
-  }
-}
-
-async function loadAlertDetail(alert) {
-  if (!alert?.equipmentId) return
-  detailLoading.value = true
-  try {
-    const res = await getManholeAlarmDeviceDetail(alert.equipmentId)
-    if (res?.data) {
-      const merged = normalizeAlert(res.data, alert)
-      if (merged) {
-        const idx = alerts.value.findIndex(item => item.id === merged.id)
-        if (idx > -1) {
-          alerts.value[idx] = merged
-        }
-        selectedAlert.value = merged
-      }
-    }
-  } catch (error) {
-    ElMessage.error('报警详情加载失败')
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-const totalAlerts = computed(() => allAlerts.value.length)
-const criticalCount = computed(() => allAlerts.value.filter(a => a.level === 'critical').length)
-const warningCount = computed(() => allAlerts.value.filter(a => a.level === 'warning').length)
-const unhandledCount = computed(() => allAlerts.value.length)
-const hasAlertCoordinate = computed(() => isValidCoordinate(toNumber(selectedAlert.value?.lng), toNumber(selectedAlert.value?.lat)))
-
-const filterAlerts = (type) => {
-  if (filterType.value === type) return
-  filterType.value = type
-  currentPage.value = 1
-  loadAlerts()
-}
-
-const selectAlert = async (alert) => {
-  selectedAlert.value = alert
-  await loadAlertDetail(alert)
-}
-
-const viewOnMap = () => {
-  mapDialogVisible.value = true
-}
-
-function clearDialogMapMarker() {
-  if (!dialogMapInstance || !dialogMapOverlays.length) return
-  dialogMapOverlays.forEach((overlay) => {
-    try {
-      dialogMapInstance.removeOverlay(overlay)
-    } catch (_) {}
-  })
-  dialogMapOverlays = []
-}
-
-function getDialogInfoWindowHtml() {
-  const levelTagColor = selectedAlert.value?.level === 'critical' ? '#f56c6c' : '#e6a23c'
-  const levelText = selectedAlert.value?.levelText || '报警'
-  return `
-    <div style="min-width:220px;max-width:280px;padding:4px 2px;line-height:1.7;color:#303133;">
-      <div style="font-size:14px;font-weight:700;color:#1f2937;margin-bottom:6px;">
-        ${selectedAlert.value?.deviceName || '报警设备'}
-      </div>
-      <div style="margin-bottom:4px;">
-        <span style="display:inline-block;padding:1px 8px;border-radius:999px;background:${levelTagColor};color:#fff;font-size:12px;">
-          ${levelText}
-        </span>
-      </div>
-      <div style="font-size:12px;"><span style="color:#909399;">报警类型:</span>${selectedAlert.value?.alertType || '—'}</div>
-      <div style="font-size:12px;"><span style="color:#909399;">报警时间:</span>${selectedAlert.value?.time || '—'}</div>
-      <div style="font-size:12px;"><span style="color:#909399;">实时数据:</span>${selectedAlert.value?.realData || selectedAlert.value?.exceedValue || '—'}</div>
-      <div style="font-size:12px;"><span style="color:#909399;">阈值:</span>${selectedAlert.value?.threshold || '—'}</div>
-      <div style="font-size:12px;"><span style="color:#909399;">报警位置:</span>${selectedAlert.value?.location || '—'}</div>
-    </div>
-  `
-}
-
-function ensureBaiduMap() {
-  if (window.BMapGL) {
-    return Promise.resolve(window.BMapGL)
-  }
-  if (baiduMapScriptPromise) {
-    return baiduMapScriptPromise
-  }
-  if (!BAIDU_MAP_AK) {
-    return Promise.reject(new Error('未配置百度地图 AK,请在 .env.development 中配置 VITE_BAIDU_MAP_AK'))
-  }
-
-  baiduMapScriptPromise = new Promise((resolve, reject) => {
-    const existing = document.querySelector('script[data-baidu-map="webgl"]')
-    if (existing) {
-      let attempts = 0
-      const timer = window.setInterval(() => {
-        attempts++
-        if (window.BMapGL) {
-          window.clearInterval(timer)
-          resolve(window.BMapGL)
-        } else if (attempts >= 50) {
-          window.clearInterval(timer)
-          reject(new Error('百度地图脚本加载超时'))
-        }
-      }, 200)
-      return
-    }
-
-    const script = document.createElement('script')
-    script.src = `https://api.map.baidu.com/api?type=webgl&v=1.0&ak=${BAIDU_MAP_AK}`
-    script.async = true
-    script.defer = true
-    script.setAttribute('data-baidu-map', 'webgl')
-    script.onload = () => {
-      let attempts = 0
-      const timer = window.setInterval(() => {
-        attempts++
-        if (window.BMapGL) {
-          window.clearInterval(timer)
-          resolve(window.BMapGL)
-        } else if (attempts >= 50) {
-          window.clearInterval(timer)
-          reject(new Error('百度地图脚本初始化超时'))
-        }
-      }, 200)
-    }
-    script.onerror = () => reject(new Error('百度地图脚本加载失败'))
-    document.head.appendChild(script)
-  })
-
-  return baiduMapScriptPromise
-}
-
-function renderDialogMapMarker() {
-  if (!dialogMapInstance || !window.BMapGL) return
-  clearDialogMapMarker()
-
-  const lng = toNumber(selectedAlert.value?.lng)
-  const lat = toNumber(selectedAlert.value?.lat)
-  if (!isValidCoordinate(lng, lat)) {
-    dialogMapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 13)
-    return
-  }
-
-  const point = new window.BMapGL.Point(lng, lat)
-  dialogMapInstance.centerAndZoom(point, 15)
-  const marker = new window.BMapGL.Marker(point)
-  const infoWindow = new window.BMapGL.InfoWindow(getDialogInfoWindowHtml(), {
-    width: 260,
-    title: '报警信息'
-  })
-  dialogMapInstance.addOverlay(marker)
-  dialogMapOverlays.push(marker)
-  marker.addEventListener('click', () => {
-    dialogMapInstance.openInfoWindow(infoWindow, point)
-  })
-
-  const label = new window.BMapGL.Label(
-    `<div style="padding:4px 8px;border-radius:999px;background:rgba(20,33,61,0.82);color:#fff;font-size:12px;line-height:1.2;white-space:nowrap;">${selectedAlert.value?.deviceName || '报警设备'}</div>`,
-    {
-      position: point,
-      offset: new window.BMapGL.Size(-30, -40)
-    }
-  )
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0'
-  })
-  label.addEventListener('click', () => {
-    dialogMapInstance.openInfoWindow(infoWindow, point)
-  })
-  dialogMapInstance.addOverlay(label)
-  dialogMapOverlays.push(label)
-  dialogMapInstance.openInfoWindow(infoWindow, point)
-}
-
-async function initDialogMap() {
-  if (!dialogMapRef.value) return
-  try {
-    await ensureBaiduMap()
-    if (!dialogMapInstance) {
-      dialogMapInstance = new window.BMapGL.Map(dialogMapRef.value, { enableMapClick: true })
-      dialogMapInstance.enableScrollWheelZoom(true)
-      dialogMapInstance.centerAndZoom(new window.BMapGL.Point(DEFAULT_MAP_CENTER.lng, DEFAULT_MAP_CENTER.lat), 13)
-    }
-    dialogMapError.value = ''
-    renderDialogMapMarker()
-  } catch (error) {
-    dialogMapError.value = error?.message || '百度地图加载失败'
-  }
-}
-
-async function handleMapDialogOpened() {
-  await nextTick()
-  await initDialogMap()
-}
-
-watch(
-  () => selectedAlert.value?.id,
-  async () => {
-    if (!mapDialogVisible.value) return
-    await nextTick()
-    renderDialogMapMarker()
-  }
-)
-
-onMounted(async () => {
-  await Promise.all([loadAlerts(), loadStats()])
-})
-
-onUnmounted(() => {
-  clearTimeout(searchTimer)
-  clearDialogMapMarker()
-  dialogMapInstance = null
-})
-</script>
-
-<style scoped>
-.alert-dashboard {
-  background: #f0f2f6;
-  min-height: 100vh;
-  padding: 20px;
-}
-.alert-header {
-  background: white;
-  border-radius: 20px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
-}
-.title-badge {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  margin-bottom: 16px;
-}
-.blink-icon {
-  font-size: 28px;
-  color: #f56c6c;
-  animation: blink 1s infinite;
-}
-@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
-.title { font-size: 22px; font-weight: 700; }
-.stats-cards { display: flex; gap: 20px; margin-top: 6px; }
-.stat-item { background: #f8fafc; padding: 10px 24px; border-radius: 16px; cursor: pointer; text-align: center; transition: all 0.2s; min-width: 100px; }
-.stat-item.active { background: #ecf5ff; border: 1px solid #409eff; }
-.stat-num { font-size: 28px; font-weight: bold; }
-.stat-item.critical .stat-num { color: #f56c6c; }
-.stat-item.warning .stat-num { color: #e6a23c; }
-.main-content { display: flex; gap: 20px; }
-.alert-list-panel {
-  width: 400px;
-  background: white;
-  border-radius: 20px;
-  padding: 16px;
-  display: flex;
-  flex-direction: column;
-}
-.panel-header { display: flex; justify-content: space-between; margin-bottom: 16px; font-weight: 600; }
-.alert-list { max-height: calc(100vh - 200px); overflow-y: auto; }
-.alert-item {
-  display: flex;
-  align-items: flex-start;
-  gap: 12px;
-  padding: 14px;
-  margin-bottom: 12px;
-  border-radius: 14px;
-  background: #fafbfc;
-  cursor: pointer;
-  border: 1px solid #e9ecef;
-  transition: all 0.2s;
-}
-.alert-item.selected { background: #ecf5ff; border-color: #409eff; }
-.alert-item.critical { border-left: 4px solid #f56c6c; }
-.alert-item.warning { border-left: 4px solid #e6a23c; }
-.alert-level-icon { font-size: 22px; margin-top: 4px; }
-.alert-content { flex: 1; }
-.alert-title { display: flex; justify-content: space-between; font-weight: 600; margin-bottom: 4px; }
-.alert-time { font-size: 11px; color: #909399; }
-.alert-location { font-size: 12px; color: #606266; display: flex; align-items: center; gap: 4px; margin: 4px 0; }
-.alert-type { font-size: 12px; color: #409eff; }
-.alert-value { font-size: 12px; margin-top: 6px; }
-.exceed { color: #f56c6c; }
-.detail-panel {
-  flex: 1;
-  min-width: 0;
-  background: white;
-  border-radius: 20px;
-  padding: 20px;
-  overflow-y: auto;
-  max-height: calc(100vh - 150px);
-}
-.action-buttons { display: flex; gap: 8px; }
-.empty-detail { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
-
-/* ==================== 字体放大与自适应 ==================== */
-/* 详情区字体放大 */
-.detail-desc :deep(.el-descriptions__label) {
-  font-size: 14px;
-}
-.detail-desc :deep(.el-descriptions__content) {
-  font-size: 15px;
-}
-.detail-desc :deep(.el-descriptions__content strong) {
-  font-size: 16px;
-}
-.detail-desc .warning-value {
-  color: #f56c6c;
-  font-size: 22px;
-  margin: 0 4px;
-}
-.detail-desc .real-time-data {
-  background: #fff9f0;
-  padding: 8px;
-  border-radius: 8px;
-  font-size: 15px;
-}
-.section-title {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
-  font-weight: 600;
-  font-size: 18px;
-}
-.detail-section {
-  margin-bottom: 24px;
-  border-bottom: 1px solid #f0f0f0;
-  padding-bottom: 20px;
-}
-
-/* 自适应:窄屏时左侧列表收窄,详情不被挤压 */
-@media (max-width: 1500px) {
-  .alert-list-panel {
-    width: 340px;
-  }
-  .detail-desc :deep(.el-descriptions) {
-    --el-descriptions-item-bordered-label-background: #f5f7fa;
-  }
-}
-@media (max-width: 1200px) {
-  .main-content {
-    flex-direction: column;
-  }
-  .alert-list-panel {
-    width: 100%;
-  }
-  .alert-list {
-    max-height: 360px;
-  }
-}
-/* ==================== 地图弹窗 ==================== */
-.dialog-map-wrapper {
-  position: relative;
-}
-/* 地图高度放大 */
-.dialog-baidu-map {
-  height: 520px;
-  border-radius: 12px;
-  overflow: hidden;
-  background: #eef2f0;
-}
-.dialog-map-footer {
-  padding-top: 12px;
-  font-size: 14px;
-  color: #606266;
-  line-height: 1.8;
-}
-.dialog-map-empty {
-  position: absolute;
-  top: 16px;
-  left: 16px;
-  background: rgba(255, 255, 255, 0.94);
-  color: #909399;
-  font-size: 12px;
-  padding: 8px 12px;
-  border-radius: 10px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
-  z-index: 2;
-}
-.dialog-map-error {
-  position: absolute;
-  top: 16px;
-  right: 16px;
-  max-width: 280px;
-  background: rgba(255, 255, 255, 0.95);
-  color: #f56c6c;
-  border: 1px solid rgba(245, 108, 108, 0.25);
-  border-radius: 10px;
-  padding: 8px 12px;
-  font-size: 12px;
-  line-height: 1.6;
-  z-index: 2;
-}
-/* 弹窗宽度自适应(放大) */
-.map-dialog {
-  max-width: 1200px;
-}
-.map-dialog :deep(.el-dialog__body) {
-  padding: 16px 20px 20px;
-}
-
-.pagination-area {
-  padding-top: 10px;
-  display: flex;
-  justify-content: center;
-}
-</style>

+ 0 - 668
src/views/subSystem/manholeCover/alarm/index.vue

@@ -1,668 +0,0 @@
-<template>
-  <div class="threshold-management">
-    <div class="main-content">
-      <div class="point-list-panel">
-        <div class="panel-title">
-          <span>设备列表</span>
-          <el-button :icon="Refresh" text @click="loadDeviceList">刷新</el-button>
-        </div>
-        <el-input
-          v-model="searchKeyword"
-          placeholder="搜索设备编号/名称"
-          clearable
-          :prefix-icon="Search"
-          size="small"
-          @input="handleSearch"
-        />
-
-        <div v-loading="listLoading" class="point-list">
-          <div
-            v-for="point in deviceList"
-            :key="point.equipmentId"
-            class="point-item"
-            :class="{ active: selectedPoint?.equipmentId === point.equipmentId }"
-            @click="selectPoint(point)"
-          >
-            <div class="point-header">
-              <span class="point-name">{{ point.equipmentName || point.equipmentCode || '未命名设备' }}</span>
-              <el-tag :type="getAlarmTagType(point)" size="small">
-                {{ getAlarmText(point) }}
-              </el-tag>
-            </div>
-            <div class="point-location">
-              <el-icon><Location /></el-icon>
-              <span>{{ point.equipmentLocation || '暂无位置' }}</span>
-            </div>
-            <div class="point-meta">
-              <span>编号:{{ point.equipmentCode || '-' }}</span>
-              <span>类型:{{ point.equipmentTypeName || '-' }}</span>
-            </div>
-            <div class="point-status-row">
-              <el-tag size="small" effect="plain" :type="getOnlineTagType(point.equipmentStatus?.onlineStatus)">
-                {{ getOnlineText(point.equipmentStatus?.onlineStatus) }}
-              </el-tag>
-              <el-tag size="small" effect="plain" :type="getUseStatusTagType(point.equipmentStatus?.currentStatus)">
-                {{ getUseStatusText(point.equipmentStatus?.currentStatus) }}
-              </el-tag>
-            </div>
-          </div>
-
-          <el-empty
-            v-if="!listLoading && !deviceList.length"
-            description="暂无井盖监测设备"
-            :image-size="120"
-          />
-        </div>
-
-        <div class="pagination-area">
-          <el-pagination
-            small
-            background
-            layout="total, prev, pager, next"
-            :total="total"
-            :page-size="pageSize"
-            v-model:current-page="currentPage"
-            @current-change="loadDeviceList"
-          />
-        </div>
-      </div>
-
-      <div v-if="selectedPoint" v-loading="detailLoading" class="config-panel">
-        <div class="info-card">
-          <div class="card-title">
-            <span><el-icon><Monitor /></el-icon> 设备基本信息</span>
-          </div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="设备编号">{{ selectedDetail?.equipmentCode || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="设备名称">{{ selectedDetail?.equipmentName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="设备类型">{{ selectedDetail?.equipmentTypeName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="设备型号">{{ selectedDetail?.equipmentModel || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="生产厂商">{{ selectedDetail?.manufacturer || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="安装时间">{{ formatDateTime(selectedDetail?.installTime) }}</el-descriptions-item>
-            <el-descriptions-item label="设备位置">{{ selectedDetail?.equipmentLocation || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="维护人员">{{ selectedDetail?.maintainer || '-' }}</el-descriptions-item>
-          </el-descriptions>
-        </div>
-
-        <div class="info-card">
-          <div class="card-title">
-            <span><el-icon><Warning /></el-icon> 最新监测状态</span>
-          </div>
-          <div class="status-grid">
-            <div class="status-item">
-              <div class="status-label">在线状态</div>
-              <div class="status-value">{{ getOnlineText(selectedDetail?.equipmentStatus?.onlineStatus) }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">报警状态</div>
-              <div class="status-value">{{ getAlarmText(selectedDetail) }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">倾斜角度</div>
-              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.tiltAngle, '°') }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">电池电量</div>
-              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.batteryLevel, '%') }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">信号强度</div>
-              <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.signalStrength, '%') }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">水浸状态</div>
-              <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterInfiltrationAlarmStatus) }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">水位状态</div>
-              <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterLevelAlarmStatus) }}</div>
-            </div>
-            <div class="status-item">
-              <div class="status-label">激活状态</div>
-              <div class="status-value">{{ selectedDetail?.manholeData?.activationStatus || '-' }}</div>
-            </div>
-          </div>
-        </div>
-
-        <div class="threshold-card">
-          <div class="card-title">
-            <span><el-icon><Setting /></el-icon> 报警阈值设置</span>
-            <div class="card-actions">
-              <el-button :icon="Refresh" size="small" @click="resetThresholds">重置</el-button>
-              <el-button :loading="saveLoading" size="small" type="primary" @click="saveThresholds">保存设置</el-button>
-            </div>
-          </div>
-
-          <div class="threshold-tip">
-            当前版本先对接后端已支持的 3 类阈值:倾斜、水位、温度。
-          </div>
-
-          <el-tabs v-model="activeTab" class="threshold-tabs">
-            <el-tab-pane
-              v-for="item in thresholdConfigList"
-              :key="item.key"
-              :label="item.label"
-              :name="item.key"
-            >
-              <div class="threshold-item">
-                <div class="item-label">{{ item.desc }}</div>
-                <div class="item-control">
-                  <span class="field-label">最小值</span>
-                  <el-input-number
-                    v-model="thresholds[item.key].minValue"
-                    :controls="false"
-                    :precision="item.precision"
-                    :step="item.step"
-                    class="threshold-input"
-                    placeholder="未设置"
-                  />
-                  <span class="unit">{{ item.unit }}</span>
-
-                  <span class="field-label">最大值</span>
-                  <el-input-number
-                    v-model="thresholds[item.key].maxValue"
-                    :controls="false"
-                    :precision="item.precision"
-                    :step="item.step"
-                    class="threshold-input"
-                    placeholder="未设置"
-                  />
-                  <span class="unit">{{ item.unit }}</span>
-                </div>
-                <el-input
-                  v-model="thresholds[item.key].remark"
-                  maxlength="100"
-                  placeholder="请输入备注"
-                />
-                <div class="saved-hint">
-                  当前阈值配置将保存到后端预警规则
-                </div>
-              </div>
-            </el-tab-pane>
-          </el-tabs>
-        </div>
-      </div>
-
-      <div v-else class="empty-panel">
-        <el-empty description="请从左侧选择设备查看阈值配置" :image-size="140" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import {onBeforeUnmount, onMounted, ref} from 'vue'
-import {ElMessage} from 'element-plus'
-import {Location, Monitor, Refresh, Search, Setting, Warning} from '@element-plus/icons-vue'
-import {
-  getManholeAlarmDeviceDetail,
-  getManholeDevicePage,
-  saveManholeAlarmThreshold
-} from '@/api/pipeNetwork/basic'
-
-const thresholdConfigList = [
-  {
-    key: 'tilt',
-    label: '倾斜监测',
-    warningType: '倾斜预警',
-    warningCode: 'WARN-TILT',
-    unit: '°',
-    step: 0.1,
-    precision: 1,
-    desc: '当倾斜角度小于等于最小值或大于等于最大值时触发报警'
-  },
-  {
-    key: 'waterLevel',
-    label: '水位监测',
-    warningType: '水位预警',
-    warningCode: 'WARN-WATER-LEVEL',
-    unit: 'cm',
-    step: 0.1,
-    precision: 1,
-    desc: '用于井盖水位报警阈值配置'
-  },
-  {
-    key: 'temperature',
-    label: '温度监测',
-    warningType: '温度预警',
-    warningCode: 'WARN-TEMPERATURE',
-    unit: '℃',
-    step: 0.1,
-    precision: 1,
-    desc: '用于设备温度报警阈值配置'
-  }
-]
-
-const searchKeyword = ref('')
-const activeTab = ref('tilt')
-const listLoading = ref(false)
-const detailLoading = ref(false)
-const saveLoading = ref(false)
-const deviceList = ref([])
-const total = ref(0)
-const currentPage = ref(1)
-const pageSize = 8
-const selectedPoint = ref(null)
-const selectedDetail = ref(null)
-const thresholds = ref(createEmptyThresholds())
-
-// 搜索防抖
-let searchTimer = null
-function handleSearch() {
-  clearTimeout(searchTimer)
-  searchTimer = setTimeout(() => {
-    currentPage.value = 1
-    loadDeviceList()
-  }, 300)
-}
-
-function createEmptyThresholds() {
-  return thresholdConfigList.reduce((acc, item) => {
-    acc[item.key] = {
-      id: '',
-      minValue: null,
-      maxValue: null,
-      remark: ''
-    }
-    return acc
-  }, {})
-}
-
-function normalizeThresholdList(list) {
-  const safeList = Array.isArray(list) ? list : []
-  const result = createEmptyThresholds()
-  thresholdConfigList.forEach((item) => {
-    const matched = safeList.find(threshold => threshold.warningCode === item.warningCode)
-    if (matched) {
-      result[item.key] = {
-        id: matched.id || '',
-        minValue: matched.minValue ?? null,
-        maxValue: matched.maxValue ?? null,
-        remark: matched.remark || ''
-      }
-    }
-  })
-  return result
-}
-
-
-
-function getOnlineText(status) {
-  return Number(status) === 1 ? '在线' : '离线'
-}
-
-function getOnlineTagType(status) {
-  return Number(status) === 1 ? 'success' : 'info'
-}
-
-function getAlarmText(point) {
-  return Number(point?.equipmentStatus?.alarmStatus) === 1 ? '报警' : '正常'
-}
-
-function getAlarmTagType(point) {
-  return Number(point?.equipmentStatus?.alarmStatus) === 1 ? 'danger' : 'success'
-}
-
-function getUseStatusText(status) {
-  const map = {
-    1: '在用',
-    2: '闲置',
-    3: '维修',
-    4: '报废',
-    5: '待入库'
-  }
-  return map[Number(status)] || '未知'
-}
-
-function getUseStatusTagType(status) {
-  const map = {
-    1: 'success',
-    2: 'info',
-    3: 'warning',
-    4: 'danger',
-    5: ''
-  }
-  return map[Number(status)] || 'info'
-}
-
-function getBinaryAlarmText(status) {
-  return `${status}` === '1' ? '报警' : `${status}` === '0' ? '正常' : '-'
-}
-
-function appendUnit(value, unit) {
-  if (value === undefined || value === null || value === '') return '-'
-  return `${value}${unit}`
-}
-
-function formatDateTime(value) {
-  return value || '-'
-}
-
-async function loadDeviceList() {
-  listLoading.value = true
-  try {
-    const res = await getManholeDevicePage({
-      pageNum: currentPage.value,
-      pageSize,
-      deviceName: searchKeyword.value.trim() || null,
-      isQueryManholeData: true
-    })
-    const list = res.data?.records || []
-    deviceList.value = list
-    total.value = res.data?.total || list.length
-
-    if (!list.length) {
-      selectedPoint.value = null
-      selectedDetail.value = null
-      thresholds.value = createEmptyThresholds()
-      return
-    }
-
-    const currentId = selectedPoint.value?.equipmentId
-    const currentPoint = list.find(item => item.equipmentId === currentId) || list[0]
-    await selectPoint(currentPoint)
-  } catch (error) {
-    deviceList.value = []
-    selectedPoint.value = null
-    selectedDetail.value = null
-  } finally {
-    listLoading.value = false
-  }
-}
-
-async function selectPoint(point) {
-  if (!point?.equipmentId) return
-  selectedPoint.value = point
-  await loadDeviceDetail(point.equipmentId)
-}
-
-async function loadDeviceDetail(id) {
-  detailLoading.value = true
-  try {
-    const res = await getManholeAlarmDeviceDetail(id)
-    selectedDetail.value = res?.data || null
-    thresholds.value = normalizeThresholdList(selectedDetail.value?.warningThresholdList)
-  } catch (error) {
-    selectedDetail.value = null
-    thresholds.value = createEmptyThresholds()
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-async function resetThresholds() {
-  if (!selectedPoint.value?.equipmentId) return
-  await loadDeviceDetail(selectedPoint.value.equipmentId)
-  ElMessage.success('已恢复为服务端最新阈值')
-}
-
-function hasThresholdValue(item) {
-  return (
-    (item.minValue !== null && item.minValue !== undefined) ||
-    (item.maxValue !== null && item.maxValue !== undefined)
-  )
-}
-
-async function saveThresholds() {
-  if (!selectedDetail.value?.equipmentCode) {
-    ElMessage.warning('未获取到设备编码,无法保存')
-    return
-  }
-
-  const invalidConfigs = thresholdConfigList.filter((config) => {
-    const formItem = thresholds.value[config.key]
-    return formItem.id && !hasThresholdValue(formItem)
-  })
-
-  if (invalidConfigs.length) {
-    ElMessage.warning(`${invalidConfigs[0].label}请至少保留一个阈值`)
-    return
-  }
-
-  const payloads = thresholdConfigList
-    .map((config) => {
-      const formItem = thresholds.value[config.key]
-      if (!hasThresholdValue(formItem) && !formItem.id) return null
-      return {
-        id: formItem.id || undefined,
-        deviceCode: selectedDetail.value.equipmentCode,
-        warningType: config.warningType,
-        warningCode: config.warningCode,
-        minValue: formItem.minValue,
-        maxValue: formItem.maxValue,
-        remark: formItem.remark || ''
-      }
-    })
-    .filter(Boolean)
-
-  if (!payloads.length) {
-    ElMessage.warning('请先填写至少一项阈值')
-    return
-  }
-
-  saveLoading.value = true
-  try {
-    await Promise.all(payloads.map(item => saveManholeAlarmThreshold(item)))
-    ElMessage.success('阈值保存成功')
-    await loadDeviceDetail(selectedPoint.value.equipmentId)
-  } finally {
-    saveLoading.value = false
-  }
-}
-
-onMounted(() => {
-  loadDeviceList()
-})
-
-onBeforeUnmount(() => {
-  clearTimeout(searchTimer)
-})
-</script>
-
-<style scoped>
-.threshold-management {
-  padding: 20px;
-  background: #f0f2f6;
-  min-height: 100vh;
-}
-
-.main-content {
-  display: flex;
-  gap: 20px;
-}
-
-.point-list-panel {
-  width: 340px;
-  background: #fff;
-  border-radius: 16px;
-  padding: 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.panel-title {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-weight: 600;
-}
-
-.point-list {
-  max-height: calc(100vh - 180px);
-  overflow-y: auto;
-}
-
-.point-item {
-  border: 1px solid #e5e7eb;
-  border-radius: 12px;
-  padding: 14px;
-  margin-bottom: 12px;
-  cursor: pointer;
-  transition: all 0.2s ease;
-}
-
-.point-item:hover {
-  background: #f8fbff;
-  border-color: #bfdbfe;
-}
-
-.point-item.active {
-  background: #ecf5ff;
-  border-color: #409eff;
-}
-
-.point-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 8px;
-}
-
-.point-name {
-  font-weight: 600;
-  color: #303133;
-}
-
-.point-location {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  color: #606266;
-  font-size: 12px;
-  margin-bottom: 8px;
-}
-
-.point-meta {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  font-size: 12px;
-  color: #409eff;
-  margin-bottom: 8px;
-}
-
-.point-status-row {
-  display: flex;
-  gap: 8px;
-}
-
-.config-panel {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-
-.info-card,
-.threshold-card {
-  background: #fff;
-  border-radius: 16px;
-  padding: 18px 20px;
-  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
-}
-
-.card-title {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
-  padding-left: 12px;
-  border-left: 4px solid #409eff;
-  font-size: 16px;
-  font-weight: 600;
-}
-
-.card-actions {
-  display: flex;
-  gap: 8px;
-}
-
-.status-grid {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 12px;
-}
-
-.status-item {
-  background: #f8fafc;
-  border: 1px solid #edf2f7;
-  border-radius: 12px;
-  padding: 14px;
-}
-
-.status-label {
-  color: #909399;
-  font-size: 13px;
-  margin-bottom: 8px;
-}
-
-.status-value {
-  color: #303133;
-  font-size: 16px;
-  font-weight: 600;
-}
-
-.threshold-tip {
-  margin-bottom: 12px;
-  padding: 10px 12px;
-  border-radius: 8px;
-  background: #f4f9ff;
-  color: #606266;
-  font-size: 13px;
-}
-
-.threshold-item {
-  padding: 8px 4px 4px;
-}
-
-.item-label {
-  margin-bottom: 16px;
-  color: #606266;
-  font-size: 14px;
-}
-
-.item-control {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 8px;
-  margin-bottom: 16px;
-}
-
-.field-label {
-  color: #303133;
-  font-weight: 500;
-}
-
-.threshold-input {
-  width: 160px;
-}
-
-.unit {
-  color: #909399;
-  margin-right: 16px;
-}
-
-.saved-hint {
-  margin-top: 10px;
-  color: #909399;
-  font-size: 12px;
-}
-
-.empty-panel {
-  flex: 1;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: #fff;
-  border-radius: 16px;
-}
-
-.pagination-area {
-  padding: 8px 0 0;
-  display: flex;
-  justify-content: center;
-}
-</style>

+ 0 - 586
src/views/subSystem/manholeCover/device/index.vue

@@ -1,586 +0,0 @@
-<!--监测设备台账(窨井设备)-->
-<template>
-  <div class="manhole-cover-management">
-    <!-- 页面头部 -->
-    <div class="page-header">
-      <div class="stats-section">
-        <el-card class="stat-card" shadow="hover">
-          <div class="stat-content">
-            <el-icon><Monitor /></el-icon>
-            <div>
-              <div class="stat-label">在线设备</div>
-              <div class="stat-number">{{ onlineDevicesCount }}</div>
-            </div>
-          </div>
-        </el-card>
-        <el-card class="stat-card" shadow="hover">
-          <div class="stat-content">
-            <el-icon><CircleClose /></el-icon>
-            <div>
-              <div class="stat-label">离线设备</div>
-              <div class="stat-number">{{ offlineDevicesCount }}</div>
-            </div>
-          </div>
-        </el-card>
-        <el-card class="stat-card" shadow="hover">
-          <div class="stat-content">
-            <el-icon><Location /></el-icon>
-            <div>
-              <div class="stat-label">设备总数</div>
-              <div class="stat-number">{{ totalDevicesCount }}</div>
-            </div>
-          </div>
-        </el-card>
-      </div>
-    </div>
-
-    <!-- 筛选工具栏 -->
-    <div class="filter-bar">
-      <el-input
-        v-model="searchKeyword"
-        placeholder="搜索设备名称"
-        clearable
-        style="width: 240px"
-        :prefix-icon="Search"
-        @keyup.enter="handleSearch"
-        @clear="handleSearch"
-      />
-      <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width: 160px" @change="handleSearch">
-        <el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
-      </el-select>
-      <el-button type="primary" :icon="Refresh" @click="resetFilters">重置筛选</el-button>
-    </div>
-
-    <!-- 设备列表展示区 -->
-    <div class="device-list-container">
-      <el-table
-        :data="devices"
-        v-loading="loading"
-        stripe
-        border
-        style="width: 100%"
-        :row-class-name="tableRowClassName"
-        @row-click="handleRowClick"
-      >
-        <el-table-column prop="deviceCode" label="设备编号" width="150" show-overflow-tooltip />
-        <el-table-column prop="deviceName" label="设备名称" width="180" show-overflow-tooltip />
-        <el-table-column prop="typeName" label="设备类型" width="130">
-          <template #default="{ row }">
-            <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="location" label="设备位置" min-width="200" show-overflow-tooltip />
-        <el-table-column prop="lastMaintainDate" label="最近运维时间" width="130">
-          <template #default="{ row }">{{ row.lastMaintainDate || '—' }}</template>
-        </el-table-column>
-        <el-table-column prop="onlineStatus" label="在线状态" width="110" align="center">
-          <template #default="{ row }">
-            <el-tag :type="getOnlineTagType(row.onlineStatus)" size="small">{{ getOnlineText(row.onlineStatus) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="180" fixed="right">
-          <template #default="{ row }">
-            <el-button link type="primary" size="small" @click.stop="showDetail(row)">详情</el-button>
-            <el-button link type="success" size="small" @click.stop="openRelDialog(row)">关联井下设备</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <!-- 分页 -->
-      <div class="pagination-wrapper">
-        <el-pagination
-          background
-          layout="total, prev, pager, next"
-          :total="total"
-          :page-size="pageSize"
-          v-model:current-page="currentPage"
-          @current-change="loadDevices"
-        />
-      </div>
-    </div>
-
-    <!-- 设备详情抽屉 -->
-    <el-drawer v-model="drawerVisible" title="设备电子档案 - 详细信息" size="40%" direction="rtl" v-loading="detailLoading" class="detail-drawer">
-      <template v-if="selectedDevice">
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="设备编号">{{ selectedDevice.deviceCode }}</el-descriptions-item>
-          <el-descriptions-item label="设备名称">{{ selectedDevice.deviceName }}</el-descriptions-item>
-          <el-descriptions-item label="设备类型">{{ getTypeText(selectedDevice) }}</el-descriptions-item>
-          <el-descriptions-item label="在线状态">
-            <el-tag :type="getOnlineTagType(selectedDevice.onlineStatus)">{{ getOnlineText(selectedDevice.onlineStatus) }}</el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="设备位置">{{ selectedDevice.location || '—' }}</el-descriptions-item>
-          <el-descriptions-item label="最近运维时间">{{ selectedDevice.lastMaintainDate || '—' }}</el-descriptions-item>
-          <el-descriptions-item label="电池电量/健康度" v-if="selectedDevice.batteryLevel !== null && selectedDevice.batteryLevel !== undefined && selectedDevice.batteryLevel !== ''">{{ selectedDevice.batteryLevel }}%</el-descriptions-item>
-          <el-descriptions-item label="最后通讯时间">{{ selectedDevice.lastCommunication || '—' }}</el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">动态监测指标</el-divider>
-        <div class="monitor-metrics">
-          <el-row :gutter="12">
-            <el-col :span="12"><el-statistic title="井盖倾斜角" :value="selectedDevice.metrics?.tilt ?? '—'" suffix="°" /></el-col>
-            <el-col :span="12"><el-statistic title="水浸状态" :value="selectedDevice.metrics?.waterIntrusion ? '已浸水' : '正常'" /></el-col>
-            <el-col :span="12"><el-statistic title="温度" :value="selectedDevice.metrics?.temperature ?? '—'" suffix="℃" /></el-col>
-            <el-col :span="12"><el-statistic title="信号量" :value="selectedDevice.metrics?.signal ?? '—'" /></el-col>
-          </el-row>
-        </div>
-
-        <el-divider content-position="left">预警阈值</el-divider>
-        <el-table v-if="selectedDevice.warningThresholds.length" :data="selectedDevice.warningThresholds" size="small" border>
-          <el-table-column prop="warningType" label="预警类型" width="100" />
-          <el-table-column prop="warningCode" label="预警编码" width="120" />
-          <el-table-column prop="minValue" label="最小值" width="90" />
-          <el-table-column prop="maxValue" label="最大值" width="90" />
-          <el-table-column prop="remark" label="备注" show-overflow-tooltip />
-        </el-table>
-        <div v-else style="color:#909399;font-size:13px;">暂无预警阈值配置</div>
-
-        <div style="margin-top: 20px; text-align: right;">
-          <el-button type="primary" @click="closeDrawer">关闭</el-button>
-        </div>
-      </template>
-    </el-drawer>
-
-    <!-- 关联设备管理对话框 -->
-    <el-dialog v-model="relDialogVisible" :title="`井下设备关联 - ${relDevice?.deviceName || ''}`" width="860px" @close="onRelDialogClose">
-      <div class="rel-container">
-        <!-- 已关联设备列表 -->
-        <div class="rel-section">
-          <div class="rel-section-title">
-            <span>已关联设备</span>
-            <el-tag type="info" size="small">{{ relList.length }} 台</el-tag>
-          </div>
-          <el-table :data="relList" border size="small" v-loading="relLoading" max-height="240">
-            <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
-            <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
-            <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
-            <el-table-column label="操作" width="90" align="center">
-              <template #default="{ row }">
-                <el-button link type="danger" size="small" @click="removeRel(row)">取消关联</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-        </div>
-
-        <!-- 搜索可选设备 -->
-        <div class="rel-section">
-          <div class="rel-section-title">
-            <span>添加关联设备</span>
-            <el-input
-              v-model="relSearchKeyword"
-              placeholder="搜索设备编号/名称/位置"
-              clearable
-              size="small"
-              style="width: 260px"
-              :prefix-icon="Search"
-              @keyup.enter="searchRelDevices"
-              @clear="searchRelDevices"
-            />
-          </div>
-          <el-table :data="searchResults" border size="small" v-loading="relSearching" max-height="240" @selection-change="handleSearchSelectionChange">
-            <el-table-column type="selection" width="45" />
-            <el-table-column prop="equipmentCode" label="设备编号" min-width="140" show-overflow-tooltip />
-            <el-table-column prop="equipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
-            <el-table-column prop="equipmentLocation" label="位置" min-width="160" show-overflow-tooltip />
-            <el-table-column label="类型" width="120">
-              <template #default="{ row }">
-                <el-tag size="small" effect="plain">{{ getTypeText(row) }}</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-          <div class="rel-search-footer">
-            <el-button type="primary" size="small" :disabled="!searchSelection.length" :loading="relSaving" @click="batchAddRel">关联选中设备 ({{ searchSelection.length }})</el-button>
-          </div>
-        </div>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { Monitor, CircleClose, Location, Search, Refresh } from '@element-plus/icons-vue'
-import { getManholeDevicePage, getManholeDeviceById, getEquipmentTypeList, searchManholeRelDevices, getManholeDeviceRels, addManholeDeviceRel, deleteManholeDeviceRel } from '@/api/pipeNetwork/basic'
-import { ElMessage } from 'element-plus'
-
-// 窨井设备类别:供水窨井(type_id=5)/ 排水窨井(type_id=9)/ 燃气窨井(type_id=10)
-const MANHOLE_CATEGORY_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
-
-// 设备列表 & 分页
-const devices = ref([])
-const loading = ref(false)
-const total = ref(0)
-const currentPage = ref(1)
-const pageSize = 10
-
-// 筛选条件
-const searchKeyword = ref('')
-const typeFilter = ref('')
-// 窨井类别选项(value=窨井类别ID:供水窨井5/排水窨井9/燃气窨井10)
-const typeOptions = ref([
-  { value: '5', label: '供水窨井' },
-  { value: '9', label: '排水窨井' },
-  { value: '10', label: '燃气窨井' }
-])
-// 全部类型:id -> { typeId, typeName, parentTypeId }
-const typeIdMap = ref({})
-
-// 统计
-const onlineDevicesCount = ref(0)
-const offlineDevicesCount = ref(0)
-const totalDevicesCount = ref(0)
-
-// 加载全部设备类型(用于类型列归属显示)
-async function loadTypeOptions() {
-  try {
-    const res = await getEquipmentTypeList()
-    const all = res.data || []
-    all.forEach(t => { typeIdMap.value[t.id] = t })
-  } catch (e) { /* 类型接口异常时,类型列回退显示 typeName */ }
-}
-
-// 设备类型显示文本(按所属窨井类别显示)
-const getTypeText = (row) => {
-  const t = typeIdMap.value[row.equipmentTypeId]
-  if (!t) return row.typeName || '—'
-  return MANHOLE_CATEGORY_MAP[t.parentTypeId] || MANHOLE_CATEGORY_MAP[row.equipmentTypeId] || t.typeName || '—'
-}
-
-// 加载设备列表(后端已按窨井设备类型过滤)
-async function loadDevices() {
-  loading.value = true
-  try {
-    const res = await getManholeDevicePage({
-      pageNum: currentPage.value,
-      pageSize,
-      deviceName: searchKeyword.value || null,
-      equipmentTypeId: typeFilter.value || null,
-      isQueryManholeData: true
-    })
-    const rows = res.data?.records || []
-    devices.value = rows.map(d => ({
-      equipmentId: d.equipmentId,
-      equipmentTypeId: d.equipmentTypeId,
-      deviceCode: d.equipmentCode,
-      deviceName: d.equipmentName,
-      typeName: d.equipmentTypeName,
-      location: d.equipmentLocation,
-      onlineStatus: d.equipmentStatus?.onlineStatus,
-      lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
-      batteryLevel: d.manholeData?.batteryLevel,
-      lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
-      metrics: d.manholeData ? {
-        tilt: d.manholeData.tiltAngle,
-        waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
-        temperature: d.manholeData.temperatureValue,
-        signal: d.manholeData.signalStrength
-      } : null
-    }))
-    total.value = res.data?.total || 0
-  } catch (e) {
-    ElMessage.error('设备列表加载失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-// 统计(基于当前筛选条件下的全量设备)
-async function loadStats() {
-  try {
-    const res = await getManholeDevicePage({
-      pageNum: 1,
-      pageSize: 1000,
-      deviceName: searchKeyword.value || null,
-      equipmentTypeId: typeFilter.value || null,
-      isQueryManholeData: false
-    })
-    const rows = res.data?.records || []
-    totalDevicesCount.value = res.data?.total || rows.length
-    onlineDevicesCount.value = rows.filter(d => Number(d.equipmentStatus?.onlineStatus) === 1).length
-    offlineDevicesCount.value = rows.length - onlineDevicesCount.value
-  } catch (e) {
-    onlineDevicesCount.value = 0
-    offlineDevicesCount.value = 0
-    totalDevicesCount.value = 0
-  }
-}
-
-// 在线状态
-const getOnlineText = (status) => {
-  return Number(status) === 1 ? '在线' : '离线'
-}
-const getOnlineTagType = (status) => {
-  return Number(status) === 1 ? 'success' : 'info'
-}
-
-// 搜索/筛选变化
-const handleSearch = () => {
-  currentPage.value = 1
-  loadDevices()
-  loadStats()
-}
-
-const resetFilters = () => {
-  searchKeyword.value = ''
-  typeFilter.value = ''
-  currentPage.value = 1
-  loadDevices()
-  loadStats()
-}
-
-const tableRowClassName = ({ row }) => {
-  if (Number(row.onlineStatus) !== 1) return 'inactive-row'
-  return ''
-}
-
-// 详情
-const drawerVisible = ref(false)
-const detailLoading = ref(false)
-const selectedDevice = ref(null)
-
-// 映射详情接口返回数据
-const mapDeviceDetail = (d) => ({
-  equipmentId: d.equipmentId,
-  equipmentTypeId: d.equipmentTypeId,
-  deviceCode: d.equipmentCode,
-  deviceName: d.equipmentName,
-  typeName: d.equipmentTypeName,
-  location: d.equipmentLocation,
-  onlineStatus: d.equipmentStatus?.onlineStatus,
-  lastMaintainDate: d.equipmentStatus?.lastMaintainDate,
-  batteryLevel: d.manholeData?.batteryLevel,
-  lastCommunication: d.manholeData?.uploadTime || d.manholeData?.createTime,
-  metrics: d.manholeData ? {
-    tilt: d.manholeData.tiltAngle,
-    waterIntrusion: Number(d.manholeData.waterInfiltrationAlarmStatus) === 1,
-    temperature: d.manholeData.temperatureValue,
-    signal: d.manholeData.signalStrength
-  } : null,
-  warningThresholds: d.warningThresholdList || []
-})
-
-const showDetail = async (device) => {
-  drawerVisible.value = true
-  detailLoading.value = true
-  try {
-    const res = await getManholeDeviceById(device.equipmentId)
-    selectedDevice.value = res.data ? mapDeviceDetail(res.data) : null
-  } catch (e) {
-    selectedDevice.value = null
-    ElMessage.error('设备详情加载失败')
-  } finally {
-    detailLoading.value = false
-  }
-}
-const handleRowClick = (row) => {
-  showDetail(row)
-}
-const closeDrawer = () => {
-  drawerVisible.value = false
-}
-
-// ============ 关联设备管理 ============
-const relDialogVisible = ref(false)
-const relDevice = ref(null)
-const relList = ref([])
-const relLoading = ref(false)
-const relSearchKeyword = ref('')
-const searchResults = ref([])
-const relSearching = ref(false)
-const searchSelection = ref([])
-const relSaving = ref(false)
-
-const openRelDialog = async (device) => {
-  relDevice.value = device
-  relDialogVisible.value = true
-  relSearchKeyword.value = ''
-  searchResults.value = []
-  searchSelection.value = []
-  await loadRelList(device.equipmentId)
-  await searchRelDevices()
-}
-
-const loadRelList = async (equipmentId) => {
-  relLoading.value = true
-  try {
-    const res = await getManholeDeviceRels(equipmentId)
-    relList.value = res.data || []
-  } catch (e) {
-    relList.value = []
-    ElMessage.error('关联设备列表加载失败')
-  } finally {
-    relLoading.value = false
-  }
-}
-
-const searchRelDevices = async () => {
-  relSearching.value = true
-  try {
-    const keyword = relSearchKeyword.value.trim()
-    // 排除已关联设备ID + 自身设备ID
-    const excludeIds = [
-      ...relList.value.map(r => r.equipmentId),
-      relDevice.value?.equipmentId
-    ].filter(Boolean).join(',')
-    const res = await searchManholeRelDevices({
-      keyword: keyword || undefined,
-      excludeIds: excludeIds || undefined
-    })
-    searchResults.value = res.data || []
-  } catch (e) {
-    searchResults.value = []
-    ElMessage.error('设备搜索失败')
-  } finally {
-    relSearching.value = false
-  }
-}
-
-const handleSearchSelectionChange = (selection) => {
-  searchSelection.value = selection || []
-}
-
-const batchAddRel = async () => {
-  if (!searchSelection.value.length) return
-  relSaving.value = true
-  try {
-    for (const item of searchSelection.value) {
-      await addManholeDeviceRel({
-        manholeId: relDevice.value.equipmentId,
-        equipmentId: item.equipmentId
-      })
-    }
-    ElMessage.success(`已关联 ${searchSelection.value.length} 台设备`)
-    searchSelection.value = []
-    relSearchKeyword.value = ''
-    await loadRelList(relDevice.value.equipmentId)
-    await searchRelDevices()
-  } catch (e) {
-    const msg = e?.message || e?.msg || '关联失败'
-    ElMessage.error(msg)
-  } finally {
-    relSaving.value = false
-  }
-}
-
-const removeRel = async (row) => {
-  try {
-    await deleteManholeDeviceRel(row.relId)
-    ElMessage.success('已取消关联')
-    await loadRelList(relDevice.value.equipmentId)
-    await searchRelDevices()
-  } catch (e) {
-    const msg = e?.message || e?.msg || '取消关联失败'
-    ElMessage.error(msg)
-  }
-}
-
-const onRelDialogClose = () => {
-  relList.value = []
-  searchResults.value = []
-  searchSelection.value = []
-  relSearchKeyword.value = ''
-}
-
-// 初始化
-onMounted(() => {
-  loadTypeOptions()
-  loadDevices()
-  loadStats()
-})
-</script>
-
-<style scoped>
-.manhole-cover-management {
-  padding: 20px;
-  background: #f5f7fa;
-  min-height: 100vh;
-}
-.page-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20px;
-  flex-wrap: wrap;
-}
-.stats-section {
-  display: flex;
-  gap: 16px;
-}
-.stat-card {
-  width: 160px;
-  border-radius: 10px;
-}
-.stat-content {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.stat-content .el-icon {
-  font-size: 32px;
-  color: #409eff;
-}
-.stat-label {
-  font-size: 14px;
-  color: #6c757d;
-}
-.stat-number {
-  font-size: 24px;
-  font-weight: bold;
-  color: #2c3e50;
-}
-.filter-bar {
-  margin-bottom: 20px;
-  display: flex;
-  gap: 12px;
-  flex-wrap: wrap;
-  background: white;
-  padding: 12px 16px;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
-}
-.device-list-container {
-  background: white;
-  border-radius: 12px;
-  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
-  overflow: hidden;
-}
-.pagination-wrapper {
-  display: flex;
-  justify-content: flex-end;
-  padding: 15px 20px;
-  background: white;
-  border-top: 1px solid #e4e7ed;
-}
-.monitor-metrics {
-  background: #f8f9fc;
-  border-radius: 8px;
-  padding: 10px;
-}
-:deep(.inactive-row) {
-  --el-table-tr-bg-color: #f5f5f5;
-  color: #a8a8a8;
-}
-.rel-container {
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-.rel-section {
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
-  padding: 12px;
-}
-.rel-section-title {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 10px;
-  font-size: 14px;
-  font-weight: 600;
-  color: #2c3e50;
-}
-.rel-search-footer {
-  margin-top: 10px;
-  text-align: right;
-}
-</style>

+ 8 - 2
src/views/subSystem/manholeCover/home/index.vue

@@ -126,13 +126,19 @@ let alarmChart = null, areaChart = null, deviceTypeChart = null
 
 // ==================== 常量映射 ====================
 const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+
+// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
+const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
+function districtLabel(code) {
+  return code == null ? '' : (DISTRICT_MAP[code] || String(code))
+}
 const tableHeaderStyle = { background: 'rgba(0,100,200,0.3)', color: '#fff', borderColor: '#2b4b8c' }
 
 const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
 const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
 /** 待办工单口径:待派单/待接单/已接单/处理中/延期中(枚举派生,见 workOrderDict.js) */
 const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
-/** 设备父类别ID(equipment_type.parent_type_id),与工单列表页保持同一口径 */
+/** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
 const DEVICE_TYPE_PARENT_ID = '4'
 const typeText = (id) => TYPE_MAP[id] || '-'
 const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
@@ -174,7 +180,7 @@ async function loadDevices() {
       return {
         id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
         typeId: d.equipmentTypeId, typeName: d.equipmentTypeName,
-        location: d.equipmentLocation, district: d.district,
+        location: d.equipmentLocation, district: districtLabel(d.district),
         lng: d.longitude != null ? Number(d.longitude) : null,
         lat: d.latitude != null ? Number(d.latitude) : null,
         battery: d.manholeData?.batteryLevel ?? null,

+ 8 - 2
src/views/subSystem/manholeCover/operation/faultMana.vue

@@ -124,7 +124,7 @@
           <el-descriptions-item label="设备编号">{{ selectedDevice.code }}</el-descriptions-item>
           <el-descriptions-item label="设备名称">{{ selectedDevice.name }}</el-descriptions-item>
           <el-descriptions-item label="设备类型">{{ selectedDevice.deviceTypeName }}</el-descriptions-item>
-          <el-descriptions-item label="所属片区">{{ selectedDevice.district || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="所属片区">{{ districtLabel(selectedDevice.district) || '—' }}</el-descriptions-item>
           <el-descriptions-item label="详细地址">{{ selectedDevice.location }}</el-descriptions-item>
           <el-descriptions-item label="经纬度">{{ selectedDevice.lng ?? '—' }}, {{ selectedDevice.lat ?? '—' }}</el-descriptions-item>
           <el-descriptions-item label="故障类型">{{ selectedDevice.faultTypeName }}</el-descriptions-item>
@@ -240,6 +240,12 @@ import locationIcon from '@/assets/images/location.png'
 
 // ==================== 常量 ====================
 const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+
+// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
+const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
+function districtLabel(code) {
+  return code == null ? '' : (DISTRICT_MAP[code] || String(code))
+}
 const LOW_BATTERY_THRESHOLD = 15
 const LOW_SIGNAL_THRESHOLD = 20
 const ORDER_STATUS_MAP = { 0: '待派单', 1: '待接单', 2: '已接单', 3: '处理中', 4: '待验收', 5: '已结案', 6: '延期中', 7: '已取消' }
@@ -330,7 +336,7 @@ function normalizeDevice(raw) {
     equipmentId: raw?.equipmentId || '',
     name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
     deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
-    district: raw?.district || '',
+    district: districtLabel(raw?.district),
     location: raw?.equipmentLocation || '暂无位置信息',
     locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
     lng: raw?.longitude != null ? Number(raw.longitude) : null,

+ 7 - 1
src/views/subSystem/manholeCover/operation/mOdataStatistics.vue

@@ -215,6 +215,12 @@ import {
 
 // ==================== 常量 ====================
 const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
+
+// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
+const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
+function districtLabel(code) {
+  return code == null ? '' : (DISTRICT_MAP[code] || String(code))
+}
 const LOW_BATTERY_THRESHOLD = 15
 const LOW_SIGNAL_THRESHOLD = 20
 
@@ -307,7 +313,7 @@ function normalizeDevice(raw) {
     deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
     location: raw?.equipmentLocation || '暂无位置信息',
     locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
-    district: raw?.district || '',
+    district: districtLabel(raw?.district),
     battery, signal, temperature, tiltAngle, angleThreshold,
     status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal, offline },
     faultTypes, hasAlert,

+ 2 - 2
src/views/subSystem/pipeNetwork/ops/workOrder/list.vue

@@ -109,7 +109,7 @@ const route = useRoute()
  * 本组件服务于「案件管理」下两个列表菜单,展示与运维工单列表一致,仅查询口径不同:
  * - 经办工单:未办结状态(待派单 / 待接单 / 已接单 / 处理中 / 待验收 / 延期中)
  * - 办结工单:已结案
- * 两者都只查 equipment_type.parent_type_id = 4(案件类设备)下的工单。
+ * 两者都只查 equipment_type.parent_id = 4(案件类设备)下的工单。
  *
  * 判定方式:同一组件由后台 sys_menu.component 指向本文件,菜单的 path 与菜单名会分别体现在
  * 路由 path、route.meta.title 中,因此按关键字区分来源;两者都未命中时走默认(不限状态),
@@ -134,7 +134,7 @@ const MENU_SCOPES = [
   }
 ]
 
-/** 案件类设备的父类别ID(equipment_type.parent_type_id),本列表仅查询该类设备产生的工单 */
+/** 案件类设备的父类别ID(equipment_type.parent_id),本列表仅查询该类设备产生的工单 */
 const CASE_DEVICE_TYPE_PARENT_ID = '4'
 
 const menuScope = computed(() => {

+ 0 - 16
src/views/subSystem/pipeNetwork/pdeviceInform/Pbasicinfo.vue

@@ -315,22 +315,6 @@
                 />
               </el-table>
             </div>
-
-            <div class="side-actions">
-              <el-button
-                type="primary"
-                icon="Edit"
-                @click="handleUpdate(selectedNetwork)"
-                >编辑设施</el-button
-              >
-              <el-button
-                type="danger"
-                plain
-                icon="Delete"
-                @click="handleDelete(selectedNetwork)"
-                >删除设施</el-button
-              >
-            </div>
           </template>
         </el-card>
       </el-col>

+ 0 - 850
src/views/subSystem/pipeNetwork/pdeviceInform/PequipmentLed.vue

@@ -1,850 +0,0 @@
-<!-- 物联感知设备管理 -->
-<template>
-  <div class="app-container equipment-page">
-    <div class="summary-grid">
-      <el-card shadow="hover" class="summary-card primary">
-        <div class="summary-label">设备总数</div>
-        <div class="summary-value">{{ summary.total }}</div>
-        <div class="summary-desc">平台已接入物联感知设备</div>
-      </el-card>
-      <el-card shadow="hover" class="summary-card success">
-        <div class="summary-label">在用设备</div>
-        <div class="summary-value">{{ summary.active }}</div>
-        <div class="summary-desc">当前状态为在用的设备</div>
-      </el-card>
-      <el-card shadow="hover" class="summary-card warning">
-        <div class="summary-label">维修/待处理</div>
-        <div class="summary-value">{{ summary.warning }}</div>
-        <div class="summary-desc">维修、待入库、闲置等状态</div>
-      </el-card>
-      <el-card shadow="hover" class="summary-card info">
-        <div class="summary-label">设备类型</div>
-        <div class="summary-value">{{ summary.typeCount }}</div>
-        <div class="summary-desc">当前已维护的设备分类数</div>
-      </el-card>
-    </div>
-
-    <el-card shadow="never" class="query-card">
-      <el-form ref="queryRef" :model="queryParams" :inline="true" v-show="showSearch">
-        <el-form-item label="设备编码" prop="equipmentCode">
-          <el-input
-            v-model="queryParams.equipmentCode"
-            placeholder="请输入设备编码"
-            clearable
-            style="width: 220px"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="设备名称" prop="equipmentName">
-          <el-input
-            v-model="queryParams.equipmentName"
-            placeholder="请输入设备名称"
-            clearable
-            style="width: 220px"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="设备型号" prop="equipmentModel">
-          <el-input
-            v-model="queryParams.equipmentModel"
-            placeholder="请输入设备型号"
-            clearable
-            style="width: 220px"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="制造商" prop="manufacturer">
-          <el-input
-            v-model="queryParams.manufacturer"
-            placeholder="请输入制造商"
-            clearable
-            style="width: 220px"
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-row :gutter="16" class="main-row">
-      <el-col :xs="24" :lg="16">
-        <el-card shadow="never" class="table-card">
-          <template #header>
-            <div class="card-header-row">
-              <div>
-                <div class="card-title">设备台账</div>
-                <div class="card-subtitle">对平台的传感器类型监测设备的基本信息进行管理</div>
-              </div>
-              <div class="card-actions">
-                <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
-                <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
-                <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
-              </div>
-            </div>
-          </template>
-
-          <el-table
-            v-loading="loading"
-            :data="equipmentList"
-            highlight-current-row
-            @selection-change="handleSelectionChange"
-            @current-change="handleCurrentChange"
-            @row-click="handleRowClick"
-          >
-            <el-table-column type="selection" width="55" align="center" />
-            <el-table-column label="设备编码" align="center" prop="equipmentCode" min-width="140" />
-            <el-table-column label="设备名称" align="center" prop="equipmentName" min-width="160" show-overflow-tooltip />
-            <el-table-column label="设备型号" align="center" prop="equipmentModel" min-width="120" />
-            <el-table-column label="设备类型" align="center" min-width="140">
-              <template #default="scope">
-                {{ typeNameMap[scope.row.equipmentTypeId] || scope.row.equipmentTypeId || "-" }}
-              </template>
-            </el-table-column>
-            <el-table-column label="资产状态" align="center" width="110">
-              <template #default="scope">
-                <el-tag :type="getStatusTagType(getRowStatus(scope.row.equipmentId))">
-                  {{ getStatusText(getRowStatus(scope.row.equipmentId)) }}
-                </el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column label="制造商" align="center" prop="manufacturer" min-width="140" show-overflow-tooltip />
-            <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
-              <template #default="scope">
-                <el-button link type="primary" icon="Edit" @click.stop="handleUpdate(scope.row)">修改</el-button>
-                <el-button link type="warning" icon="Tools" @click.stop="openStatusDialog(scope.row)">资产状态维护</el-button>
-                <el-button link type="success" icon="Aim" @click.stop="openTestDialog(scope.row)">测试验证</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-
-          <pagination
-            v-show="total > 0"
-            :total="total"
-            v-model:page="queryParams.pageNum"
-            v-model:limit="queryParams.pageSize"
-            @pagination="getList"
-          />
-        </el-card>
-      </el-col>
-
-      <el-col :xs="24" :lg="8">
-        <el-card shadow="never" class="detail-card">
-          <template #header>
-            <div class="card-header-row">
-              <div>
-                <div class="card-title">设备详情</div>
-                <div class="card-subtitle">选中设备后查看基本信息、关联点位与测试记录</div>
-              </div>
-            </div>
-          </template>
-
-          <el-empty v-if="!selectedDetail.equipmentInfo" description="请从左侧选择一台设备" />
-
-          <template v-else>
-            <div class="detail-hero">
-              <div class="hero-title">{{ selectedDetail.equipmentInfo.equipmentName }}</div>
-              <div class="hero-meta">
-                <span>{{ selectedDetail.equipmentInfo.equipmentCode }}</span>
-                <span>{{ selectedDetail.typeInfo?.typeName || "-" }}</span>
-                <span>{{ getStatusText(selectedDetail.statusInfo?.currentStatus) }}</span>
-              </div>
-            </div>
-
-            <el-descriptions :column="1" border size="small">
-              <el-descriptions-item label="设备型号">{{ selectedDetail.equipmentInfo.equipmentModel || "-" }}</el-descriptions-item>
-              <el-descriptions-item label="制造商">{{ selectedDetail.equipmentInfo.manufacturer || "-" }}</el-descriptions-item>
-              <el-descriptions-item label="设备位置">{{ selectedDetail.equipmentInfo.equipmentLocation || "-" }}</el-descriptions-item>
-              <el-descriptions-item label="规格参数">{{ selectedDetail.equipmentInfo.equipmentSpec || "-" }}</el-descriptions-item>
-              <el-descriptions-item label="维护人员">
-                {{ selectedDetail.equipmentInfo.maintainer || "-" }}
-              </el-descriptions-item>
-              <el-descriptions-item label="联系电话">
-                {{ selectedDetail.equipmentInfo.maintainerPhone || "-" }}
-              </el-descriptions-item>
-            </el-descriptions>
-
-            <div class="section-block">
-              <div class="section-title">关联点位</div>
-              <el-table :data="selectedDetail.pointRelList || []" size="small" max-height="180">
-                <el-table-column label="监测点编码" prop="pointCode" min-width="160" show-overflow-tooltip />
-                <el-table-column label="监测点名称" prop="pointName" min-width="160" show-overflow-tooltip />
-              </el-table>
-            </div>
-
-            <div class="section-block">
-              <div class="section-title">测试记录</div>
-              <el-table :data="selectedDetail.testList || []" size="small" max-height="180">
-                <el-table-column label="时间" prop="testTime" min-width="150" />
-                <el-table-column label="类型" prop="testType" min-width="100" />
-                <el-table-column label="结果" prop="testResult" width="80" />
-              </el-table>
-            </div>
-          </template>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-dialog :title="title" v-model="open" width="860px" append-to-body>
-      <el-form ref="equipmentRef" :model="form" :rules="rules" label-width="110px">
-        <el-row :gutter="16">
-          <el-col :span="12">
-            <el-form-item label="设备编码" prop="equipmentCode">
-              <el-input v-model="form.equipmentCode" placeholder="请输入设备编码" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备名称" prop="equipmentName">
-              <el-input v-model="form.equipmentName" placeholder="请输入设备名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备型号" prop="equipmentModel">
-              <el-input v-model="form.equipmentModel" placeholder="请输入设备型号" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="设备类型" prop="equipmentTypeId">
-              <el-tree-select
-                v-model="form.equipmentTypeId"
-                :data="equipmentTypeTree"
-                node-key="id"
-                :props="{ label: 'typeName', children: 'children', disabled: 'disabled' }"
-                default-expand-all
-                :render-after-expand="false"
-                placeholder="请选择设备类型"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="制造商" prop="manufacturer">
-              <el-input v-model="form.manufacturer" placeholder="请输入制造商" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="生产日期" prop="productionDate">
-              <el-date-picker
-                v-model="form.productionDate"
-                type="date"
-                value-format="YYYY-MM-DD"
-                placeholder="请选择生产日期"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="规格参数" prop="equipmentSpec">
-              <el-input v-model="form.equipmentSpec" placeholder="请输入设备规格参数" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="资产价值(元)" prop="assetValue">
-              <el-input-number v-model="form.assetValue" :precision="2" :step="100" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="使用年限(年)" prop="useLife">
-              <el-input-number v-model="form.useLife" :precision="0" :step="1" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="设备位置" prop="equipmentLocation">
-              <el-input v-model="form.equipmentLocation" placeholder="请输入设备位置" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="经度" prop="longitude">
-              <el-input-number v-model="form.longitude" :precision="6" :step="0.000001" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="纬度" prop="latitude">
-              <el-input-number v-model="form.latitude" :precision="6" :step="0.000001" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="安装时间" prop="installTime">
-              <el-date-picker
-                v-model="form.installTime"
-                type="datetime"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                placeholder="请选择安装时间"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="维护人员" prop="maintainer">
-              <el-input v-model="form.maintainer" placeholder="请输入维护人员" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="联系电话" prop="maintainerPhone">
-              <el-input v-model="form.maintainerPhone" placeholder="请输入联系电话" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="备注" prop="remark">
-              <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <el-dialog title="设备状态维护" v-model="statusOpen" width="420px" append-to-body>
-      <el-form label-width="90px">
-        <el-form-item label="设备名称">
-          <span>{{ currentRow.equipmentName || "-" }}</span>
-        </el-form-item>
-        <el-form-item label="资产状态">
-          <el-select v-model="statusForm.currentStatus" style="width: 100%">
-            <el-option
-              v-for="item in equipmentStatusOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitStatus">确 定</el-button>
-          <el-button @click="statusOpen = false">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <el-dialog title="设备测试验证" v-model="testOpen" width="520px" append-to-body>
-      <el-form ref="testRef" :model="testForm" label-width="90px">
-        <el-form-item label="设备名称">
-          <span>{{ currentRow.equipmentName || "-" }}</span>
-        </el-form-item>
-        <el-form-item label="测试类型">
-          <el-input v-model="testForm.testType" placeholder="例如:通信测试" />
-        </el-form-item>
-        <el-form-item label="测试人员">
-          <el-input v-model="testForm.testUser" placeholder="请输入测试人员" />
-        </el-form-item>
-        <el-form-item label="测试详情">
-          <el-input v-model="testForm.testDetail" type="textarea" :rows="4" placeholder="请输入测试详情" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitTest">确 定</el-button>
-          <el-button @click="testOpen = false">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup>
-import { computed, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
-import {
-  addEquipment,
-  delEquipment,
-  getEquipment,
-  getEquipmentDetail,
-  listEquipment,
-  listEquipmentStatusAll,
-  listEquipmentTypeAll,
-  testEquipment,
-  updateEquipment,
-  updateEquipmentStatus
-} from "@/api/pipeNetwork/pdeviceInform";
-
-const { proxy } = getCurrentInstance();
-
-const equipmentList = ref([]);
-const equipmentTypeOptions = ref([]);
-const equipmentStatusList = ref([]);
-const loading = ref(false);
-const open = ref(false);
-const statusOpen = ref(false);
-const testOpen = ref(false);
-const showSearch = ref(true);
-const ids = ref([]);
-const multiple = ref(true);
-const total = ref(0);
-const title = ref("");
-const currentRow = ref({});
-const selectedDetail = ref({});
-
-const equipmentStatusOptions = [
-  { label: "在用", value: 1 },
-  { label: "闲置", value: 2 },
-  { label: "维修", value: 3 },
-  { label: "报废", value: 4 },
-  { label: "待入库", value: 5 }
-];
-
-const summary = reactive({
-  total: 0,
-  active: 0,
-  warning: 0,
-  typeCount: 0
-});
-
-const typeNameMap = computed(() => {
-  return equipmentTypeOptions.value.reduce((acc, item) => {
-    acc[item.id] = item.typeName;
-    return acc;
-  }, {});
-});
-
-// parentTypeId 为 0(或空)表示父类
-function isTopLevelType(item) {
-  const parentTypeId = item?.parentTypeId;
-  return parentTypeId === 0 || parentTypeId === "0" || parentTypeId === null || parentTypeId === undefined || parentTypeId === "";
-}
-
-const equipmentTypeTree = computed(() => {
-  const list = equipmentTypeOptions.value || [];
-  const entries = [];
-  // 同时按 typeId / id 建索引,兼容 parentTypeId 指向不同主键字段的情况
-  const nodeIndex = {};
-
-  list.forEach((item) => {
-    const node = {
-      id: item.id,
-      typeId: item.typeId,
-      typeName: item.typeName,
-      disabled: isTopLevelType(item),
-      children: []
-    };
-    entries.push({ item, node });
-    [item.typeId, item.id].forEach((key) => {
-      if (key !== undefined && key !== null) {
-        nodeIndex[String(key)] = node;
-      }
-    });
-  });
-
-  const roots = [];
-  // 父节点不在返回列表中时兜底归类,避免子类型在下拉中丢失
-  const fallbackNode = { id: "__other__", typeId: "__other__", typeName: "未分类", disabled: true, children: [] };
-
-  entries.forEach(({ item, node }) => {
-    if (isTopLevelType(item)) {
-      roots.push(node);
-      return;
-    }
-    const parent = nodeIndex[String(item.parentTypeId)];
-    if (parent && parent !== node) {
-      parent.children.push(node);
-    } else {
-      fallbackNode.children.push(node);
-    }
-  });
-
-  // 带子级的节点(即父类)统一禁选,叶子节点可选;空 children 去掉渲染为叶子
-  const prune = (node) => {
-    if (node.children && node.children.length) {
-      node.disabled = true;
-      node.children.forEach(prune);
-    } else {
-      delete node.children;
-    }
-    return node;
-  };
-
-  const result = roots.map(prune);
-  if (fallbackNode.children.length) {
-    result.push(prune(fallbackNode));
-  }
-  return result;
-});
-
-const statusMap = computed(() => {
-  return equipmentStatusList.value.reduce((acc, item) => {
-    acc[item.equipmentId] = item;
-    return acc;
-  }, {});
-});
-
-const data = reactive({
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    equipmentCode: undefined,
-    equipmentName: undefined,
-    equipmentModel: undefined,
-    manufacturer: undefined
-  },
-  form: {},
-  statusForm: {
-    currentStatus: 1
-  },
-  testForm: {
-    testType: undefined,
-    testDetail: undefined,
-    testUser: undefined
-  },
-  rules: {
-    equipmentCode: [{ required: true, message: "设备编码不能为空", trigger: "blur" }],
-    equipmentName: [{ required: true, message: "设备名称不能为空", trigger: "blur" }],
-    equipmentTypeId: [{ required: true, message: "设备类型不能为空", trigger: "change" }]
-  }
-});
-
-const { queryParams, form, statusForm, testForm, rules } = toRefs(data);
-
-function createEmptyForm() {
-  return {
-    equipmentId: undefined,
-    equipmentCode: undefined,
-    equipmentName: undefined,
-    equipmentModel: undefined,
-    equipmentSpec: undefined,
-    manufacturer: undefined,
-    productionDate: undefined,
-    equipmentTypeId: undefined,
-    assetValue: undefined,
-    useLife: undefined,
-    longitude: undefined,
-    latitude: undefined,
-    equipmentLocation: undefined,
-    installTime: undefined,
-    maintainer: undefined,
-    maintainerPhone: undefined,
-    remark: undefined
-  };
-}
-
-function getRowStatus(equipmentId) {
-  return statusMap.value[equipmentId]?.currentStatus;
-}
-
-function getStatusText(status) {
-  const found = equipmentStatusOptions.find((item) => item.value === status);
-  return found?.label || "未维护";
-}
-
-function getStatusTagType(status) {
-  const map = {
-    1: "success",
-    2: "info",
-    3: "warning",
-    4: "danger",
-    5: ""
-  };
-  return map[status] || "";
-}
-
-function buildSummary() {
-  summary.total = equipmentStatusList.value.length;
-  summary.active = equipmentStatusList.value.filter((item) => item.currentStatus === 1).length;
-  summary.warning = equipmentStatusList.value.filter((item) => item.currentStatus !== 1).length;
-  summary.typeCount = new Set(equipmentTypeOptions.value.map((item) => item.typeName).filter(Boolean)).size;
-}
-
-function getList() {
-  loading.value = true;
-  return listEquipment(queryParams.value)
-    .then((response) => {
-      equipmentList.value = response.records || [];
-      total.value = response.total || 0;
-      if (!selectedDetail.value.equipmentInfo && equipmentList.value.length) {
-        return handleView(equipmentList.value[0]);
-      }
-      return undefined;
-    })
-    .finally(() => {
-      loading.value = false;
-    });
-}
-
-function loadAssistData() {
-  return Promise.all([listEquipmentTypeAll(), listEquipmentStatusAll()]).then(([typeRes, statusRes]) => {
-    equipmentTypeOptions.value = typeRes.data || [];
-    equipmentStatusList.value = statusRes.data || [];
-    buildSummary();
-  });
-}
-
-function reset() {
-  form.value = createEmptyForm();
-  proxy.resetForm("equipmentRef");
-}
-
-function handleQuery() {
-  queryParams.value.pageNum = 1;
-  getList();
-}
-
-function resetQuery() {
-  proxy.resetForm("queryRef");
-  handleQuery();
-}
-
-function handleSelectionChange(selection) {
-  ids.value = selection.map((item) => item.equipmentId);
-  multiple.value = !selection.length;
-}
-
-function handleCurrentChange(currentRow) {
-  if (currentRow?.equipmentId) {
-    handleView(currentRow);
-  }
-}
-
-function handleRowClick(row) {
-  handleView(row);
-}
-
-function handleAdd() {
-  reset();
-  open.value = true;
-  title.value = "新增物联感知设备";
-}
-
-function handleUpdate(row) {
-  reset();
-  const equipmentId = row?.equipmentId || ids.value[0];
-  if (!equipmentId) {
-    return;
-  }
-  getEquipment(equipmentId).then((response) => {
-    form.value = { ...createEmptyForm(), ...(response.data || {}) };
-    open.value = true;
-    title.value = "修改物联感知设备";
-  });
-}
-
-function handleView(row) {
-  return getEquipmentDetail(row.equipmentId).then((response) => {
-    selectedDetail.value = response.data || {};
-  });
-}
-
-function cancel() {
-  open.value = false;
-  reset();
-}
-
-function submitForm() {
-  proxy.$refs.equipmentRef.validate((valid) => {
-    if (!valid) {
-      return;
-    }
-    const request = form.value.equipmentId ? updateEquipment(form.value) : addEquipment(form.value);
-    request.then(() => {
-      proxy.$modal.msgSuccess(form.value.equipmentId ? "修改成功" : "新增成功");
-      open.value = false;
-      Promise.all([getList(), loadAssistData()]).then(() => {
-        if (form.value.equipmentId) {
-          handleView({ equipmentId: form.value.equipmentId });
-        }
-      });
-    });
-  });
-}
-
-function handleDelete() {
-  const deleteIds = ids.value;
-  if (!deleteIds.length) {
-    return;
-  }
-  proxy.$modal
-    .confirm(`是否确认删除选中的 ${deleteIds.length} 台设备?`)
-    .then(() => Promise.all(deleteIds.map((id) => delEquipment(id))))
-    .then(() => {
-      proxy.$modal.msgSuccess("删除成功");
-      if (deleteIds.includes(selectedDetail.value?.equipmentInfo?.equipmentId)) {
-        selectedDetail.value = {};
-      }
-      return Promise.all([getList(), loadAssistData()]);
-    });
-}
-
-function openStatusDialog(row) {
-  currentRow.value = row;
-  statusForm.value.currentStatus = getRowStatus(row.equipmentId) || 1;
-  statusOpen.value = true;
-}
-
-function submitStatus() {
-  updateEquipmentStatus(currentRow.value.equipmentId, statusForm.value.currentStatus).then(() => {
-    proxy.$modal.msgSuccess("状态维护成功");
-    statusOpen.value = false;
-    Promise.all([loadAssistData(), handleView(currentRow.value)]);
-  });
-}
-
-function openTestDialog(row) {
-  currentRow.value = row;
-  testForm.value = {
-    testType: "通信测试",
-    testDetail: undefined,
-    testUser: undefined
-  };
-  testOpen.value = true;
-}
-
-function submitTest() {
-  testEquipment(currentRow.value.equipmentId, testForm.value).then(() => {
-    proxy.$modal.msgSuccess("测试记录已提交");
-    testOpen.value = false;
-    handleView(currentRow.value);
-  });
-}
-
-onMounted(() => {
-  reset();
-  Promise.all([getList(), loadAssistData()]);
-});
-</script>
-
-<style scoped>
-.equipment-page {
-  background: #f5f8fc;
-}
-
-.summary-grid {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 16px;
-  margin-bottom: 16px;
-}
-
-.summary-card {
-  border: none;
-  color: #fff;
-}
-
-.summary-card :deep(.el-card__body) {
-  padding: 20px;
-}
-
-.summary-card.primary {
-  background: linear-gradient(135deg, #2563eb, #5b93ff);
-}
-
-.summary-card.success {
-  background: linear-gradient(135deg, #059669, #34d399);
-}
-
-.summary-card.warning {
-  background: linear-gradient(135deg, #d97706, #f7b955);
-}
-
-.summary-card.info {
-  background: linear-gradient(135deg, #7c3aed, #9d72ff);
-}
-
-.summary-label {
-  font-size: 14px;
-  opacity: 0.9;
-}
-
-.summary-value {
-  margin: 8px 0 10px;
-  font-size: 30px;
-  font-weight: 700;
-  line-height: 1;
-}
-
-.summary-desc {
-  font-size: 12px;
-  opacity: 0.9;
-}
-
-.query-card,
-.table-card,
-.detail-card {
-  border-radius: 16px;
-}
-
-.query-card {
-  margin-bottom: 16px;
-}
-
-.card-header-row {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  gap: 12px;
-}
-
-.card-title {
-  font-size: 16px;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-.card-subtitle {
-  margin-top: 4px;
-  font-size: 12px;
-  color: #6b7280;
-}
-
-.card-actions {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  flex-wrap: wrap;
-}
-
-.detail-hero {
-  margin-bottom: 16px;
-  padding: 16px;
-  border-radius: 14px;
-  background: linear-gradient(135deg, #eff6ff, #f8fbff);
-  border: 1px solid #dbeafe;
-}
-
-.hero-title {
-  font-size: 18px;
-  font-weight: 700;
-  color: #1d4ed8;
-}
-
-.hero-meta {
-  display: flex;
-  gap: 10px;
-  flex-wrap: wrap;
-  margin-top: 8px;
-  font-size: 12px;
-  color: #4b5563;
-}
-
-.section-block {
-  margin-top: 16px;
-}
-
-.section-title {
-  margin-bottom: 10px;
-  font-size: 15px;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-@media (max-width: 1200px) {
-  .summary-grid {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-}
-
-@media (max-width: 768px) {
-  .summary-grid {
-    grid-template-columns: 1fr;
-  }
-
-  .card-header-row {
-    flex-direction: column;
-    align-items: flex-start;
-  }
-}
-</style>

+ 1242 - 0
src/views/subSystem/pipeNetwork/pdeviceInform/index.vue

@@ -0,0 +1,1242 @@
+<!-- 物联感知设备管理 -->
+<template>
+  <div class="app-container equipment-page">
+    <!-- 页头 -->
+    <div class="page-head">
+      <div class="page-head-titles">
+        <div class="page-title">设备统计</div>
+      </div>
+      <el-button type="primary" icon="Plus" class="btn-primary" @click="handleAdd">新增设备</el-button>
+    </div>
+
+    <!-- KPI -->
+    <div class="kpi-row">
+      <el-card shadow="never" class="kpi">
+        <div class="kpi-icon total"><el-icon><monitor /></el-icon></div>
+        <div class="kpi-data">
+          <div class="kpi-label">设备总数(台)</div>
+          <div class="kpi-value">{{ statistics.total }}</div>
+        </div>
+      </el-card>
+      <el-card shadow="never" class="kpi">
+        <div class="kpi-icon online"><el-icon><circle-check /></el-icon></div>
+        <div class="kpi-data">
+          <div class="kpi-label">在线设备(台)</div>
+          <div class="kpi-value">{{ statistics.online }}</div>
+        </div>
+      </el-card>
+      <el-card shadow="never" class="kpi">
+        <div class="kpi-icon offline"><el-icon><circle-close /></el-icon></div>
+        <div class="kpi-data">
+          <div class="kpi-label">离线设备(台)</div>
+          <div class="kpi-value">{{ statistics.offline }}</div>
+        </div>
+      </el-card>
+      <el-card shadow="never" class="kpi">
+        <div class="kpi-icon alarm"><el-icon><warning /></el-icon></div>
+        <div class="kpi-data">
+          <div class="kpi-label">报警设备(台)</div>
+          <div class="kpi-value">{{ statistics.alarm }}</div>
+        </div>
+      </el-card>
+    </div>
+
+    <!-- 设备台账 -->
+    <div class="page-title section-block-title">设备台账</div>
+
+    <!-- 查询条件 -->
+    <el-card shadow="never" class="filter-card">
+      <el-form ref="queryRef" :model="queryParams" :inline="true" class="filters">
+        <el-form-item label="设备编码" prop="equipmentCode">
+          <el-input
+            v-model="queryParams.equipmentCode"
+            placeholder="请输入"
+            clearable
+            class="input--code"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="设备名称" prop="equipmentName">
+          <el-input
+            v-model="queryParams.equipmentName"
+            placeholder="请输入"
+            clearable
+            class="input--name"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="设备类型" prop="equipmentTypeId">
+          <el-tree-select
+            v-model="queryParams.equipmentTypeId"
+            :data="equipmentTypeTree"
+            node-key="id"
+            :props="{ label: 'typeName', children: 'children', disabled: 'disabled' }"
+            check-strictly
+            :render-after-expand="false"
+            clearable
+            placeholder="请选择"
+            class="select--type"
+          />
+        </el-form-item>
+        <el-form-item label="在线状态" prop="onlineStatus">
+          <el-select v-model="queryParams.onlineStatus" clearable placeholder="请选择" class="select--online">
+            <el-option label="在线" :value="1" />
+            <el-option label="离线" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="报警" prop="alarmOnly">
+          <el-checkbox v-model="alarmOnly" @change="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" class="btn-query" @click="handleQuery">查询</el-button>
+          <el-button class="btn-reset" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card shadow="never" class="ledger">
+      <div v-loading="loading" class="grid">
+        <el-empty v-if="!equipmentList.length" description="暂无设备数据" />
+        <div
+          v-for="row in equipmentList"
+          :key="row.equipmentId"
+          class="card"
+          :class="{ 'is-selected': selectedId === row.equipmentId, 'is-alarm': getRowAlarm(row.equipmentId) === 1 }"
+          @click="handleView(row)"
+        >
+          <div class="card-top">
+            <div class="card-name">{{ row.equipmentName || "未命名设备" }}</div>
+            <div class="status-group">
+              <span class="badge" :class="getRowOnline(row.equipmentId) === 1 ? 'online' : 'offline'">
+                <span class="badge-dot"></span>
+                <span class="badge-text">{{ getRowOnline(row.equipmentId) === 1 ? "在线" : "离线" }}</span>
+              </span>
+              <span v-if="getRowAlarm(row.equipmentId) === 1" class="badge alarm">
+                <el-icon class="badge-icon"><warning-filled /></el-icon>
+                <span class="badge-text">报警</span>
+              </span>
+            </div>
+          </div>
+
+          <div class="card-code">{{ row.equipmentCode }}</div>
+          <div class="card-district">片区:{{ districtLabel(row.district) }}</div>
+
+          <div class="card-bottom">
+            <span class="type-tag">
+              {{ (row.equipmentTypeId && typeNameMap[row.equipmentTypeId]) || "未分类" }}
+            </span>
+            <div class="card-actions">
+              <el-button link class="link-detail" @click.stop="handleView(row)">详情</el-button>
+              <el-button link class="link-delete" @click.stop="handleDelete(row)">删除</el-button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 设备详情弹窗 -->
+    <el-dialog title="设备详情" v-model="detailOpen" width="800px" append-to-body class="detail-dialog">
+      <template v-if="selectedDetail.equipmentInfo">
+        <div class="dlg-summary">
+          <div class="summary-name">{{ selectedDetail.equipmentInfo.equipmentName || "未命名设备" }}</div>
+          <div class="summary-code">{{ selectedDetail.equipmentInfo.equipmentCode }}</div>
+          <span class="summary-tag">
+            {{ selectedDetail.typeInfo?.typeName || (selectedDetail.equipmentInfo.equipmentTypeId && typeNameMap[selectedDetail.equipmentInfo.equipmentTypeId]) || "未分类" }}
+          </span>
+          <div class="status-group">
+            <span class="badge" :class="selectedDetail.statusInfo?.onlineStatus === 1 ? 'online' : 'offline'">
+              <span class="badge-dot"></span>
+              <span class="badge-text">{{ selectedDetail.statusInfo?.onlineStatus === 1 ? "在线" : "离线" }}</span>
+            </span>
+            <span v-if="selectedDetail.statusInfo?.alarmStatus === 1" class="badge alarm">
+              <el-icon class="badge-icon"><warning-filled /></el-icon>
+              <span class="badge-text">报警</span>
+            </span>
+          </div>
+        </div>
+
+        <div class="form-section-title detail-section-title">基础信息</div>
+        <el-descriptions :column="2" border size="small" class="dlg-grid">
+          <el-descriptions-item label="设备编码">{{ selectedDetail.equipmentInfo.equipmentCode || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="设备名称">{{ selectedDetail.equipmentInfo.equipmentName || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="设备类型">{{ selectedDetail.typeInfo?.typeName || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="所属片区">{{ districtLabel(selectedDetail.equipmentInfo.district) }}</el-descriptions-item>
+          <el-descriptions-item label="设备型号">{{ selectedDetail.equipmentInfo.equipmentModel || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="制造商">{{ selectedDetail.equipmentInfo.manufacturer || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="生产日期">{{ selectedDetail.equipmentInfo.productionDate || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="规格参数">{{ selectedDetail.equipmentInfo.equipmentSpec || "-" }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="form-section-title detail-section-title">资产信息</div>
+        <el-descriptions :column="2" border size="small" class="dlg-grid">
+          <el-descriptions-item label="资产状态">{{ currentStatusMeta(selectedDetail.statusInfo?.currentStatus).text }}</el-descriptions-item>
+          <el-descriptions-item label="资产价值(元)">{{ selectedDetail.equipmentInfo.assetValue ?? "-" }}</el-descriptions-item>
+          <el-descriptions-item label="使用年限(年)">{{ selectedDetail.equipmentInfo.useLife ?? "-" }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="form-section-title detail-section-title">部署运维</div>
+        <el-descriptions :column="2" border size="small" class="dlg-grid">
+          <el-descriptions-item label="安装位置" :span="2">{{ selectedDetail.equipmentInfo.equipmentLocation || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="经度">{{ selectedDetail.equipmentInfo.longitude ?? "-" }}</el-descriptions-item>
+          <el-descriptions-item label="纬度">{{ selectedDetail.equipmentInfo.latitude ?? "-" }}</el-descriptions-item>
+          <el-descriptions-item label="安装时间">{{ selectedDetail.equipmentInfo.installTime || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="维护人员">{{ selectedDetail.equipmentInfo.maintainer || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="联系电话" :span="2">{{ selectedDetail.equipmentInfo.maintainerPhone || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="备注" :span="2">{{ selectedDetail.equipmentInfo.remark || "-" }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div v-if="selectedDetail.equipmentInfo.equipmentImage" class="detail-image">
+          <div class="field-label">设备图片</div>
+          <el-image
+            :src="resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)"
+            :preview-src-list="[resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)]"
+            preview-teleported
+            fit="cover"
+            class="field-media"
+          />
+        </div>
+      </template>
+      <el-empty v-else description="未选中设备" />
+      <template #footer>
+        <div class="dlg-footer">
+          <el-button class="btn-ghost" @click="detailOpen = false">关 闭</el-button>
+          <el-button type="primary" icon="Edit" @click="handleUpdate(selectedDetail.equipmentInfo)">编辑设备</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog :title="title" v-model="open" width="860px" append-to-body>
+      <el-form ref="equipmentRef" :model="form" :rules="rules" label-width="110px">
+        <div class="form-section-title">基础信息</div>
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="设备编码" prop="equipmentCode">
+              <el-input v-model="form.equipmentCode" placeholder="请输入设备编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="设备名称" prop="equipmentName">
+              <el-input v-model="form.equipmentName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="设备类型" prop="equipmentTypeId">
+              <el-tree-select
+                v-model="form.equipmentTypeId"
+                :data="equipmentTypeTree"
+                node-key="id"
+                :props="{ label: 'typeName', children: 'children', disabled: 'disabled' }"
+                default-expand-all
+                :render-after-expand="false"
+                placeholder="请选择设备类型"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属片区" prop="district">
+              <el-select v-model="form.district" placeholder="请选择所属片区" style="width: 100%">
+                <el-option v-for="item in districtOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="设备型号" prop="equipmentModel">
+              <el-input v-model="form.equipmentModel" placeholder="请输入设备型号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="制造商" prop="manufacturer">
+              <el-input v-model="form.manufacturer" placeholder="请输入制造商" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="生产日期" prop="productionDate">
+              <el-date-picker v-model="form.productionDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择生产日期" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="规格参数" prop="equipmentSpec">
+              <el-input v-model="form.equipmentSpec" placeholder="请输入设备规格参数" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <div class="form-section-title">资产信息</div>
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="资产状态" prop="currentStatus">
+              <el-select v-model="form.currentStatus" placeholder="请选择资产状态" style="width: 100%">
+                <el-option v-for="item in equipmentStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="资产价值(元)" prop="assetValue">
+              <el-input-number v-model="form.assetValue" :precision="2" :step="100" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="使用年限(年)" prop="useLife">
+              <el-input-number v-model="form.useLife" :precision="0" :step="1" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <div class="form-section-title">部署运维</div>
+        <el-row :gutter="16">
+          <el-col :span="24">
+            <el-form-item label="安装位置" prop="equipmentLocation">
+              <el-input v-model="form.equipmentLocation" placeholder="请输入或通过地图选择安装位置" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="地图坐标">
+              <div class="coordinate-row">
+                <el-input-number v-model="form.longitude" :precision="6" :step="0.000001" controls-position="right" placeholder="经度" />
+                <el-input-number v-model="form.latitude" :precision="6" :step="0.000001" controls-position="right" placeholder="纬度" />
+                <el-button type="primary" plain @click="openMapPicker">地图选点</el-button>
+              </div>
+              <div class="coordinate-tip">{{ locationTip || "点击地图获取经纬度,地址将自动回填到安装位置,也可以直接手工修改。" }}</div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="安装时间" prop="installTime">
+              <el-date-picker
+                v-model="form.installTime"
+                type="datetime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                placeholder="请选择安装时间"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="维护人员" prop="maintainer">
+              <el-input v-model="form.maintainer" placeholder="请输入维护人员" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="联系电话" prop="maintainerPhone">
+              <el-input v-model="form.maintainerPhone" placeholder="请输入联系电话" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="设备图片" prop="equipmentImage">
+              <image-upload v-model="form.equipmentImage" :limit="1" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 地图选点弹窗 -->
+    <el-dialog v-model="mapOpen" title="地图选点" width="760px" append-to-body>
+      <div class="map-picker">
+        <GisMapCanvas
+          :center="mapCenter"
+          :points="mapPoints"
+          :enable-clustering="false"
+          :show-marker-name="false"
+          @map-click="handleMapClick"
+        />
+      </div>
+      <div class="map-picker-coordinates">
+        当前坐标:经度 {{ form.longitude ?? "-" }},纬度 {{ form.latitude ?? "-" }}
+      </div>
+      <div class="map-picker-coordinates">
+        解析地址:{{ form.equipmentLocation || "点击地图后自动解析" }}
+      </div>
+      <template #footer>
+        <el-button @click="mapOpen = false">关 闭</el-button>
+        <el-button type="primary" @click="mapOpen = false">使用当前坐标</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
+import GisMapCanvas from "@/components/GisMapCanvas.vue";
+import { bd09ToWgs84 } from "@/utils/coordTransform";
+import { useRoute } from "vue-router";
+import {
+  addEquipment,
+  delEquipment,
+  getEquipment,
+  getEquipmentDetail,
+  getEquipmentStatusStatistics,
+  listEquipment,
+  listEquipmentTypeAll,
+  updateEquipment,
+  updateEquipmentStatus
+} from "@/api/pipeNetwork/pdeviceInform";
+
+const { proxy } = getCurrentInstance();
+
+// 数据范围:由系统菜单 query 配置 {"rootTypeId":1} 注入,对应 EquipmentTypeEnum 的 ROOT_ 大类
+const route = useRoute();
+const rootTypeId = ref(route.query.rootTypeId ? Number(route.query.rootTypeId) : undefined);
+
+const equipmentList = ref([]);
+const allTypeList = ref([]);
+const loading = ref(false);
+const open = ref(false);
+const detailOpen = ref(false);
+const mapOpen = ref(false);
+const alarmOnly = ref(false);
+const mapCenter = ref({ lng: 110.3965, lat: 28.4638 });
+const mapPoints = ref([]);
+const total = ref(0);
+const title = ref("");
+const selectedDetail = ref({});
+const selectedId = ref("");
+const locationTip = ref("");
+
+const statistics = reactive({
+  total: 0,
+  online: 0,
+  offline: 0,
+  alarm: 0
+});
+
+// 资产状态:0-在用,1-维修,2-报废
+const equipmentStatusOptions = [
+  { label: "在用", value: 0 },
+  { label: "维修", value: 1 },
+  { label: "报废", value: 2 }
+];
+
+// 所属片区:与后端 DistrictEnum 对齐(0-城南 1-城北 2-太常)
+const districtOptions = [
+  { label: "城南", value: 0 },
+  { label: "城北", value: 1 },
+  { label: "太常", value: 2 }
+];
+
+function districtLabel(code) {
+  const hit = districtOptions.find((item) => item.value === code);
+  return hit ? hit.label : "-";
+}
+
+const typeNameMap = computed(() => {
+  return allTypeList.value.reduce((acc, item) => {
+    acc[item.id] = item.typeName;
+    return acc;
+  }, {});
+});
+
+// 设备状态索引:equipmentId -> EquipmentStatus
+function getRowStatus(equipmentId) {
+  return equipmentList.value.find(item => item.equipmentId === equipmentId)?.currentStatus;
+}
+
+function getRowOnline(equipmentId) {
+  return equipmentList.value.find(item => item.equipmentId === equipmentId)?.onlineStatus;
+}
+
+function getRowAlarm(equipmentId) {
+  return equipmentList.value.find(item => item.equipmentId === equipmentId)?.alarmStatus;
+}
+
+// parentId 为 0(或空)表示父类
+function isTopLevelType(item) {
+  const parentId = item?.parentId;
+  return parentId === 0 || parentId === "0" || parentId === null || parentId === undefined || parentId === "";
+}
+
+function buildTypeTree(list) {
+  const entries = [];
+  const nodeIndex = {};
+
+  list.forEach((item) => {
+    const node = {
+      id: item.id,
+      typeName: item.typeName,
+      disabled: isTopLevelType(item),
+      children: []
+    };
+    entries.push({ item, node });
+    if (item.id !== undefined && item.id !== null) {
+      nodeIndex[String(item.id)] = node;
+    }
+  });
+
+  const roots = [];
+  const fallbackNode = { id: "__other__", typeName: "未分类", disabled: true, children: [] };
+
+  entries.forEach(({ item, node }) => {
+    if (isTopLevelType(item)) {
+      roots.push(node);
+      return;
+    }
+    const parent = nodeIndex[String(item.parentId)];
+    if (parent && parent !== node) {
+      parent.children.push(node);
+    } else {
+      fallbackNode.children.push(node);
+    }
+  });
+
+  const prune = (node) => {
+    if (node.children && node.children.length) {
+      node.disabled = true;
+      node.children.forEach(prune);
+    } else {
+      delete node.children;
+    }
+    return node;
+  };
+
+  const result = roots.map(prune);
+  if (fallbackNode.children.length) {
+    result.push(prune(fallbackNode));
+  }
+  return result;
+}
+
+// 全量类型构建树后按 rootTypeId 取对应大类分支:
+// 父节点展示真实大类名称(如“供水设备”),而非笼统的“未分类”
+const equipmentTypeTree = computed(() => {
+  const tree = buildTypeTree(allTypeList.value || []);
+  if (!rootTypeId.value) {
+    return tree;
+  }
+  const branch = tree.find((node) => String(node.id) === String(rootTypeId.value));
+  return branch ? [branch] : tree;
+});
+
+function currentStatusMeta(value) {
+  const map = {
+    0: { text: "在用", type: "success" },
+    1: { text: "维修", type: "warning" },
+    2: { text: "报废", type: "danger" }
+  };
+  return map[value] || { text: "未知", type: "info" };
+}
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+
+function resolveImageUrl(url) {
+  if (!url) return "";
+  const value = String(url).trim();
+  if (/^(https?:)?\/\//i.test(value) || value.startsWith("data:")) return value;
+  return baseUrl + (value.startsWith("/") ? value : "/" + value);
+}
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    equipmentCode: undefined,
+    equipmentName: undefined,
+    equipmentTypeId: undefined,
+    onlineStatus: undefined
+  },
+  form: {},
+  rules: {
+    equipmentCode: [{ required: true, message: "设备编码不能为空", trigger: "blur" }],
+    equipmentName: [{ required: true, message: "设备名称不能为空", trigger: "blur" }],
+    equipmentTypeId: [{ required: true, message: "设备类型不能为空", trigger: "change" }],
+    district: [{ required: true, message: "所属片区不能为空", trigger: "change" }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+function createEmptyForm() {
+  return {
+    equipmentId: undefined,
+    equipmentCode: undefined,
+    equipmentName: undefined,
+    equipmentModel: undefined,
+    equipmentSpec: undefined,
+    manufacturer: undefined,
+    productionDate: undefined,
+    equipmentTypeId: undefined,
+    district: undefined,
+    currentStatus: 0,
+    assetValue: undefined,
+    useLife: undefined,
+    longitude: undefined,
+    latitude: undefined,
+    equipmentLocation: undefined,
+    installTime: undefined,
+    maintainer: undefined,
+    maintainerPhone: undefined,
+    equipmentImage: undefined,
+    remark: undefined
+  };
+}
+
+function getList() {
+  loading.value = true;
+  return listEquipment({
+    ...queryParams.value,
+    rootTypeId: rootTypeId.value,
+    alarmStatus: alarmOnly.value ? 1 : undefined
+  })
+    .then((response) => {
+      equipmentList.value = response.records || [];
+      total.value = response.total || 0;
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+}
+
+function loadAssistData() {
+  return listEquipmentTypeAll().then((typeRes) => {
+    allTypeList.value = typeRes.data || [];
+  });
+}
+
+function loadStatistics() {
+  return getEquipmentStatusStatistics(rootTypeId.value).then((res) => {
+    const data = res.data || {};
+    statistics.total = data.total || 0;
+    statistics.online = data.online || 0;
+    statistics.offline = data.offline || 0;
+    statistics.alarm = data.alarm || 0;
+  });
+}
+
+function reset() {
+  form.value = createEmptyForm();
+  locationTip.value = "";
+  proxy.resetForm("equipmentRef");
+}
+
+// 逆地理编码:传入百度 BD-09 坐标,返回结构化地址文本,失败时返回空串
+function reverseGeocode(lng, lat) {
+  return new Promise((resolve) => {
+    const BMapGL = window.BMapGL;
+    if (!BMapGL || typeof BMapGL.Geocoder !== "function") {
+      resolve("");
+      return;
+    }
+    try {
+      const geocoder = new BMapGL.Geocoder();
+      geocoder.getLocation(new BMapGL.Point(lng, lat), (result) => {
+        resolve(extractAddress(result));
+      });
+    } catch (error) {
+      resolve("");
+    }
+  });
+}
+
+function extractAddress(result) {
+  const content = result?.content || result || {};
+  const address = result?.address || content.address;
+  if (address) {
+    return String(address);
+  }
+  const components = result?.addressComponents || content.address_detail || {};
+  return [components.province, components.city, components.district, components.street, components.streetNumber]
+    .filter(Boolean)
+    .join("");
+}
+
+// 地图选点后按坐标回填安装位置,解析失败时保留原值
+async function fillLocationByPoint(lng, lat) {
+  locationTip.value = "正在解析地址...";
+  const address = await reverseGeocode(lng, lat);
+  if (address) {
+    form.value.equipmentLocation = address;
+    locationTip.value = "已按选点回填安装位置,可手工修改。";
+  } else {
+    locationTip.value = "地址解析失败,请手动填写安装位置。";
+  }
+}
+
+function openMapPicker() {
+  const longitude = Number(form.value.longitude);
+  const latitude = Number(form.value.latitude);
+  mapCenter.value = Number.isFinite(longitude) && Number.isFinite(latitude)
+    ? { lng: longitude, lat: latitude }
+    : { lng: 110.3965, lat: 28.4638 };
+  mapPoints.value = Number.isFinite(longitude) && Number.isFinite(latitude)
+    ? [{ lng: longitude, lat: latitude, name: form.value.equipmentName || "设备位置" }]
+    : [];
+  locationTip.value = "";
+  mapOpen.value = true;
+}
+
+function handleMapClick(point) {
+  if (!point) return;
+  const [longitude, latitude] = bd09ToWgs84(point.lng, point.lat);
+  form.value.longitude = Number(longitude.toFixed(6));
+  form.value.latitude = Number(latitude.toFixed(6));
+  mapPoints.value = [{ lng: form.value.longitude, lat: form.value.latitude, name: form.value.equipmentName || "设备位置" }];
+  fillLocationByPoint(point.lng, point.lat);
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  alarmOnly.value = false;
+  handleQuery();
+}
+
+function handleAdd() {
+  reset();
+  open.value = true;
+  title.value = "新增设备";
+}
+
+function handleUpdate(row) {
+  detailOpen.value = false;
+  reset();
+  const equipmentId = row?.equipmentId;
+  if (!equipmentId) {
+    return;
+  }
+  getEquipment(equipmentId).then((response) => {
+    form.value = { ...createEmptyForm(), ...(response.data || {}) };
+    form.value.equipmentTypeId =
+      form.value.equipmentTypeId !== undefined &&
+      form.value.equipmentTypeId !== null &&
+      form.value.equipmentTypeId !== ""
+        ? Number(form.value.equipmentTypeId)
+        : undefined;
+    form.value.currentStatus = getRowStatus(equipmentId) ?? 0;
+    open.value = true;
+    title.value = "修改设备";
+  });
+}
+
+function handleView(row) {
+  if (!row?.equipmentId) {
+    return;
+  }
+  selectedId.value = row.equipmentId;
+  return getEquipmentDetail(row.equipmentId).then((response) => {
+    selectedDetail.value = response.data || {};
+    detailOpen.value = true;
+  });
+}
+
+function handleDelete(row) {
+  if (!row?.equipmentId) {
+    return;
+  }
+  proxy.$modal
+    .confirm(`即将删除 ${row.equipmentName || "该设备"}(${row.equipmentCode}),确认继续?`)
+    .then(() => delEquipment(row.equipmentId))
+    .then(() => {
+      proxy.$modal.msgSuccess("删除成功");
+      if (selectedDetail.value?.equipmentInfo?.equipmentId === row.equipmentId) {
+        selectedDetail.value = {};
+        selectedId.value = "";
+        detailOpen.value = false;
+      }
+      return Promise.all([getList(), loadAssistData(), loadStatistics()]);
+    });
+}
+
+function cancel() {
+  open.value = false;
+  reset();
+}
+
+function submitForm() {
+  proxy.$refs.equipmentRef.validate((valid) => {
+    if (!valid) {
+      return;
+    }
+    const isEdit = !!form.value.equipmentId;
+    const equipmentId = form.value.equipmentId;
+    const currentStatus = form.value.currentStatus;
+    const request = isEdit ? updateEquipment(form.value) : addEquipment(form.value);
+    request.then(() => {
+      proxy.$modal.msgSuccess(isEdit ? "修改成功" : "新增成功");
+      open.value = false;
+      const tasks = [getList(), loadAssistData(), loadStatistics()];
+      if (isEdit && currentStatus !== undefined && currentStatus !== null) {
+        tasks.push(updateEquipmentStatus(equipmentId, currentStatus));
+      }
+      Promise.all(tasks).then(() => {
+        if (isEdit) {
+          handleView({ equipmentId });
+        }
+      });
+    });
+  });
+}
+
+onMounted(() => {
+  reset();
+  Promise.all([getList(), loadAssistData(), loadStatistics()]);
+});
+</script>
+
+<style scoped>
+.equipment-page {
+  background: #f9f9f9;
+}
+
+/* 页头 */
+.page-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+}
+
+.page-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: #0a1628;
+  line-height: 1.4;
+}
+
+.btn-primary :deep(.el-icon) {
+  font-size: 14px;
+}
+
+/* KPI */
+.kpi-row {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+.kpi {
+  flex: 1 1 0;
+  min-width: 0;
+  border-radius: 12px;
+}
+
+.kpi :deep(.el-card__body) {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 20px;
+}
+
+.kpi-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 44px;
+  height: 44px;
+  flex: 0 0 44px;
+  border-radius: 10px;
+  font-size: 22px;
+}
+
+.kpi-icon.total {
+  background: #edf3fc;
+  color: #014db2;
+}
+
+.kpi-icon.online {
+  background: #e7f8f1;
+  color: #10b981;
+}
+
+.kpi-icon.offline {
+  background: #fef4e2;
+  color: #f59e0b;
+}
+
+.kpi-icon.alarm {
+  background: #fdecec;
+  color: #ef4444;
+}
+
+.kpi-label {
+  font-size: 13px;
+  font-weight: 400;
+  color: #6b7280;
+}
+
+.kpi-value {
+  margin-top: 6px;
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1;
+  color: #0a1628;
+}
+
+/* 台账卡片 */
+.filter-card {
+  margin-bottom: 20px;
+  border-radius: 12px;
+}
+
+.section-block-title {
+  margin: 20px 0 12px;
+}
+
+.ledger {
+  border-radius: 12px;
+}
+
+.ledger :deep(.el-card__body) {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  padding: 20px;
+}
+
+/* 筛选栏 */
+.filters {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 4px 16px;
+}
+
+.filters :deep(.el-form-item) {
+  margin-right: 0;
+  margin-bottom: 0;
+}
+
+.filters :deep(.el-form-item__label) {
+  font-size: 12px;
+  font-weight: 400;
+  color: #6b7280;
+  padding-right: 8px;
+}
+
+.input--code,
+.input--name {
+  width: 150px;
+}
+
+.select--type {
+  width: 150px;
+}
+
+.select--online {
+  width: 120px;
+}
+
+.btn-query {
+  height: 34px;
+  padding: 0 16px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 500;
+}
+
+.btn-reset {
+  height: 34px;
+  padding: 0 16px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 400;
+}
+
+/* 设备卡片 */
+.grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  align-items: start;
+  gap: 10px;
+  min-height: 160px;
+}
+
+.card {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  align-self: start;
+  min-height: 0;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background: #fff;
+  border: 1px solid #ebebed;
+  cursor: pointer;
+  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
+}
+
+.card:hover {
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
+}
+
+.card.is-selected {
+  background: #f5f9ff;
+  border: 1.5px solid #014db2;
+}
+
+.card.is-alarm {
+  border: 1.5px solid #fca5a5;
+}
+
+.card-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.card-name {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 14px;
+  font-weight: 500;
+  color: #0a1628;
+}
+
+.status-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-shrink: 0;
+}
+
+.badge {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.badge-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 3px;
+  flex-shrink: 0;
+}
+
+.badge-text {
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.badge.online .badge-dot {
+  background: #10b981;
+}
+
+.badge.online .badge-text {
+  color: #10b981;
+}
+
+.badge.offline .badge-dot {
+  background: #f59e0b;
+}
+
+.badge.offline .badge-text {
+  color: #f59e0b;
+}
+
+.badge.alarm .badge-text {
+  color: #ef4444;
+}
+
+.badge-icon {
+  font-size: 13px;
+  color: #ef4444;
+}
+
+.card-code {
+  font-size: 12px;
+  font-weight: 400;
+  color: #9ca3af;
+}
+
+.card-district {
+  overflow: hidden;
+  color: #6b7280;
+  font-size: 12px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-bottom {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.type-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 3px 8px;
+  border-radius: 4px;
+  background: #edf3fc;
+  font-size: 11px;
+  font-weight: 500;
+  color: #014db2;
+}
+
+.card-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.card-actions :deep(.el-button) {
+  height: auto;
+  padding: 0;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.link-detail {
+  color: #014db2;
+}
+
+.link-delete {
+  color: #ef4444;
+}
+
+/* 详情弹窗 */
+.dlg-summary {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+  padding: 14px;
+  border-radius: 8px;
+  background: #f5f9ff;
+}
+
+.summary-name {
+  font-size: 15px;
+  font-weight: 500;
+  color: #014db2;
+}
+
+.summary-code {
+  font-size: 12px;
+  font-weight: 400;
+  color: #6b7280;
+}
+
+.summary-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 3px 8px;
+  border-radius: 4px;
+  background: #fff;
+  font-size: 11px;
+  font-weight: 500;
+  color: #014db2;
+}
+
+.dlg-grid {
+  margin-top: 16px;
+}
+
+.detail-image {
+  margin-top: 16px;
+}
+
+.field-label {
+  margin-bottom: 6px;
+  font-size: 12px;
+  font-weight: 400;
+  color: #6b7280;
+}
+
+.field-media {
+  width: 320px;
+  max-width: 100%;
+  height: 180px;
+  border-radius: 6px;
+  border: 1px solid #e6e6eb;
+}
+
+.form-section-title {
+  margin: 8px 0 12px;
+  padding-left: 10px;
+  border-left: 3px solid #014db2;
+  color: #1f2937;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.detail-section-title {
+  margin-top: 18px;
+}
+
+.coordinate-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+}
+
+.coordinate-row :deep(.el-input-number) {
+  flex: 1;
+}
+
+.coordinate-tip {
+  margin-top: 6px;
+  color: #9ca3af;
+  font-size: 12px;
+}
+
+.map-picker {
+  height: 420px;
+  overflow: hidden;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+}
+
+.map-picker-coordinates {
+  margin-top: 10px;
+  color: #6b7280;
+  font-size: 13px;
+}
+
+.dlg-footer,
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+@media (max-width: 1200px) {
+  .kpi-row {
+    flex-wrap: wrap;
+  }
+
+  .kpi {
+    flex: 1 1 45%;
+  }
+}
+
+@media (max-width: 768px) {
+  .kpi {
+    flex: 1 1 100%;
+  }
+
+  .page-head {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+  }
+}
+</style>

+ 20 - 153
src/views/subSystem/waterSupply/components/WaterAlarmPage.vue

@@ -21,17 +21,7 @@
             </div>
         </div>
         <!-- 审核工作台:按工单状态统计处置进度,分组口径与统一工单页一致 -->
-        <div v-if="mode === 'audit'" class="metric-grid">
-            <div v-for="card in auditOrderStatCards" :key="card.label" class="metric">
-                <div class="metric-icon"><el-icon><Tickets/></el-icon></div>
-                <div class="metric-body">
-                    <span>{{ card.label }}</span>
-                    <div class="metric-value">
-                        <strong :class="card.tone">{{ card.value }}</strong><small>单</small>
-                    </div>
-                </div>
-            </div>
-        </div>
+
         <div v-if="mode === 'manage'" class="chart-grid">
             <el-card shadow="never" class="water-panel chart-card trend-card">
                 <div class="card-title">
@@ -101,7 +91,7 @@
                 <div class="card-title">
                     <span class="card-heading">
                         <el-icon><Tickets /></el-icon>
-                        <span>{{ mode === 'audit' ? '待审核报警清单' : '报警流水' }}</span>
+                        <span>{{ '报警流水' }}</span>
                         <span v-if="mode === 'manage'" class="muted">逐条报警记录 · 仅供水设备 · 展示状态由后端推导</span>
                     </span>
                     <el-button v-if="mode === 'manage'" type="success" plain :icon="Download"
@@ -175,7 +165,7 @@
                 </el-table-column>
                 <el-table-column label="操作" width="220" fixed="right">
                     <template #default="{row}">
-                        <el-button link type="primary" @click="openDetail(row)">{{ mode === 'audit' ? '审核' : '详情' }}</el-button>
+                        <el-button link type="primary" @click="openDetail(row)">{{ '详情' }}</el-button>
                     </template>
                 </el-table-column>
             </el-table>
@@ -353,48 +343,15 @@
         </el-drawer>
 
         <el-drawer v-if="mode !== 'map'" v-model="detailVisible" class="detail-drawer alarm-detail-drawer"
-                   :title="mode === 'audit' ? '报警审核' : '报警详情'" direction="rtl"
-                   :size="mode === 'audit' ? '720px' : '420px'" append-to-body>
+                   title="报警详情" direction="rtl"
+                   size="420px" append-to-body>
             <el-descriptions :column="2" border class="detail-descriptions">
                 <el-descriptions-item v-for="field in detailFields" :key="field.label" :label="field.label">
                     {{ field.value }}
                 </el-descriptions-item>
             </el-descriptions>
-            <div v-if="mode === 'audit'" class="audit-form">
-                <el-form :model="auditForm" label-width="90px">
-                    <el-form-item label="审核结论">
-                        <el-radio-group v-model="auditForm.decision">
-                            <el-radio value="transfer">转工单</el-radio>
-                            <el-radio value="release">误报解除</el-radio>
-                        </el-radio-group>
-                    </el-form-item>
-                    <el-form-item label="审核意见">
-                        <el-input v-model="auditForm.auditOpinion" type="textarea" :rows="3" maxlength="500"
-                                  :required="auditForm.decision === 'release'"
-                                  placeholder="误报解除时必须说明理由" show-word-limit/>
-                    </el-form-item>
-                    <el-form-item label="附件地址">
-                        <el-input v-model="auditForm.attachUrl" placeholder="可填写多个地址,以逗号分隔"/>
-                    </el-form-item>
-                </el-form>
-            </div>
-            <div v-if="mode === 'audit'" class="flow-section">
-                <div class="section-label">审核记录</div>
-                <el-timeline>
-                    <el-timeline-item v-for="item in audits" :key="item.auditId || item.id"
-                                      :timestamp="item.auditTime || item.createTime"><b>{{ item.auditUserName ||
-                        item.auditUser || '系统用户' }}</b><span> {{ auditResultText(item.auditResult) }}:{{ item.auditOpinion || '-' }}</span>
-                    </el-timeline-item>
-                    <el-timeline-item v-if="!audits.length" timestamp="暂无记录">尚未提交审核</el-timeline-item>
-                </el-timeline>
-                <div class="section-label">全流程跟踪</div>
-                <el-timeline>
-                    <el-timeline-item v-for="item in flow" :key="item.id || item.nodeId || item.time"
-                                      :timestamp="item.time || item.createTime">{{ item.name || item.nodeName ||
-                        item.description || '-' }}
-                    </el-timeline-item>
-                </el-timeline>
-            </div>
+
+
             <template #footer>
                 <div class="drawer-footer">
                     <div v-if="mode === 'manage'" class="footer-jumps">
@@ -406,7 +363,6 @@
                     </div>
                     <div class="footer-actions">
                         <el-button @click="detailVisible=false">关闭</el-button>
-                        <el-button v-if="mode === 'audit'" type="primary" @click="submitAudit">提交审核</el-button>
                     </div>
                 </div>
             </template>
@@ -425,17 +381,14 @@
     import {saveAs} from 'file-saver'
     import * as echarts from 'echarts'
     import {
-        auditAlarm,
         exportAlarmLedger,
         getAlarmDetail,
-        getAlarmFlow,
         getAlarmLedgerDashboard,
-        getAuditOrderStats,
         getDeviceGisAllPoints,
-        listAlarmAudits,
         listAlarmLedger,
         listAlarmMapPoints,
     } from '@/api/pipeNetwork/waterSupply'
+    import { getAlarmReviewOptions } from '@/api/alarmReview'
     import GisMapCanvas from '@/components/GisMapCanvas.vue'
     import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
     import {
@@ -479,29 +432,6 @@
         query.value.startTime = start || undefined;
         query.value.endTime = end || undefined;
     }
-    // 从报警管理详情跳转进入审核页时携带的报警ID:列表加载后自动打开对应报警
-    const pendingAlarmId = ref('');
-
-    /** 读取跳转 query,预填筛选条件;仅审核模式消费 */
-    function applyIncomingQuery() {
-        const incoming = route.query || {}
-        if (props.mode !== 'audit') return
-        if (incoming.deviceCode) query.value.keyword = String(incoming.deviceCode)
-        if (incoming.alarmId) pendingAlarmId.value = String(incoming.alarmId)
-    }
-
-    /** 列表就绪后打开跳转指定的报警;找不到(可能已被处理出队)则清空待办标记 */
-    function focusIncomingAlarm() {
-        if (props.mode !== 'audit' || !pendingAlarmId.value) return
-        const wanted = pendingAlarmId.value
-        pendingAlarmId.value = ''
-        const row = (page.value.records || []).find(item => String(item.id ?? item.alarmId) === wanted)
-        if (row) {
-            openDetail(row)
-        } else {
-            ElMessage.warning('该报警不在当前待审核清单中,已按设备筛选展示')
-        }
-    }
     const page = ref({records: [], total: 0});
     const dashboard = ref({totalCount: 0, pendingAuditCount: 0, transferredCount: 0, releasedCount: 0, alarmingDeviceCount: 0});
     const trendDays = 7;
@@ -523,19 +453,6 @@
     const detailVisible = ref(false);
     const detail = ref({});
     const selectedPoint = computed(() => detail.value || {});
-    const audits = ref([]);
-    const flow = ref([]);
-    // 审核只有两个动作;auditAction / auditResult 由结论映射,前端不再暴露四种组合
-    const AUDIT_DISPLAY_PENDING = 1
-    const auditForm = ref({decision: 'transfer', auditOpinion: '', attachUrl: ''})
-    const auditOrderStats = ref({total: 0, pendingCount: 0, processingCount: 0, verifyingCount: 0, finishedCount: 0})
-    const auditOrderStatCards = computed(() => [
-        {label: '全部工单', value: auditOrderStats.value.total ?? 0, tone: ''},
-        {label: '待处理', value: auditOrderStats.value.pendingCount ?? 0, tone: 'danger'},
-        {label: '处理中', value: auditOrderStats.value.processingCount ?? 0, tone: 'warning'},
-        {label: '待验收', value: auditOrderStats.value.verifyingCount ?? 0, tone: 'primary'},
-        {label: '已完成', value: auditOrderStats.value.finishedCount ?? 0, tone: 'success'}
-    ])
     const pageIcon = computed(() => props.mode === 'map' ? MapLocation : Bell)
     const levels = [{label: '一级', value: 1}, {label: '二级', value: 2}, {label: '三级', value: 3}, {
         label: '四级',
@@ -544,7 +461,8 @@
     const displayStatuses = [
         {label: '待审核', value: 1},
         {label: '已转工单', value: 2},
-        {label: '已解除', value: 3}
+        {label: '已解除', value: 3},
+        {label: '误报警', value: 4}
     ]
     const filteredAlarmPoints = computed(() => filterAlarmGisPoints(
         mapPoints.value,
@@ -651,8 +569,7 @@
         {text: '近 7 天', value: () => [dayOffset(-6), dayOffset(0, true)]},
         {text: '近 30 天', value: () => [dayOffset(-29), dayOffset(0, true)]}
     ];
-    const auditResultText = value => Number(value) === 2 ? '误报/已解除' : '属实/已通过'
-    const displayStatusTextOf = value => ({1: '待审核', 2: '已转工单', 3: '已解除'}[Number(value)] || '未知');
+    const displayStatusTextOf = value => ({1: '待审核', 2: '已转工单', 3: '已解除', 4: '误报警'}[Number(value)] || '未知');
     const displayStatusType = value => ({1: 'warning', 2: 'primary', 3: 'success'}[Number(value)] || 'info');
     // 顶部态势卡片:数字来自后端聚合接口,不在前端二次计算
     const metricCards = computed(() => [
@@ -675,15 +592,6 @@
                 ])
                 dashboard.value = unwrapResponse(summary) || {}
                 page.value = normalizePage(ledger)
-            } else if (props.mode === 'audit') {
-                // 审核队列 = 后端推导展示状态为「待审核」的报警流水;
-                // 审核完成后该行展示状态变为「已转工单/已解除」并离开队列,去向由计数条可见
-                const [orders, ledger] = await Promise.all([
-                    getAuditOrderStats(),
-                    listAlarmLedger({...query.value, displayStatus: AUDIT_DISPLAY_PENDING})
-                ])
-                auditOrderStats.value = unwrapResponse(orders) || {}
-                page.value = normalizePage(ledger)
             } else {
                 await loadAlarmMapData()
             }
@@ -699,7 +607,6 @@
         } finally {
             loading.value = false
             if (props.mode === 'manage') await renderCharts()
-            focusIncomingAlarm()
         }
     }
 
@@ -900,16 +807,22 @@
     }
 
     /** 可传入某条报警,跳转后审核页据此预填筛选并打开对应报警 */
-    function goAudit(target) {
-        const path = resolveRoutePath('/monitorAlarm/audit')
+    async function goAudit(target) {
+        const path = resolveRoutePath('/monitorAlarm/alarmAudit')
         if (!path) {
             ElMessage.warning('未找到报警审核入口')
             return
         }
         const alarm = target && target.id ? target : {}
+        let config
+        try { config = await getAlarmReviewOptions() }
+        catch { ElMessage.error('无法进入报警审核,请检查菜单权限或稍后重试'); return }
+        const root = config.data.rootTypes.find(item => item.key === 'ROOT_WATER_SUPPLY_DEVICE')
+        if (!root) return ElMessage.error('未找到供水设备大类配置')
         router.push({
             path,
             query: {
+                rootTypeId: root.value,
                 alarmId: alarm.id || alarm.alarmId || undefined,
                 deviceCode: alarm.deviceCode || undefined
             }
@@ -940,15 +853,9 @@
         const id = row.id || row.alarmId
         if (!id) return
         try {
-            const [detailResponse, auditResponse, flowResponse] = await Promise.all([
-                getAlarmDetail(id),
-                props.mode === 'audit' ? listAlarmAudits(id) : Promise.resolve(null),
-                props.mode === 'audit' ? getAlarmFlow(id) : Promise.resolve(null)
-            ])
+            const detailResponse = await getAlarmDetail(id)
             // 台账详情不返回设备在线/运行状态,合并保留点位侧已补全的状态,避免抽屉回退成单一状态
             detail.value = {...row, ...(unwrapResponse(detailResponse) || {})}
-            audits.value = unwrapResponse(auditResponse) || []
-            flow.value = unwrapResponse(flowResponse) || []
         } catch (error) {
             detail.value = row
         }
@@ -966,31 +873,7 @@
         openDetail(point);
     }
 
-    async function submitAudit() {
-        const {decision, auditOpinion, attachUrl} = auditForm.value
-        if (decision === 'release' && !String(auditOpinion || '').trim()) {
-            return ElMessage.warning('误报解除必须填写审核意见')
-        }
-        try {
-            await auditAlarm({
-                alarmId: detail.value.id || detail.value.alarmId,
-                // 1-转工单(属实) 2-误报解除,与后端两个动作一一对应
-                auditAction: decision === 'release' ? 2 : 1,
-                auditResult: decision === 'release' ? 2 : 1,
-                auditOpinion,
-                attachUrl
-            })
-            ElMessage.success(decision === 'release' ? '已按误报解除,未生成工单' : '已转工单,等待在统一工单页派单')
-            detailVisible.value = false
-            loadData()
-        } catch (error) {
-            ElMessage.error(getRequestErrorMessage(error, '审核提交失败,请检查权限或服务状态'))
-        }
-    }
-
-
     onMounted(() => {
-        applyIncomingQuery()
         loadData()
     })
     onBeforeUnmount(() => {
@@ -1449,22 +1332,6 @@
         color: #67c23a
     }
 
-    .audit-form, .flow-section {
-        border-top: 1px solid #ebeef5;
-        margin-top: 18px;
-        padding-top: 15px
-    }
-
-    .section-label {
-        font-weight: 600;
-        color: #303133;
-        margin: 8px 0 12px
-    }
-
-    .flow-section :deep(.el-timeline) {
-        padding-left: 5px
-    }
-
     @media (max-width: 1000px) {
         .chart-grid {
             grid-template-columns:1fr

+ 0 - 1404
src/views/subSystem/waterSupply/components/WaterDevicePage.vue

@@ -1,1404 +0,0 @@
-<template>
-    <div class="water-page device-page">
-        <div class="page-heading">
-            <div class="page-title">
-                <span class="page-title-icon"><el-icon><component :is="pageIcon"/></el-icon></span>
-                <div><h2>{{ title }}</h2><span class="muted">{{ subtitle }}</span></div>
-            </div>
-            <el-button class="heading-refresh" :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
-        </div>
-
-        <div v-if="view === 'abnormal'" class="metric-grid">
-            <div v-for="item in abnormalStats" :key="item.label" class="metric">
-                <div class="metric-icon">
-                    <el-icon>
-                        <component :is="item.icon"/>
-                    </el-icon>
-                </div>
-                <div class="metric-body">
-                    <span>{{ item.label }}</span>
-                    <div class="metric-value"><strong>{{ item.value }}</strong><small>{{ item.unit }}</small></div>
-                </div>
-            </div>
-        </div>
-
-        <div v-if="view === 'ledger'" class="metric-grid">
-            <div v-for="item in ledgerStatsCards" :key="item.label" class="metric">
-                <div class="metric-icon">
-                    <el-icon>
-                        <component :is="item.icon"/>
-                    </el-icon>
-                </div>
-                <div class="metric-body">
-                    <span>{{ item.label }}</span>
-                    <div class="metric-value"><strong>{{ item.value }}</strong><small>{{ item.unit }}</small></div>
-                </div>
-            </div>
-        </div>
-
-        <el-card shadow="never" class="filter-card">
-            <el-form :inline="true" :model="query">
-                <el-form-item v-if="view !== 'gis'" label="设备名称">
-                    <el-input v-model="query.equipmentName" clearable/>
-                </el-form-item>
-                <el-form-item v-if="view !== 'gis'" label="设备编码">
-                    <el-input v-model="query.equipmentCode" clearable/>
-                </el-form-item>
-                <el-form-item v-if="view === 'ledger'" label="报警状态">
-                    <el-select v-model="query.alarmStatus" clearable class="filter-select" style="width:180px">
-                        <el-option label="正常" :value="0"/>
-                        <el-option label="报警" :value="1"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item v-if="view === 'ledger' || view === 'dispatch' || view === 'abnormal'" label="报警等级">
-                    <el-select v-model="query.alertLevel" clearable class="filter-select" style="width:180px">
-                        <el-option v-for="item in levels" :key="item.value" v-bind="item"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item v-if="view === 'dispatch' || view === 'abnormal'" label="异常类型">
-                    <el-select v-model="query.abnormalType" clearable class="filter-select" style="width:160px">
-                        <el-option label="报警" value="alarm"/>
-                        <el-option label="维修" value="repair"/>
-                        <el-option label="离线" value="offline"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item v-if="view === 'dispatch' || view === 'abnormal'" label="区域">
-                    <el-select v-model="query.region" clearable class="filter-select" style="width:140px">
-                        <el-option label="城南" value="城南"/>
-                        <el-option label="城北" value="城北"/>
-                        <el-option label="太常" value="太常"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item>
-                    <el-button type="primary" @click="loadData">查询</el-button>
-                    <el-button @click="reset">重置</el-button>
-                </el-form-item>
-            </el-form>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel">
-            <div class="card-title"><span><el-icon><Monitor/></el-icon> {{ title }}清单</span>
-                <div class="panel-actions">
-                    <el-button v-if="view === 'ledger'" type="primary" plain size="small" :icon="Plus"
-                               @click="openCreate">新增
-                    </el-button>
-                    <el-button v-if="view === 'ledger'" type="danger" plain size="small" :icon="Delete"
-                               :disabled="!selectedDevices.length" @click="removeSelectedDevices">删除
-                    </el-button>
-                    <el-button v-if="view === 'ledger'" type="info" plain size="small" :icon="Upload"
-                               @click="openImport">导入
-                    </el-button>
-                    <el-button v-if="view !== 'gis'" type="success" plain size="small" :icon="Download"
-                               @click="exportData">导出
-                    </el-button>
-                </div>
-            </div>
-            <el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" class="load-error">
-                <el-button size="small" type="danger" plain @click="loadData">重试</el-button>
-            </el-alert>
-            <el-empty v-if="view === 'gis' && !loading && !loadError && !page.records.length"
-                      description="暂无设备 GIS 点位数据"/>
-            <el-table :data="page.records" border stripe v-loading="loading" row-key="equipmentId"
-                      @selection-change="handleDeviceSelectionChange">
-                <el-table-column v-if="view === 'ledger'" type="selection" width="48" align="center"/>
-                <el-table-column type="index" label="#" width="55"/>
-                <el-table-column prop="equipmentCode" label="设备编码" min-width="130"/>
-                <el-table-column prop="equipmentName" label="设备名称" min-width="150"/>
-                <el-table-column prop="equipmentTypeName" label="设备类型" min-width="120"/>
-                <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="所属区域" width="110">
-                    <template #default="{ row }">{{ row.district || '-' }}</template>
-                </el-table-column>
-                <el-table-column prop="equipmentLocation" label="安装位置" min-width="180" show-overflow-tooltip/>
-                <el-table-column v-if="view === 'ledger' || view === 'abnormal'" label="设备图片" width="110" align="center">
-                    <template #default="{ row }">
-                        <el-image v-if="row.equipmentImageUrl" :src="row.equipmentImageUrl"
-                                  :preview-src-list="[row.equipmentImageUrl]" preview-teleported hide-on-click-modal
-                                  fit="cover" class="device-image"/>
-                        <span v-else class="image-placeholder">暂无图片</span></template>
-                </el-table-column>
-                <el-table-column v-if="view !== 'maintain'" label="设备状态" width="100">
-                    <template #default="{ row }">
-                        <el-tag :type="Number(row.currentStatus) === 3 ? 'danger' : 'success'">{{ row.currentStatusText
-                            }}
-                        </el-tag>
-                    </template>
-                </el-table-column>
-                <el-table-column v-if="view !== 'maintain'" label="在线状态" width="100">
-                    <template #default="{ row }">
-                        <el-tag :type="Number(row.onlineStatus) === 1 ? 'success' : 'info'">{{ row.onlineStatusText }}
-                        </el-tag>
-                    </template>
-                </el-table-column>
-                <el-table-column v-if="view !== 'maintain'" label="报警状态" width="100">
-                    <template #default="{ row }">
-                        <el-tag :type="Number(row.alarmStatus) === 1 ? 'danger' : 'success'">{{ row.alarmStatusText }}
-                        </el-tag>
-                    </template>
-                </el-table-column>
-                <el-table-column v-if="view !== 'maintain'" label="报警等级" width="110">
-                    <template #default="{ row }">
-                        <el-tag :type="alarmLevelType(row.alertLevel)">{{ row.alarmLevelText }}</el-tag>
-                    </template>
-                </el-table-column>
-                <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="异常原因" min-width="150"
-                                 show-overflow-tooltip>
-                    <template #default="{ row }">{{ row.abnormalReasonText || '-' }}</template>
-                </el-table-column>
-                <el-table-column label="关联工单" min-width="190" show-overflow-tooltip>
-                    <template #default="{ row }">{{ view === 'maintain' ? (row.orderNo || '-') : row.relatedOrderNo }}
-                    </template>
-                </el-table-column>
-                <el-table-column label="工单状态" min-width="110">
-                    <template #default="{ row }">
-                        <el-tag v-if="view !== 'maintain' && row.workOrderStatusText !== '暂无工单'"
-                                :type="workOrderStatusType(row.workOrderStatusCode)">{{ row.workOrderStatusText }}
-                        </el-tag>
-                        <span v-else>{{ view === 'maintain' ? (row.status || '暂无') : row.workOrderStatusText }}</span>
-                    </template>
-                </el-table-column>
-                <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="接单责任人" min-width="110">
-                    <template #default="{ row }">{{ view === 'maintain' ? (row.maintainPerson || '-') :
-                        (row.relatedWorkOrderPerson || '-') }}
-                    </template>
-                </el-table-column>
-                <el-table-column label="操作" width="210" fixed="right">
-                    <template #default="{ row }">
-                        <div class="facility-row-actions">
-                            <el-tooltip content="查看详情" placement="top">
-                                <el-button class="facility-row-action is-detail" aria-label="查看详情" size="small"
-                                           type="primary" plain circle :icon="View" @click="selectPoint(row)"/>
-                            </el-tooltip>
-                            <el-tooltip v-if="view === 'ledger'" content="修改信息" placement="top">
-                                <el-button class="facility-row-action is-edit" aria-label="修改信息" size="small"
-                                           type="warning" plain circle :icon="Edit" @click="openEdit(row)"/>
-                            </el-tooltip>
-                            <el-tooltip v-if="canDispatch(row)" content="维修派单" placement="top">
-                                <el-button class="facility-row-action is-dispatch" aria-label="维修派单" size="small"
-                                           type="warning" plain circle :icon="Position" @click="openOrder(row)"/>
-                            </el-tooltip>
-                            <el-tooltip v-if="view === 'ledger'" content="删除设备" placement="top">
-                                <el-button class="facility-row-action is-delete" aria-label="删除设备" size="small"
-                                           type="danger" plain circle :icon="Delete" @click="removeDevice(row)"/>
-                            </el-tooltip>
-                            <el-tooltip v-if="view === 'maintain'" content="删除记录" placement="top">
-                                <el-button class="facility-row-action is-delete" aria-label="删除记录" size="small"
-                                           type="danger" plain circle :icon="Delete" @click="removeRecord(row)"/>
-                            </el-tooltip>
-                        </div>
-                    </template>
-                </el-table-column>
-            </el-table>
-            <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total"
-                           layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" @change="loadData"/>
-        </el-card>
-
-        <el-drawer v-model="detailVisible" class="detail-drawer device-detail-drawer" title="设备详情" direction="rtl"
-                   size="680px" append-to-body>
-            <div class="detail-summary"><span class="detail-name">{{ detail.equipmentName || '-' }}</span>
-                <el-tag :type="Number(detail.currentStatus) === 3 ? 'danger' : 'success'">{{ detail.currentStatusText ||
-                    '-' }}
-                </el-tag>
-                <el-tag :type="Number(detail.alarmStatus) === 1 ? 'danger' : 'success'">{{ detail.alarmStatusText || '-'
-                    }}
-                </el-tag>
-                <el-tag v-if="detail.workOrderStatusText && detail.workOrderStatusText !== '暂无工单'"
-                        :type="workOrderStatusType(detail.workOrderStatusCode)">{{ detail.workOrderStatusText }}
-                </el-tag>
-            </div>
-            <div v-if="view === 'ledger' || view === 'abnormal'" class="detail-media">
-                <div class="detail-media-label">设备图片</div>
-                <el-image v-if="detail.equipmentImageUrl" :src="detail.equipmentImageUrl"
-                          :preview-src-list="[detail.equipmentImageUrl]" preview-teleported hide-on-click-modal
-                          fit="cover"
-                          class="detail-image"/>
-                <div v-else class="detail-image-placeholder">暂无图片</div>
-            </div>
-            <el-descriptions :column="2" border>
-                <el-descriptions-item v-for="item in detailFields" :key="item.label" :label="item.label">{{ item.value
-                    }}
-                </el-descriptions-item>
-            </el-descriptions>
-            <template #footer>
-                <el-button @click="detailVisible = false">关闭</el-button>
-            </template>
-        </el-drawer>
-        <el-drawer v-model="editVisible" class="detail-drawer device-edit-drawer"
-                   :title="editMode === 'create' ? '新增设备信息' : '修改设备信息'" direction="rtl" size="680px"
-                   append-to-body>
-            <el-form ref="editFormRef" :model="editForm" label-width="110px" :rules="editRules">
-                <el-row :gutter="16">
-                    <el-col :span="12">
-                        <el-form-item label="设备编码" prop="equipmentCode">
-                            <el-input v-model="editForm.equipmentCode" :disabled="editMode === 'edit'" maxlength="50"/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="设备类型" prop="equipmentTypeId">
-                            <el-select v-if="editMode === 'create'" v-model="editForm.equipmentTypeId" filterable
-                                       class="full-input">
-                                <el-option v-for="item in waterDeviceTypes" :key="item.id" :label="item.typeName"
-                                           :value="item.id"/>
-                            </el-select>
-                            <el-input v-else v-model="editForm.equipmentTypeName" disabled/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="设备名称" prop="equipmentName">
-                            <el-input v-model="editForm.equipmentName" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="设备型号">
-                            <el-input v-model="editForm.equipmentModel" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="所属片区">
-                            <el-select v-model="editForm.district" clearable class="full-input">
-                                <el-option label="城南" value="城南"/>
-                                <el-option label="城北" value="城北"/>
-                                <el-option label="太常" value="太常"/>
-                            </el-select>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="制造商">
-                            <el-input v-model="editForm.manufacturer" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="经度">
-                            <el-input-number v-model="editForm.longitude" :precision="8" :controls="false"
-                                             class="full-input"/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="纬度">
-                            <el-input-number v-model="editForm.latitude" :precision="8" :controls="false"
-                                             class="full-input"/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="24">
-                        <el-form-item label="安装位置">
-                            <el-input v-model="editForm.equipmentLocation" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="设备负责人">
-                            <el-input v-model="editForm.maintainer" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="12">
-                        <el-form-item label="联系方式">
-                            <el-input v-model="editForm.maintainerPhone" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="24">
-                        <el-form-item label="权属单位">
-                            <el-input v-model="editForm.ownershipUnit" clearable/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="24">
-                        <el-form-item label="设备图片">
-                            <ImageUpload v-model="editForm.equipmentImage" :limit="1" :file-size="5"/>
-                        </el-form-item>
-                    </el-col>
-                    <el-col :span="24">
-                        <el-form-item label="备注">
-                            <el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500"
-                                      show-word-limit/>
-                        </el-form-item>
-                    </el-col>
-                </el-row>
-            </el-form>
-            <template #footer>
-                <el-button @click="editVisible = false">取消</el-button>
-                <el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button>
-            </template>
-        </el-drawer>
-        <el-dialog v-model="dispatchVisible" class="dispatch-dialog" title="设备维修派单" width="860px">
-            <div class="dispatch-layout">
-                <div class="assignee-picker">
-                    <aside class="assignee-depts">
-                        <el-input v-model="deptFilterText" clearable placeholder="筛选部门" size="small"/>
-                        <el-tree ref="deptTreeRef" :data="deptTreeData" node-key="deptId" highlight-current
-                                 :props="{ label: 'deptName', children: 'children' }"
-                                 :filter-node-method="filterDeptNode" class="assignee-tree"
-                                 @node-click="handleDeptClick"/>
-                    </aside>
-                    <section class="assignee-users">
-                        <div class="assignee-search">
-                            <el-input v-model="assigneeKeyword" clearable placeholder="搜索用户名、昵称或手机号"
-                                      @keyup.enter="searchAssigneeUsers"/>
-                            <el-button type="primary" :loading="userLoading" @click="searchAssigneeUsers">查询
-                            </el-button>
-                        </div>
-                        <el-alert v-if="userLoadError" :title="userLoadError" type="error" show-icon :closable="false">
-                            <el-button size="small" type="danger" plain @click="retryAssigneeLoading">重试</el-button>
-                        </el-alert>
-                        <el-table :data="users" border row-key="userId" highlight-current-row
-                                  :current-row-key="assigneeDraft?.userId" @row-click="chooseAssignee">
-                            <el-table-column label="选择" width="58" align="center">
-                                <template #default="{ row }">
-                                    <el-radio :model-value="assigneeDraft?.userId" :label="row.userId"
-                                              @click.stop="chooseAssignee(row)">&nbsp;
-                                    </el-radio>
-                                </template>
-                            </el-table-column>
-                            <el-table-column prop="nickName" label="昵称" min-width="100"/>
-                            <el-table-column prop="userName" label="用户名" min-width="110"/>
-                            <el-table-column prop="deptName" label="部门" min-width="120" show-overflow-tooltip/>
-                            <el-table-column prop="phone" label="手机号" min-width="130"/>
-                            <el-table-column prop="statusText" label="账号状态" width="90"/>
-                        </el-table>
-                        <el-empty v-if="!userLoading && !userLoadError && !users.length"
-                                  description="暂无符合条件的责任人" :image-size="80"/>
-                        <el-pagination v-model:current-page="assigneePageNum" v-model:page-size="assigneePageSize"
-                                       :total="assigneeTotal" layout="total, sizes, prev, pager, next"
-                                       :page-sizes="[10, 20, 50]" @current-change="loadAssigneeUsers"
-                                       @size-change="handleAssigneeSizeChange"/>
-                        <div class="assignee-picked">已选责任人:<strong v-if="assigneeDraft">{{ assigneeDraft.nickName
-                            || assigneeDraft.userName }}</strong><span v-else>未选择</span>
-                            <el-button size="small" type="primary" plain :disabled="!assigneeDraft"
-                                       @click="confirmAssignee">确认选择
-                            </el-button>
-                        </div>
-                    </section>
-                </div>
-                <el-form class="dispatch-form" :model="dispatchForm" label-width="100px">
-                    <el-form-item label="设备"><span>{{ dispatchDevice.equipmentName }}({{ dispatchDevice.equipmentCode }})</span>
-                    </el-form-item>
-                    <el-form-item label="工单类型"><span>1-故障维修</span></el-form-item>
-                    <el-form-item label="优先级" required>
-                        <el-select v-model="dispatchForm.orderLevel" placeholder="请选择优先级">
-                            <el-option label="紧急" :value="0"/>
-                            <el-option label="一般" :value="1"/>
-                            <el-option label="低" :value="2"/>
-                        </el-select>
-                    </el-form-item>
-                    <el-form-item label="问题描述" required>
-                        <el-input v-model="dispatchForm.description" type="textarea" :rows="5" maxlength="500"
-                                  show-word-limit placeholder="请输入故障或异常描述"/>
-                    </el-form-item>
-                    <el-form-item label="接单责任人" required>
-                        <span v-if="selectedAssignee" class="assignee-summary">{{ selectedAssignee.nickName || selectedAssignee.userName }}({{ selectedAssignee.deptName || '-' }})</span>
-                        <span v-else class="assignee-placeholder">请在左侧选择责任人</span>
-                    </el-form-item>
-                </el-form>
-            </div>
-            <template #footer>
-                <el-button @click="dispatchVisible = false">取消</el-button>
-                <el-button type="primary" :loading="dispatching" @click="submitDispatch">确认派单</el-button>
-            </template>
-        </el-dialog>
-
-        <el-dialog v-model="importVisible" title="供水设备批量导入" width="520px" append-to-body @closed="resetImport">
-            <el-upload ref="importUploadRef" drag :limit="1" accept=".xlsx,.xls" :auto-upload="false"
-                       :file-list="importFileList" :on-change="handleImportChange" :on-remove="handleImportRemove"
-                       :disabled="importUploading">
-                <el-icon class="el-icon--upload">
-                    <Upload/>
-                </el-icon>
-                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
-                <template #tip>
-                    <div class="el-upload__tip import-tip">仅支持 xls、xlsx;设备类型必须属于供水类型树。</div>
-                </template>
-            </el-upload>
-            <el-checkbox v-model="importUpdateSupport" class="import-update">更新已存在的设备编码</el-checkbox>
-            <template #footer>
-                <el-button type="primary" :loading="importUploading" @click="submitImport">开始导入</el-button>
-                <el-button @click="importVisible = false">取消</el-button>
-            </template>
-        </el-dialog>
-    </div>
-</template>
-
-<script setup>
-    import {computed, onMounted, ref, watch} from 'vue'
-    import {ElMessage, ElMessageBox} from 'element-plus'
-    import {
-        Monitor,
-        Refresh,
-        Warning,
-        Bell,
-        Tools,
-        DataLine,
-        Upload,
-        Download,
-        Search,
-        Plus,
-        Delete,
-        View,
-        Edit,
-        Position
-    } from '@element-plus/icons-vue'
-    import {listUser} from '@/api/system/user'
-    import {listDept} from '@/api/system/dept'
-    import {handleTree} from '@/utils/ruoyi'
-    import {
-        createAbnormalOrder,
-        createWaterDevice,
-        deleteMaintainRecord,
-        deleteWaterDevices,
-        exportAbnormalDevices,
-        exportDeviceLedger,
-        getAbnormalDeviceStats,
-        getDeviceLedgerStatistics,
-        getWaterDeviceById,
-        importDeviceLedger,
-        listAbnormalDevices,
-        listDeviceGisPoints,
-        listDeviceLedger,
-        listMaintainRecords,
-        updateWaterDevice
-    } from '@/api/pipeNetwork/waterSupply'
-    import {listEquipmentTypeAll} from '@/api/pipeNetwork/pdeviceInform'
-    import {normalizePage, readMonitoringExportError} from '@/utils/waterSupplyModel'
-    import {
-        buildAssigneeQuery,
-        buildMaintainOrderPayload,
-        canDispatchDevice,
-        normalizeAssigneeUser,
-        normalizeDeviceGisPoints,
-        normalizeDeviceRow,
-        normalizeDispatchOrderResponse,
-        preserveDevicePageOnError,
-        selectSingleAssignee
-    } from '@/utils/waterSupplyUiModel'
-    import {saveAs} from 'file-saver'
-    import {orderStatusType as workOrderStatusType} from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
-
-    const props = defineProps({
-        title: {type: String, required: true},
-        subtitle: {type: String, default: '设备信息、运行状态与维护过程管理'},
-        view: {type: String, default: 'ledger'}
-    })
-    const {title, subtitle, view} = props
-    const query = ref({
-        pageNum: 1,
-        pageSize: 10,
-        equipmentName: '',
-        equipmentCode: '',
-        alarmStatus: undefined,
-        alertLevel: undefined,
-        abnormalType: undefined,
-        region: undefined
-    })
-    const importVisible = ref(false);
-    const importUploading = ref(false);
-    const importUpdateSupport = ref(false);
-    const importFileList = ref([]);
-    const importUploadRef = ref(null);
-    const editVisible = ref(false);
-    const editSaving = ref(false);
-    const editFormRef = ref(null);
-    const editForm = ref({})
-    const editMode = ref('edit');
-    const selectedDevices = ref([]);
-    const waterDeviceTypes = ref([])
-    const editRules = {
-        equipmentCode: [{required: true, message: '请输入设备编码', trigger: 'blur'}],
-        equipmentTypeId: [{required: true, message: '请选择设备类型', trigger: 'change'}],
-        equipmentName: [{required: true, message: '请输入设备名称', trigger: 'blur'}]
-    }
-    const page = ref({records: [], total: 0});
-    const ledgerStats = ref({total: 0, online: 0, offline: 0, onlineRate: 0});
-    const abnormalStatsData = ref({total: 0, alarm: 0, repair: 0, offline: 0});
-    const loading = ref(false);
-    const loadError = ref('');
-    const detailVisible = ref(false);
-    const detail = ref({});
-    const dispatchVisible = ref(false);
-    const dispatching = ref(false);
-    const dispatchDevice = ref({});
-    const dispatchForm = ref({description: '', receiveUserId: undefined});
-    const users = ref([]);
-    const userLoading = ref(false);
-    const userLoadError = ref('');
-    const deptLoadError = ref('');
-    const selectedAssignee = ref(null);
-    const assigneeDraft = ref(null);
-    const deptTreeRef = ref(null);
-    const deptTreeData = ref([]);
-    const deptFilterText = ref('');
-    const assigneeKeyword = ref('');
-    const assigneeDeptId = ref(undefined);
-    const assigneePageNum = ref(1);
-    const assigneePageSize = ref(10);
-    const assigneeTotal = ref(0)
-    const levels = [{label: '一级报警', value: 1}, {label: '二级报警', value: 2}, {
-        label: '三级报警',
-        value: 3
-    }, {label: '四级报警', value: 4}];
-    const pageIcon = Monitor;
-    const alarmLevelType = level => ({1: 'danger', 2: 'warning', 3: 'primary', 4: 'success'}[Number(level)] || 'info')
-
-    function canDispatch(row) {
-        if (view !== 'dispatch') return false
-        return canDispatchDevice(row)
-    }
-
-
-    const abnormalStats = computed(() => {
-        return [
-            {label: '异常设备', value: abnormalStatsData.value.total, unit: '台', icon: Warning},
-            {label: '报警中', value: abnormalStatsData.value.alarm, unit: '台', icon: Bell},
-            {label: '维修中', value: abnormalStatsData.value.repair, unit: '台', icon: Tools},
-            {label: '离线设备', value: abnormalStatsData.value.offline, unit: '台', icon: DataLine}
-        ]
-    })
-    const ledgerStatsCards = computed(() => [
-        {label: '设备数量', value: ledgerStats.value.total, unit: '台', icon: Monitor},
-        {label: '在线设备', value: ledgerStats.value.online, unit: '台', icon: DataLine},
-        {label: '离线设备', value: ledgerStats.value.offline, unit: '台', icon: Warning},
-        {label: '在线率', value: ledgerStats.value.onlineRate, unit: '%', icon: Tools}
-    ])
-
-    const abnormalViews = ['abnormal', 'dispatch']
-    const detailFields = computed(() => [
-        ['设备编码', detail.value.equipmentCode],
-        ['设备类型', detail.value.equipmentTypeName || detail.value.typeName],
-        ...(abnormalViews.includes(view) ? [['所属区域', detail.value.district]] : []),
-        ['安装位置', detail.value.equipmentLocation],
-        ['设备状态', detail.value.currentStatusText],
-        ['在线状态', detail.value.onlineStatusText],
-        ['报警状态', detail.value.alarmStatusText],
-        ['报警等级', detail.value.alarmLevelText],
-        ['异常原因', detail.value.abnormalReasonText],
-        ['关联工单', detail.value.relatedOrderNo || detail.value.orderNo],
-        ['工单状态', detail.value.workOrderStatusText || detail.value.status],
-        ['接单责任人', detail.value.relatedWorkOrderPerson || detail.value.maintainPerson],
-        ['状态更新时间', detail.value.statusUpdateTime]
-    ].map(([label, value]) => ({
-        label,
-        value: value ?? '-'
-    })))
-
-    async function loadData() {
-        loading.value = true;
-        loadError.value = '';
-        try {
-            if (view === 'gis') {
-                const response = await listDeviceGisPoints({...query.value});
-                const records = normalizeDeviceGisPoints(response).map(normalizeDeviceRow);
-                page.value = {records, total: records.length, current: 1, size: records.length || 10};
-                return
-            }
-            let response;
-            if (view === 'maintain') response = await listMaintainRecords({...query.value}); else if (view === 'dispatch' || view === 'abnormal') response = await listAbnormalDevices({...query.value, ...(view === 'dispatch' ? {dispatchableOnly: true} : {})}); else response = await listDeviceLedger({...query.value});
-            const normalized = normalizePage(response);
-            page.value = {...normalized, records: normalized.records.map(normalizeDeviceRow)}
-            if (view === 'ledger') await loadLedgerStats()
-            if (view === 'abnormal') await loadAbnormalStats()
-        } catch (error) {
-            const retained = preserveDevicePageOnError(page.value);
-            page.value = retained.page;
-            loadError.value = retained.error;
-            console.warn(`[${title}] 数据加载失败`, error)
-        } finally {
-            loading.value = false
-        }
-    }
-
-    async function loadLedgerStats() {
-        try {
-            const response = await getDeviceLedgerStatistics()
-            const stats = response?.data ?? response ?? {}
-            ledgerStats.value = {
-                total: Number(stats.total ?? 0) || 0,
-                online: Number(stats.online ?? 0) || 0,
-                offline: Number(stats.offline ?? 0) || 0,
-                onlineRate: Number(stats.onlineRate ?? 0) || 0
-            }
-        } catch (error) {
-            console.warn('设备台账统计加载失败', error)
-        }
-    }
-
-    async function loadAbnormalStats() {
-        try {
-            const response = await getAbnormalDeviceStats()
-            const stats = response?.data ?? response ?? {}
-            abnormalStatsData.value = {
-                total: Number(stats.total ?? 0) || 0,
-                alarm: Number(stats.alarm ?? 0) || 0,
-                repair: Number(stats.repair ?? 0) || 0,
-                offline: Number(stats.offline ?? 0) || 0
-            }
-        } catch (error) {
-            console.warn('[设备异常管理] 统计加载失败', error)
-        }
-    }
-
-    function reset() {
-        query.value = {
-            pageNum: 1,
-            pageSize: 10,
-            equipmentName: '',
-            equipmentCode: '',
-            alarmStatus: undefined,
-            alertLevel: undefined,
-            abnormalType: undefined,
-            region: undefined
-        };
-        loadData()
-    }
-
-    function selectPoint(row) {
-        detail.value = normalizeDeviceRow(row);
-        detailVisible.value = true
-    }
-
-    watch(deptFilterText, value => deptTreeRef.value?.filter(value))
-
-    function filterDeptNode(value, data) {
-        return !value || String(data.deptName || '').includes(value)
-    }
-
-    async function loadDeptTree(force = false) {
-        if (!force && deptTreeData.value.length) return true;
-        deptLoadError.value = '';
-        try {
-            const response = await listDept();
-            deptTreeData.value = handleTree(response.data || [], 'deptId');
-            return true
-        } catch (error) {
-            deptLoadError.value = '部门列表加载失败,请点击重试';
-            userLoadError.value = deptLoadError.value;
-            return false
-        }
-    }
-
-    async function loadAssigneeUsers() {
-        userLoading.value = true;
-        userLoadError.value = '';
-        try {
-            const response = await listUser(buildAssigneeQuery({
-                deptId: assigneeDeptId.value,
-                keyword: assigneeKeyword.value,
-                pageNum: assigneePageNum.value,
-                pageSize: assigneePageSize.value
-            }));
-            const rows = response.rows || response.data?.rows || [];
-            users.value = rows.map(normalizeAssigneeUser).filter(user => user.eligible);
-            assigneeTotal.value = Number(response.total ?? response.data?.total ?? users.value.length) || 0
-        } catch (error) {
-            userLoadError.value = '责任人列表加载失败,请点击重试'
-        } finally {
-            userLoading.value = false
-        }
-    }
-
-    function searchAssigneeUsers() {
-        assigneePageNum.value = 1;
-        loadAssigneeUsers()
-    }
-
-    function handleAssigneeSizeChange() {
-        assigneePageNum.value = 1;
-        loadAssigneeUsers()
-    }
-
-    async function retryAssigneeLoading() {
-        const deptLoaded = await loadDeptTree(true);
-        if (!deptLoaded) return;
-        await loadAssigneeUsers()
-    }
-
-    async function openOrder(row) {
-        dispatchDevice.value = row
-        dispatchForm.value = {
-            description: row.abnormalReason || row.abnormalReasonText || row.description || '',
-            orderLevel: 1,
-            receiveUserId: undefined
-        }
-        selectedAssignee.value = null
-        assigneeDraft.value = null
-        assigneeDeptId.value = undefined
-        assigneeKeyword.value = ''
-        assigneePageNum.value = 1
-        dispatchVisible.value = true
-        await retryAssigneeLoading()
-    }
-
-    function handleDeptClick(node) {
-        assigneeDeptId.value = node.deptId;
-        assigneePageNum.value = 1;
-        loadAssigneeUsers()
-    }
-
-    function chooseAssignee(row) {
-        assigneeDraft.value = selectSingleAssignee([row])
-    }
-
-    function confirmAssignee() {
-        selectedAssignee.value = assigneeDraft.value;
-        dispatchForm.value.receiveUserId = assigneeDraft.value?.userId
-    }
-
-    async function submitDispatch() {
-        if (!dispatchForm.value.description?.trim()) return ElMessage.warning('请填写问题描述')
-        if (!dispatchForm.value.receiveUserId) return ElMessage.warning('请选择接单责任人')
-        dispatching.value = true
-        try {
-            const response = await createAbnormalOrder(buildMaintainOrderPayload(dispatchDevice.value, dispatchForm.value))
-            const summary = normalizeDispatchOrderResponse(response)
-            if (!summary) throw new Error('派单失败:服务端未返回有效的待接单工单')
-            ElMessage.success(`已派单,等待接单(${summary.orderNo})`)
-            dispatchVisible.value = false
-            await loadData()
-        } catch (error) {
-            const responseData = error?.response?.data
-            const message = responseData?.msg || responseData?.message || error?.message || '派单失败,请检查权限或服务状态'
-            // A conflict means another order now owns the device; refresh the list so its order number is visible.
-            if (/已有|冲突|重复|处理中的工单|关联工单/.test(String(message))) {
-                await loadData()
-                ElMessage.warning(`该设备已有相关处置工单:${message}`)
-            } else {
-                ElMessage.error(message)
-            }
-        } finally {
-            dispatching.value = false
-        }
-    }
-
-    async function removeRecord(row) {
-        try {
-            await deleteMaintainRecord(row.maintainId || row.id);
-            ElMessage.success('记录已删除');
-            await loadData()
-        } catch (error) {
-            ElMessage.error('删除失败')
-        }
-    }
-
-    function emptyDeviceForm() {
-        return {
-            equipmentId: undefined, equipmentCode: '', equipmentTypeId: undefined, equipmentTypeName: '',
-            equipmentName: '', equipmentModel: '', district: '', manufacturer: '', longitude: undefined,
-            latitude: undefined, equipmentLocation: '', maintainer: '', maintainerPhone: '', ownershipUnit: '',
-            equipmentImage: '', remark: ''
-        }
-    }
-
-    function isWaterDeviceType(type, typesById) {
-        if (!type || (type.typeName === '供水' && type.parentTypeId === '0')) return false
-        let cursor = type
-        let guard = 0
-        while (cursor && guard++ <= typesById.size) {
-            if (cursor.typeName === '供水' && cursor.parentTypeId === '0') return true
-            cursor = typesById.get(cursor.parentTypeId)
-        }
-        return false
-    }
-
-    async function loadWaterTypes() {
-        if (waterDeviceTypes.value.length) return
-        try {
-            const response = await listEquipmentTypeAll()
-            const rows = response?.data || []
-            const typesById = new Map(rows.map(item => [item.id, item]))
-            waterDeviceTypes.value = rows.filter(item => isWaterDeviceType(item, typesById))
-                .map(({id, typeName}) => ({id, typeName}))
-        } catch (error) {
-            ElMessage.warning('供水设备类型加载失败,请重试')
-        }
-    }
-
-    async function openCreate() {
-        editMode.value = 'create'
-        editVisible.value = true
-        editSaving.value = false
-        editForm.value = emptyDeviceForm()
-        await loadWaterTypes()
-    }
-
-    function handleDeviceSelectionChange(rows) {
-        selectedDevices.value = rows
-    }
-
-    async function confirmDeleteDevices(ids) {
-        await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 台设备吗?删除后不可恢复。`, '删除设备', {type: 'warning'})
-        await deleteWaterDevices(ids)
-        ElMessage.success('删除成功')
-        selectedDevices.value = []
-        query.value.pageNum = 1
-        await loadData()
-    }
-
-    async function removeDevice(row) {
-        if (!row?.equipmentId) return
-        await confirmDeleteDevices([row.equipmentId])
-    }
-
-    async function removeSelectedDevices() {
-        const ids = selectedDevices.value.map(row => row.equipmentId).filter(Boolean)
-        if (!ids.length) return
-        await confirmDeleteDevices(ids)
-    }
-
-    async function openEdit(row) {
-        editMode.value = 'edit'
-        editVisible.value = true
-        editSaving.value = false
-        editForm.value = {...row}
-        try {
-            const response = await getWaterDeviceById(row.equipmentId)
-            const full = response?.data || response
-            if (full) editForm.value = {...editForm.value, ...full}
-        } catch (error) {
-            ElMessage.warning('设备详情加载失败,已使用列表数据填充')
-        }
-    }
-
-    async function submitEdit() {
-        try {
-            await editFormRef.value?.validate()
-        } catch {
-            return
-        }
-        editSaving.value = true
-        try {
-            const creating = editMode.value === 'create'
-            if (creating) await createWaterDevice(editForm.value)
-            else await updateWaterDevice(editForm.value)
-            ElMessage.success(creating ? '新增成功' : '修改成功')
-            editVisible.value = false
-            if (creating) query.value.pageNum = 1
-            await loadData()
-        } catch (error) {
-            ElMessage.error(error?.response?.data?.msg || error?.message || (editMode.value === 'create' ? '新增失败,请稍后重试' : '修改失败,请稍后重试'))
-        } finally {
-            editSaving.value = false
-        }
-    }
-
-    function openImport() {
-        importVisible.value = true
-    }
-
-    function handleImportChange(file, fileList) {
-        if (!/\.(xlsx|xls)$/iu.test(file?.name || '')) {
-            ElMessage.error('仅支持 xls、xlsx 文件')
-            importFileList.value = []
-            return false
-        }
-        importFileList.value = fileList.slice(-1)
-    }
-
-    function handleImportRemove(file, fileList) {
-        importFileList.value = fileList
-    }
-
-    function resetImport() {
-        importUploading.value = false
-        importUpdateSupport.value = false
-        importFileList.value = []
-        importUploadRef.value?.clearFiles()
-    }
-
-    async function submitImport() {
-        if (!importFileList.value.length) return ElMessage.warning('请选择 xls 或 xlsx 文件')
-        importUploading.value = true
-        try {
-            const response = await importDeviceLedger(importFileList.value[0].raw, importUpdateSupport.value)
-            if (response?.code && response.code !== 200) throw new Error(response.msg || response.message || '导入失败,请检查文件内容')
-            ElMessage.success(response?.msg || response?.data || '导入成功')
-            importVisible.value = false
-            await loadData()
-        } catch (error) {
-            ElMessage.error(error?.response?.data?.msg || error?.response?.data?.message || error?.message || '导入失败,请稍后重试')
-        } finally {
-            importUploading.value = false
-        }
-    }
-
-    async function exportData() {
-        try {
-            const response = await (view === 'dispatch' || view === 'abnormal' ? exportAbnormalDevices : exportDeviceLedger)({...query.value})
-            const errorMessage = await readMonitoringExportError(response)
-            if (errorMessage) throw new Error(errorMessage)
-            const blob = response instanceof Blob ? response : new Blob([response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})
-            saveAs(blob, `${title}_${new Date().toISOString().slice(0, 10)}.xlsx`)
-            ElMessage.success('导出成功')
-        } catch (error) {
-            const backendMessage = await readMonitoringExportError(error?.response?.data)
-            ElMessage.error(backendMessage || error?.message || '导出失败,请稍后重试')
-        }
-    }
-
-    onMounted(loadData)
-</script>
-
-<style scoped>
-    .device-page {
-        padding: 20px;
-        background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
-        min-height: calc(100vh - 84px)
-    }
-
-    .page-heading {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        gap: 16px;
-        margin-bottom: 16px
-    }
-
-    .page-title {
-        display: flex;
-        align-items: center;
-        gap: 12px;
-        min-width: 0
-    }
-
-    .page-title-icon {
-        display: grid;
-        place-items: center;
-        width: 42px;
-        height: 42px;
-        color: #fff;
-        background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
-        border-radius: 8px;
-        box-shadow: 0 8px 16px rgba(37, 99, 235, .2)
-    }
-
-    .page-title h2 {
-        margin: 0 0 4px;
-        font-size: 20px;
-        color: #1e293b
-    }
-
-    .muted {
-        font-size: 13px;
-        color: #64748b
-    }
-
-    .heading-refresh {
-        border-color: #bfdbfe;
-        color: #2563eb;
-        background: #fff
-    }
-
-    .filter-card, .water-panel {
-        border: 1px solid #e2e8f0;
-        border-radius: 8px;
-        box-shadow: 0 6px 14px rgba(15, 23, 42, .04)
-    }
-
-    .filter-card {
-        margin-bottom: 16px;
-        background: #fff
-    }
-
-    .filter-card :deep(.el-card__header) {
-        padding: 14px 18px;
-        border-bottom: 1px solid #e2e8f0;
-        background: #fbfdff
-    }
-
-    .filter-card :deep(.el-form-item) {
-        margin-top: 8px;
-        margin-right: 16px;
-        margin-bottom: 8px
-    }
-
-    .filter-card :deep(.el-form-item__label) {
-        color: #475569
-    }
-
-    .metric-grid {
-        display: grid;
-        grid-template-columns:repeat(4, minmax(0, 1fr));
-        gap: 14px;
-        margin-bottom: 16px
-    }
-
-    .metric {
-        display: flex;
-        align-items: center;
-        gap: 14px;
-        min-width: 0;
-        padding: 16px 18px;
-        background: #fff;
-        border: 1px solid #e2e8f0;
-        border-radius: 8px;
-        box-shadow: 0 6px 14px rgba(15, 23, 42, .04)
-    }
-
-    .metric-icon {
-        position: static;
-        flex: 0 0 42px;
-        display: grid;
-        place-items: center;
-        width: 42px;
-        height: 42px;
-        color: #2563eb;
-        background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
-        border-radius: 8px
-    }
-
-    .metric-icon .el-icon {
-        font-size: 22px
-    }
-
-    .metric-body {
-        min-width: 0;
-        text-align: left
-    }
-
-    .metric-body span {
-        display: block;
-        overflow: hidden;
-        margin-bottom: 3px;
-        color: #64748b;
-        font-size: 13px;
-        text-overflow: ellipsis;
-        white-space: nowrap
-    }
-
-    .metric-value {
-        display: flex;
-        align-items: baseline;
-        gap: 5px;
-        min-width: 0
-    }
-
-    .metric-value strong {
-        font-size: 24px;
-        line-height: 1.2;
-        color: #1e293b
-    }
-
-    .metric-value small {
-        color: #94a3b8
-    }
-
-    .water-panel {
-        overflow: hidden
-    }
-
-    .water-panel :deep(.el-card__header) {
-        padding: 14px 18px;
-        border-bottom: 1px solid #e2e8f0;
-        background: #fbfdff
-    }
-
-    .card-title {
-        display: flex;
-        align-items: center;
-        gap: 8px;
-        color: #1e293b;
-        font-weight: 600;
-        margin-bottom: 16px
-    }
-
-    .card-title .el-icon {
-        color: #2563eb;
-        font-size: 18px
-    }
-
-    .table-toolbar {
-        display: flex;
-        justify-content: space-between;
-        align-items: center
-    }
-
-    .panel-actions {
-        display: flex;
-        align-items: center;
-        gap: 8px
-    }
-
-    .load-error {
-        margin-bottom: 14px
-    }
-
-    .device-page :deep(.el-table) {
-        --el-table-border-color: #e2e8f0;
-        --el-table-header-bg-color: #f8fafc;
-        --el-table-header-text-color: #475569;
-        --el-table-row-hover-bg-color: #eff6ff
-    }
-
-    .device-page :deep(.el-table__row) {
-        cursor: pointer
-    }
-
-    .device-image {
-        width: 66px;
-        height: 44px;
-        border-radius: 6px;
-        overflow: hidden
-    }
-
-    .image-placeholder {
-        display: inline-grid;
-        place-items: center;
-        width: 66px;
-        height: 44px;
-        color: #94a3b8;
-        font-size: 12px;
-        background: #f1f5f9;
-        border-radius: 6px
-    }
-
-    .detail-summary {
-        display: flex;
-        align-items: center;
-        gap: 12px;
-        margin-bottom: 16px
-    }
-
-    .detail-name {
-        font-size: 18px;
-        font-weight: 700;
-        color: #17324d
-    }
-
-    .device-detail-drawer :deep(.el-drawer__body), .device-edit-drawer :deep(.el-drawer__body) {
-        overflow: auto;
-        padding: 20px 24px
-    }
-
-    .device-detail-drawer :deep(.el-descriptions__label) {
-        white-space: nowrap
-    }
-
-    .device-detail-drawer :deep(.el-descriptions__content) {
-        overflow-wrap: anywhere;
-        word-break: break-word
-    }
-
-    .detail-media {
-        margin-bottom: 14px;
-        padding: 12px;
-        background: #f7fbff;
-        border: 1px solid rgba(37, 99, 235, .1);
-        border-radius: 10px
-    }
-
-    .detail-media-label {
-        margin-bottom: 8px;
-        font-weight: 600;
-        color: #17324d
-    }
-
-    .detail-image {
-        width: 100%;
-        height: 180px;
-        border-radius: 8px;
-        overflow: hidden
-    }
-
-    .detail-image-placeholder {
-        display: grid;
-        place-items: center;
-        height: 120px;
-        background: #f1f5f9;
-        border-radius: 8px;
-        color: #909399
-    }
-
-    .full-input {
-        width: 100%
-    }
-
-    .import-update {
-        margin-top: 10px
-    }
-
-    .import-tip {
-        line-height: 1.6
-    }
-
-    .el-pagination {
-        justify-content: flex-end;
-        margin-top: 16px
-    }
-
-    .dispatch-layout {
-        display: flex;
-        gap: 20px;
-        min-height: 420px
-    }
-
-    .dispatch-form {
-        width: 300px;
-        min-width: 280px;
-        padding-left: 20px;
-        border-left: 1px solid #e4e7ed
-    }
-
-    .assignee-field {
-        display: flex;
-        align-items: center;
-        gap: 12px;
-        width: 100%;
-        min-height: 32px
-    }
-
-    .assignee-summary {
-        color: #17324d
-    }
-
-    .assignee-placeholder {
-        color: #909399
-    }
-
-    .assignee-picker {
-        display: flex;
-        gap: 16px;
-        flex: 1;
-        min-width: 0;
-        min-height: 390px
-    }
-
-    .assignee-depts {
-        width: 190px;
-        min-width: 190px;
-        padding-right: 16px;
-        border-right: 1px solid #e4e7ed
-    }
-
-    .assignee-tree {
-        margin-top: 10px;
-        max-height: 340px;
-        overflow: auto
-    }
-
-    .assignee-users {
-        flex: 1;
-        min-width: 0
-    }
-
-    .assignee-search {
-        display: flex;
-        gap: 8px;
-        margin-bottom: 12px
-    }
-
-    .assignee-search .el-input {
-        flex: 1
-    }
-
-    .assignee-users .el-pagination {
-        margin-top: 12px
-    }
-
-    .assignee-picked {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        gap: 8px;
-        margin-top: 14px;
-        padding: 10px 12px;
-        background: #f5f7fa;
-        border: 1px solid #ebeef5;
-        color: #606266
-    }
-
-    .assignee-picked strong {
-        color: #2563eb
-    }
-
-    .assignee-users .el-alert {
-        margin-bottom: 12px
-    }
-
-    @media (max-width: 900px) {
-        .metric-grid {
-            grid-template-columns:repeat(2, minmax(0, 1fr))
-        }
-
-        .filter-card :deep(.el-form-item) {
-            margin-bottom: 12px
-        }
-    }
-
-    @media (max-width: 768px) {
-        .device-detail-drawer, .device-edit-drawer {
-            width: 100% !important
-        }
-
-        .dispatch-dialog {
-            width: calc(100% - 24px) !important;
-            margin-left: auto;
-            margin-right: auto
-        }
-
-        .dispatch-layout {
-            flex-direction: column
-        }
-
-        .assignee-picker {
-            flex-direction: column
-        }
-
-        .assignee-depts {
-            width: auto;
-            min-width: 0;
-            padding-right: 0;
-            padding-bottom: 12px;
-            border-right: 0;
-            border-bottom: 1px solid #e4e7ed
-        }
-
-        .assignee-tree {
-            max-height: 180px
-        }
-
-        .dispatch-form {
-            width: auto;
-            min-width: 0;
-            padding-left: 0;
-            padding-top: 16px;
-            border-left: 0;
-            border-top: 1px solid #e4e7ed
-        }
-    }
-</style>
-<style lang="scss">
-    @use './waterSupplyPageTheme.scss';
-    @media (max-width: 768px) {
-        .device-detail-drawer {
-            width: 100% !important
-        }
-
-        .dispatch-dialog {
-            width: calc(100% - 24px) !important;
-            margin-left: auto;
-            margin-right: auto
-        }
-
-        .dispatch-layout {
-            flex-direction: column
-        }
-
-        .assignee-picker {
-            flex-direction: column
-        }
-
-        .assignee-depts {
-            width: auto;
-            min-width: 0;
-            padding-right: 0;
-            padding-bottom: 12px;
-            border-right: 0;
-            border-bottom: 1px solid #e4e7ed
-        }
-
-        .assignee-tree {
-            max-height: 180px
-        }
-
-        .dispatch-form {
-            width: auto;
-            min-width: 0;
-            padding-left: 0;
-            padding-top: 16px;
-            border-left: 0;
-            border-top: 1px solid #e4e7ed
-        }
-    }
-</style>

+ 1135 - 899
src/views/subSystem/waterSupply/components/WaterSupplyHome.vue

@@ -1,1140 +1,1376 @@
+<script setup>
+import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { Odometer, CircleCheckFilled, VideoCameraFilled, WarnTriangleFilled } from '@element-plus/icons-vue'
+import { convertPipeline, convertPoints } from '@/utils/coordTransform'
+import {
+  getAlarmStats,
+  getWaterFacilityFeatures,
+  getWaterFacilityOverview,
+  getDeviceGisAllPoints,
+  getDeviceGisStatistics,
+  listRealtimeAlarms,
+  listFlowMonitoring,
+  listPressureMonitoring,
+  listLeakageMonitoring
+} from '@/api/pipeNetwork/waterSupply'
+import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
+import { normalizeGeoJson, normalizeMonitoringRecord, normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
+
+const nowText = ref('')
+const weekText = ref('')
+const mapError = ref('')
+const loading = ref(false)
+
+const dashboardData = ref({})
+const pipelineLines = ref([])
+const pipePoints = ref([])
+
+const facilityTypes = [
+  { label: '水源地', value: 'source', color: '#38a3ff' },
+  { label: '水厂', value: 'plant', color: '#38d0a0' },
+  { label: '泵站', value: 'pumpStation', color: '#ffc15e' },
+  { label: '管网', value: 'pipe', color: '#7b9bb8' },
+  { label: '用水户', value: 'user', color: '#ff7894' }
+]
+
+const typeCount = type => {
+  const row = (dashboardData.value.facilityOverview || []).find(item => item.facilityType === type)
+  return Number(row?.totalCount ?? row?.count ?? 0)
+}
+
+const baseCards = computed(() => {
+  const fs = dashboardData.value.facilityOverview || []
+  const es = dashboardData.value.equipmentStats || {}
+  const alarm = dashboardData.value.alarmStats || {}
+  const totalFacility = fs.reduce((sum, item) => sum + Number(item.totalCount || 0), 0)
+  const totalDevice = Number(es.total ?? es.totalDeviceCount ?? 0)
+  const onlineDevice = Number(es.online ?? es.onlineCount ?? 0)
+  const realtimeAlarm = Number(alarm.realtimeAlarmCount ?? alarm.realtimeCount ?? 0)
+  return [
+    { label: '设施总数', value: totalFacility, unit: '处', icon: Odometer, color: '#60b8ff' },
+    { label: '设备总数', value: totalDevice, unit: '台', icon: CircleCheckFilled, color: '#52e0a8' },
+    { label: '在线设备', value: onlineDevice, unit: '台', icon: VideoCameraFilled, color: '#ff8e8e' },
+    { label: '实时报警', value: realtimeAlarm, unit: '条', icon: WarnTriangleFilled, color: '#ffb54d' }
+  ]
+})
+
+const operationSummary = computed(() => {
+  return [
+    { label: '供水管网', value: typeCount('pipe') + '条' },
+    { label: '水厂', value: typeCount('plant') + '座' },
+    { label: '泵站', value: typeCount('pumpStation') + '座' }
+  ]
+})
+
+const repairRecords = computed(() => {
+  const list = dashboardData.value.facilityOverview || []
+  if (list.length) {
+    return list.map(item => ({ month: item.facilityTypeName || item.facilityType || '设施', value: Number(item.totalCount || 0) }))
+  }
+  return [
+    { month: '水源地', value: 0 }, { month: '水厂', value: 0 }, { month: '泵站', value: 0 },
+    { month: '管网', value: 0 }, { month: '用水户', value: 0 }
+  ]
+})
+
+const pointRows = computed(() => {
+  const list = dashboardData.value.pointRows || []
+  return list.length > 0 ? list : [{ id: 1, name: '暂无数据', time: '--', value: '--', status: '离线' }]
+})
+
+const equipmentRows = computed(() => {
+  const list = dashboardData.value.equipmentRows || []
+  return list.length > 0 ? list : [{ id: 1, name: '暂无数据', code: '--', location: '--', status: '--' }]
+})
+
+const warningRows = computed(() => {
+  const list = dashboardData.value.warningRows || []
+  return list.length > 0 ? list : [{ level: '中', title: '暂无告警', code: '--', time: '--:--:--', desc: '当前无告警信息', tag: '正常' }]
+})
+
+const hazardSummary = computed(() => {
+  const alarm = dashboardData.value.alarmStats || {}
+  return [
+    { label: '实时报警', value: alarm.realtimeAlarmCount ?? alarm.realtimeCount ?? 0, color: '#ff6d79' },
+    { label: '今日报警', value: alarm.todayAlarmCount ?? alarm.todayCount ?? 0, color: '#ffb54d' },
+    { label: '异常率', value: alarm.deviceAbnormalRate ?? alarm.abnormalRate ?? 0, color: '#f3e64d' }
+  ]
+})
+
+const mapKpis = computed(() => {
+  const es = dashboardData.value.equipmentStats || {}
+  const alarm = dashboardData.value.alarmStats || {}
+  const totalDevice = Number(es.total ?? es.totalDeviceCount ?? 0)
+  const onlineDevice = Number(es.online ?? es.onlineCount ?? 0)
+  return [
+    { label: '设备在线率', value: totalDevice > 0 ? (onlineDevice / totalDevice * 100).toFixed(1) + '%' : '--' },
+    { label: '供水设施', value: dashboardData.value.facilityOverview?.reduce((sum, item) => sum + Number(item.totalCount || 0), 0) || 0 },
+    { label: '今日报警', value: (alarm.todayAlarmCount ?? alarm.todayCount ?? 0) + '条' }
+  ]
+})
+
+const mapWrapRef = ref(null)
+const repairChartRef = ref(null)
+const hazardChartRef = ref(null)
+let mapInstance = null
+let repairChart = null
+let hazardChart = null
+let clockTimer = null
+
+function formatWeek(day) {
+  return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
+}
+
+function pad(value) {
+  return String(value).padStart(2, '0')
+}
+
+function updateClock() {
+  const now = new Date()
+  nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+  weekText.value = formatWeek(now.getDay())
+}
+
+function initRepairChart() {
+  if (!repairChartRef.value) return
+  repairChart?.dispose()
+  repairChart = echarts.init(repairChartRef.value)
+  const data = repairRecords.value
+  repairChart.setOption({
+    grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
+    xAxis: {
+      type: 'category',
+      data: data.map(item => item.month),
+      axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
+      axisLabel: { color: '#d2efff' }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
+      axisLine: { show: false },
+      axisLabel: { color: '#d2efff' }
+    },
+    tooltip: { trigger: 'axis' },
+    series: [{
+      type: 'bar',
+      barWidth: 24,
+      data: data.map(item => item.value),
+      itemStyle: {
+        borderRadius: [6, 6, 0, 0],
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#38d8ff' },
+          { offset: 1, color: '#166dff' }
+        ])
+      }
+    }]
+  })
+}
+
+function initHazardChart() {
+  if (!hazardChartRef.value) return
+  hazardChart?.dispose()
+  hazardChart = echarts.init(hazardChartRef.value)
+  const data = hazardSummary.value
+  hazardChart.setOption({
+    grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
+    xAxis: {
+      type: 'value',
+      splitLine: { show: false },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { show: false }
+    },
+    yAxis: {
+      type: 'category',
+      data: data.map(item => item.label),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: '#d2efff', fontSize: 13 }
+    },
+    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+    series: [{
+      type: 'bar',
+      data: data.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
+      barWidth: 14,
+      label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
+      showBackground: true,
+      backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
+    }]
+  })
+}
+
+function waitForBMapGL() {
+  return new Promise((resolve, reject) => {
+    if (window.BMapGL) {
+      resolve(window.BMapGL)
+      return
+    }
+    let attempts = 0
+    const timer = setInterval(() => {
+      attempts += 1
+      if (window.BMapGL) {
+        clearInterval(timer)
+        resolve(window.BMapGL)
+        return
+      }
+      if (attempts >= 100) {
+        clearInterval(timer)
+        reject(new Error('百度地图加载超时'))
+      }
+    }, 200)
+  })
+}
+
+function addPipeLine(BMapGL, pipe) {
+  const bmapPoints = pipe.points.map(([lng, lat]) => new BMapGL.Point(lng, lat))
+  const polyline = new BMapGL.Polyline(bmapPoints, {
+    strokeColor: pipe.color,
+    strokeWeight: 4,
+    strokeOpacity: 0.95
+  })
+  mapInstance.addOverlay(polyline)
+
+  bmapPoints.forEach((point) => {
+    const ring = new BMapGL.Circle(point, 14, {
+      strokeColor: pipe.color,
+      strokeWeight: 2,
+      strokeOpacity: 0.7,
+      fillColor: 'rgba(56,182,233,0.08)',
+      fillOpacity: 0.18
+    })
+    mapInstance.addOverlay(ring)
+  })
+}
+
+function addPipePoint(BMapGL, point) {
+  const markerPoint = new BMapGL.Point(point.lng, point.lat)
+  const marker = new BMapGL.Marker(markerPoint)
+  mapInstance.addOverlay(marker)
+  const label = new BMapGL.Label(`${point.name} ${point.value}`, { offset: new BMapGL.Size(-42, -40) })
+  label.setPosition(markerPoint)
+  label.setStyle({
+    color: '#d8faff',
+    background: 'rgba(18, 68, 112, 0.75)',
+    border: '1px solid rgba(56, 182, 233, 0.55)',
+    borderRadius: '8px',
+    padding: '5px 8px',
+    fontSize: '12px'
+  })
+  mapInstance.addOverlay(label)
+}
+
+async function initMap() {
+  if (!mapWrapRef.value) return
+  try {
+    const BMapGL = await waitForBMapGL()
+    mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
+    const center = new BMapGL.Point(110.3965, 28.4638)
+    mapInstance.centerAndZoom(center, 15)
+    mapInstance.enableScrollWheelZoom(true)
+    mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
+
+    const bdPipelineLines = pipelineLines.value.map(pipe => convertPipeline(pipe))
+    const bdPipePoints = convertPoints(pipePoints.value)
+    bdPipelineLines.forEach(pipe => addPipeLine(BMapGL, pipe))
+    bdPipePoints.forEach(point => addPipePoint(BMapGL, point))
+  } catch (error) {
+    mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
+  }
+}
+
+function handleResize() {
+  repairChart?.resize()
+  hazardChart?.resize()
+}
+
+function parseFeatures(features) {
+  const lines = []
+  const points = []
+  ;(features || []).forEach(feature => {
+    const coordinates = feature.geometry?.coordinates || []
+    const properties = feature.properties || {}
+    if (feature.geometry?.type === 'LineString' && coordinates.length > 1) {
+      lines.push({
+        id: feature.id || `line-${lines.length}`,
+        name: properties.facilityTypeName || properties.name || '供水管网',
+        color: '#37b6e9',
+        points: coordinates
+      })
+    }
+    if (feature.geometry?.type === 'Point' && coordinates.length >= 2) {
+      points.push({
+        id: feature.id || properties.id,
+        name: properties.facilityName || properties.name || properties.facilityTypeName || '供水设施',
+        value: properties.facilityTypeName || properties.facilityType || '',
+        lng: coordinates[0],
+        lat: coordinates[1],
+        type: properties.facilityType,
+        properties
+      })
+    }
+  })
+  pipelineLines.value = lines
+  pipePoints.value = points
+}
+
+function buildPointRows(flowPage, pressurePage, leakagePage) {
+  const flow = normalizePage(flowPage).records.map(item => normalizeMonitoringRecord(item, 'flow'))
+  const pressure = normalizePage(pressurePage).records.map(item => normalizeMonitoringRecord(item, 'pressure'))
+  const leakage = normalizePage(leakagePage).records.map(item => normalizeMonitoringRecord(item, 'power'))
+  return [...flow, ...pressure, ...leakage].slice(0, 8).map(item => ({
+    id: item.id,
+    name: item.equipmentName,
+    time: item.updateTime,
+    value: item.value == null ? '--' : item.value,
+    status: item.status === 1 || item.status === '1' ? '预警' : '在线'
+  }))
+}
+
+async function loadDashboardData() {
+  loading.value = true
+  try {
+    const [overviewResponse, featuresResponse, devicePointsResponse, deviceStatsResponse, alarmResponse, alarmSummaryResponse, devicePageResponse, flowResponse, pressureResponse, leakageResponse] = await Promise.all([
+      getWaterFacilityOverview(),
+      getWaterFacilityFeatures(),
+      getDeviceGisAllPoints(),
+      getDeviceGisStatistics(),
+      listRealtimeAlarms(),
+      getAlarmStats(),
+      listEquipment({ pageNum: 1, pageSize: 8, rootTypeId: 1 }),
+      listFlowMonitoring({ pageNum: 1, pageSize: 5 }),
+      listPressureMonitoring({ pageNum: 1, pageSize: 5 }),
+      listLeakageMonitoring({ pageNum: 1, pageSize: 5 })
+    ])
+    const overviewData = unwrapResponse(overviewResponse)
+    const facilityOverview = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
+    const features = normalizeGeoJson(featuresResponse).features || []
+    const deviceData = unwrapResponse(devicePointsResponse)
+    const deviceRows = Array.isArray(deviceData) ? deviceData : (deviceData?.features || deviceData?.records || [])
+    const devicePage = normalizePage(devicePageResponse)
+    const rawStats = unwrapResponse(deviceStatsResponse) || {}
+    const typeStatistics = rawStats.typeStatistics || []
+    const flatten = list => (Array.isArray(list) ? list : []).reduce((sum, item) => sum + Number(item.deviceCount || item.count || 0) + flatten(item.children), 0)
+    const equipmentStats = {
+      ...rawStats,
+      total: Number(rawStats.total ?? rawStats.totalDeviceCount ?? 0) || flatten(typeStatistics) || deviceRows.length,
+      online: Number(rawStats.online ?? rawStats.onlineCount ?? deviceRows.filter(item => Number(item.onlineStatus) === 1).length),
+      abnormal: Number(rawStats.abnormal ?? rawStats.abnormalCount ?? deviceRows.filter(item => Number(item.alarmStatus) === 1).length)
+    }
+    const alarmList = Array.isArray(unwrapResponse(alarmResponse)) ? unwrapResponse(alarmResponse) : []
+    const alarmStats = unwrapResponse(alarmSummaryResponse) || {}
+    const equipmentRows = devicePage.records.slice(0, 8).map(item => ({
+      id: item.equipmentId || item.id,
+      name: item.equipmentName || item.name || '-',
+      code: item.equipmentCode || item.code || '-',
+      location: item.equipmentLocation || item.location || '-',
+      status: Number(item.status || item.onlineStatus) === 1 ? '异常' : '正常'
+    }))
+    const warningRows = alarmList.slice(0, 6).map(item => ({
+      level: Number(item.alarmLevel) <= 2 ? '高' : '中',
+      title: item.deviceName || item.deviceCode || '监测设备',
+      code: item.warningType || item.alarmCode || '-',
+      time: item.alarmTime || '--',
+      desc: item.equipmentLocation || '报警详情待查看',
+      tag: Number(item.alarmLevel) <= 2 ? '紧急' : '一般'
+    }))
+    dashboardData.value = {
+      facilityOverview,
+      equipmentStats,
+      alarmStats,
+      warningRows,
+      equipmentRows,
+      pointRows: buildPointRows(flowResponse, pressureResponse, leakageResponse)
+    }
+    parseFeatures(features)
+  } catch (error) {
+    console.warn('供水首页数据加载失败', error)
+    dashboardData.value = { facilityOverview: [], equipmentStats: {}, alarmStats: {}, warningRows: [], equipmentRows: [], pointRows: [] }
+    pipelineLines.value = []
+    pipePoints.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(async () => {
+  updateClock()
+  clockTimer = window.setInterval(updateClock, 1000)
+  await loadDashboardData()
+  await nextTick()
+  initRepairChart()
+  initHazardChart()
+  initMap()
+  window.addEventListener('resize', handleResize)
+})
+
+onBeforeUnmount(() => {
+  if (clockTimer) window.clearInterval(clockTimer)
+  window.removeEventListener('resize', handleResize)
+  repairChart?.dispose()
+  hazardChart?.dispose()
+  mapInstance = null
+})
+</script>
+
+
 <template>
-  <div class="water-page home-page">
-    <div class="page-heading">
-      <div class="page-title">
-        <span class="page-title-icon"><el-icon><HomeFilled /></el-icon></span>
-        <div>
-          <h2>供水首页工作台</h2>
-          <span class="muted">面向值班态的一站式供水运行工作台,仅统计供水设施与供水监测设备</span>
-        </div>
+  <div class="gas-home-screen">
+    <div class="screen-header">
+      <div class="header-time">{{ nowText }} {{ weekText }}</div>
+      <div class="header-title-wrap">
+        <div class="header-title">供水管网安全运行监测系统</div>
+        <div class="header-subtitle">Water Supply Operation Dashboard</div>
       </div>
-      <div class="heading-actions">
-        <el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
+      <div class="header-tags">
+        <span class="header-tag">实时监测</span>
+        <span class="header-tag">供水设施</span>
+        <span class="header-tag">报警预警</span>
       </div>
     </div>
 
-    <el-alert v-if="loadError" class="load-error" type="error" show-icon :closable="false"
-              title="部分数据加载失败" :description="loadError">
-      <el-button size="small" type="primary" :loading="loading" @click="loadAll">重试</el-button>
-    </el-alert>
-
-    <!-- A 指挥台 -->
-    <section class="home-section">
-      <div class="metric-grid">
-        <div v-for="card in homeKpis" :key="card.key" class="metric">
-          <div class="metric-icon"><el-icon><component :is="kpiIcon(card.key)"/></el-icon></div>
-          <div class="metric-body">
-            <span>{{ card.label }}</span>
-            <div class="metric-value">
-              <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
+    <div class="screen-body">
+      <div class="side-panel left-panel">
+        <section class="panel-card base-card">
+          <div class="panel-title">基础概况</div>
+          <div class="base-grid">
+            <div v-for="item in baseCards" :key="item.label" class="base-item">
+              <div class="base-icon" :style="{ background: item.color }">
+                <component :is="item.icon" class="base-icon-svg" />
+              </div>
+              <div class="base-text">
+                <div class="base-label">{{ item.label }}</div>
+                <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
+              </div>
             </div>
           </div>
-        </div>
-      </div>
-      <div class="workbench-grid">
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Bell /></el-icon><span>实时报警</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/monitorAlarm/index')">前往报警管理</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in alarmRows" :key="item.id || item.deviceCode" type="button"
-                    class="compact-item" @click="openRealtimeAlarm(item)">
-              <span class="status-dot dot-alarm" />
-              <span class="compact-main">
-                <b>{{ item.deviceName }}</b>
-                <small>{{ item.deviceCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="item.displayStatusType">{{ item.displayStatusText }}</el-tag>
-                <el-tag size="small" :type="levelType(item.alarmLevel)">{{ item.alarmLevelText }}</el-tag>
-                <small>{{ item.alarmTime }}</small>
-              </span>
-            </button>
-            <el-empty v-if="!alarmRows.length" description="当前无实时报警" :image-size="60" />
+        </section>
+
+        <section class="panel-card">
+          <div class="panel-title">设施统计</div>
+          <div ref="repairChartRef" class="chart-box repair-chart"></div>
+        </section>
+
+        <section class="panel-card table-card">
+          <div class="panel-title">监测点数据</div>
+          <div class="table-wrap">
+            <table class="data-table">
+              <thead>
+                <tr>
+                  <th>序号</th>
+                  <th>监测点</th>
+                  <th>时间</th>
+                  <th>数值</th>
+                  <th>状态</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="row in pointRows" :key="row.id">
+                  <td>{{ row.id }}</td>
+                  <td>{{ row.name }}</td>
+                  <td>{{ row.time }}</td>
+                  <td>{{ row.value }}</td>
+                  <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
+                </tr>
+              </tbody>
+            </table>
           </div>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel map-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><MapLocation /></el-icon><span>全域态势</span>
-              <div class="map-legend">
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.alarm }" />报警/异常
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.workOrder }" />工单/检修中
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.warning }" />预警/欠费
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.offline }" />离线
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.normal }" />正常
+        </section>
+      </div>
+
+      <div class="center-panel">
+        <div class="map-stage">
+          <div class="map-stage-top">
+            <div class="summary-strip">
+              <div v-for="item in operationSummary" :key="item.label" class="summary-item">
+                <span class="summary-label">{{ item.label }}</span>
+                <strong class="summary-value">{{ item.value }}</strong>
               </div>
-              <div class="panel-links">
-                <el-button link type="primary" class="panel-link" @click="goPage('/facility/gis')">设施</el-button>
-                <el-button link type="primary" class="panel-link" @click="goPage('/device/gis')">设备</el-button>
-                <el-button link type="primary" class="panel-link" @click="goPage('/monitorAlarm/map')">报警</el-button>
+            </div>
+            <div class="map-kpis">
+              <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
+                <span>{{ item.label }}</span>
+                <strong>{{ item.value }}</strong>
               </div>
             </div>
-          </template>
-          <div class="map-stage">
-            <GisMapCanvas
-              ref="overviewMapRef"
-              :points="overviewMapPoints"
-              :lines="overviewMapLines"
-              cluster-label="供水点位"
-              :center="overviewMapCenter"
-              :zoom="overviewMapZoom"
-              @point-click="openOverviewPoint"
-              @line-click="openOverviewPoint"
-              @map-click="overviewDetailOpen = false"
-              @ready="handleOverviewMapReady"
-              @error="overviewMapError = $event"
-            />
-            <el-alert v-if="overviewMapError" class="map-inline-alert" type="warning" show-icon :closable="false"
-                      :title="overviewMapError">
-              <el-button link type="primary" @click="loadAll">重试</el-button>
-            </el-alert>
-            <el-empty v-else-if="overviewLoaded && !overviewMapPoints.length && !overviewMapLines.length"
-                      class="map-inline-empty" description="暂无可展示的全域点位" :image-size="70" />
           </div>
-          <el-drawer v-model="overviewDetailOpen" title="点位详情" size="420px" direction="rtl"
-                     class="detail-drawer" append-to-body>
-            <div v-if="overviewDetail" class="overview-detail">
-              <section class="detail-hero">
-                <h3>{{ overviewDetail.name }}</h3>
-                <p>{{ overviewDetail.code }}</p>
-                <div class="status-tags">
-                  <el-tag size="small" :type="gisStatusTagType(overviewDetail.statusKey)">
-                    {{ overviewDetail.statusText || gisStatusLabel(overviewDetail.statusKey) }}
-                  </el-tag>
-                  <el-tag v-if="overviewDetail.meta" size="small" type="info">{{ overviewDetail.meta }}</el-tag>
-                </div>
-              </section>
-              <section class="detail-section">
-                <h4>点位信息</h4>
-                <dl>
-                  <dt>名称</dt><dd>{{ overviewDetail.name }}</dd>
-                  <dt>编码</dt><dd>{{ overviewDetail.code }}</dd>
-                  <dt>类别</dt><dd>{{ overviewKindLabel(overviewDetail.kind) }}</dd>
-                  <dt>状态</dt><dd>{{ overviewDetail.statusText || gisStatusLabel(overviewDetail.statusKey) }}</dd>
-                  <dt>类型 / 报警</dt><dd>{{ overviewDetail.meta }}</dd>
-                  <dt>经度</dt><dd>{{ overviewDetail.lng }}</dd>
-                  <dt>纬度</dt><dd>{{ overviewDetail.lat }}</dd>
-                </dl>
-              </section>
-            </div>
-            <el-empty v-else description="请点击地图点位查看详情" :image-size="70" />
-          </el-drawer>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Tickets /></el-icon><span>工单进展</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/device/maintain')">设备维修派单</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in workOrderRows" :key="item.orderId || item.orderNo" type="button"
-                    class="compact-item" @click="goPage('/device/maintain', { keyword: item.orderNo })">
-              <span class="status-dot" :class="dotByOrderType(item.orderStatusType)" />
-              <span class="compact-main">
-                <b>{{ item.orderNo }}</b>
-                <small>{{ item.deviceName || item.deviceCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="item.orderStatusType">{{ item.orderStatusText }}</el-tag>
-              </span>
-            </button>
-            <el-empty v-if="!workOrderRows.length" description="当前暂无供水工单" :image-size="60" />
+
+          <div ref="mapWrapRef" class="map-view"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
+
+          <div class="map-panel info-panel">
+            <div class="mini-title">供水设施信息</div>
+            <div class="mini-row"><span>供水管网</span><strong>{{ typeCount('pipe') }}条</strong></div>
+            <div class="mini-row"><span>水厂</span><strong>{{ typeCount('plant') }}座</strong></div>
+            <div class="mini-row"><span>泵站</span><strong>{{ typeCount('pumpStation') }}座</strong></div>
           </div>
-          <div class="order-brief">
-            <span>待处理 <b>{{ workOrderStat.pendingCount }}</b></span>
-            <span>处理中 <b>{{ workOrderStat.processingCount }}</b></span>
-            <span>待验收 <b>{{ workOrderStat.verifyingCount }}</b></span>
-            <span>已完成 <b>{{ workOrderStat.finishedCount }}</b></span>
+
+          <div class="map-panel point-panel">
+            <div class="mini-title">当前监测信息</div>
+            <div class="mini-row"><span>监测点总数</span><strong>{{ pointRows.length }}个</strong></div>
+            <div class="mini-row"><span>在线设备</span><strong>{{ dashboardData.equipmentStats?.online || 0 }}台</strong></div>
+            <div class="mini-row"><span>实时报警</span><strong>{{ dashboardData.alarmStats?.realtimeAlarmCount ?? dashboardData.alarmStats?.realtimeCount ?? 0 }}条</strong></div>
           </div>
-        </el-card>
+        </div>
       </div>
-    </section>
-
-    <!-- B 异常待办 -->
-    <section class="home-section">
-      <div class="workbench-grid workbench-grid--equal">
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Warning /></el-icon><span>异常待办</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/device/abnormal')">查看全部</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in abnormalTodoRows" :key="item.equipmentId" type="button"
-                    class="compact-item" :class="{ active: selectedDevice && selectedDevice.equipmentId === item.equipmentId }"
-                    @click="selectAbnormal(item)">
-              <span class="status-dot" :class="abnormalDot(item)" />
-              <span class="compact-main">
-                <b>{{ item.equipmentName || item.equipmentCode }}</b>
-                <small>{{ item.equipmentCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="abnormalType(item)">{{ item.abnormalReasonText || '设备异常' }}</el-tag>
-              </span>
-            </button>
-            <el-empty v-if="!abnormalTodoRows.length" description="当前无异常设备待办" :image-size="60" />
+
+      <div class="side-panel right-panel">
+        <section class="panel-card">
+          <div class="panel-title">设备运行</div>
+          <div class="table-wrap equipment-wrap">
+            <table class="data-table compact-table">
+              <thead>
+                <tr>
+                  <th>序号</th>
+                  <th>设备名称</th>
+                  <th>设备编号</th>
+                  <th>位置</th>
+                  <th>状态</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="row in equipmentRows" :key="row.id">
+                  <td>{{ row.id }}</td>
+                  <td>{{ row.name }}</td>
+                  <td>{{ row.code }}</td>
+                  <td>{{ row.location }}</td>
+                  <td>{{ row.status }}</td>
+                </tr>
+              </tbody>
+            </table>
           </div>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel detail-panel">
-          <template #header>
-            <div class="card-title"><el-icon><Cpu /></el-icon><span>设备详情</span></div>
-          </template>
-          <div v-if="selectedDevice" class="device-summary">
-            <div class="summary-top">
-              <div class="summary-name">{{ selectedDevice.name }}</div>
-              <div class="summary-tags">
-                <el-tag size="small" :type="Number(selectedDevice.alarmStatus) === 1 ? 'danger' : 'success'">{{ selectedDevice.alarmStatusText }}</el-tag>
-                <el-tag size="small" :type="Number(selectedDevice.onlineStatus) === 1 ? 'success' : 'info'">{{ selectedDevice.onlineStatusText }}</el-tag>
-                <el-tag v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" size="small"
-                        :type="selectedDevice.workOrderStatusCode != null ? orderType(selectedDevice.workOrderStatusCode) : 'info'">
-                  {{ selectedDevice.workOrderStatusText }}
-                </el-tag>
+        </section>
+
+        <section class="panel-card warning-card">
+          <div class="panel-title">告警动态</div>
+          <div class="warning-scroll">
+            <div class="warning-list">
+              <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
+                <div class="warning-side">
+                  <div class="warning-badge">{{ item.level }}</div>
+                  <div class="warning-tag">{{ item.tag }}</div>
+                </div>
+                <div class="warning-main">
+                  <div class="warning-title-row">
+                    <span class="warning-title">{{ item.title }}</span>
+                    <span class="warning-time">{{ item.time }}</span>
+                  </div>
+                  <div class="warning-code">{{ item.code }}</div>
+                  <div class="warning-desc">{{ item.desc }}</div>
+                </div>
               </div>
             </div>
-            <el-descriptions :column="1" border size="small">
-              <el-descriptions-item label="设备编码">{{ selectedDevice.code }}</el-descriptions-item>
-              <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
-              <el-descriptions-item label="所属区域">{{ selectedDevice.district || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="安装位置">{{ selectedDevice.location || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="报警等级">{{ selectedDevice.alarmLevelText }}</el-descriptions-item>
-              <el-descriptions-item label="异常原因">{{ selectedDevice.abnormalReasonText || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="关联工单">
-                <el-button v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" link type="primary"
-                           @click="goPage('/device/maintain', { keyword: selectedDevice.relatedOrderNo })">
-                  {{ selectedDevice.relatedOrderNo }}
-                </el-button>
-                <span v-else>-</span>
-              </el-descriptions-item>
-            </el-descriptions>
-          </div>
-          <el-empty v-else description="从左侧异常待办中选择设备" :image-size="70" />
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title"><el-icon><Grid /></el-icon><span>快捷入口</span></div>
-          </template>
-          <div class="quick-grid">
-            <button v-for="entry in quickEntries" :key="entry.key" type="button" class="quick-entry"
-                    @click="goPage(entry.suffix)">
-              <el-icon><component :is="entryIcon(entry.key)"/></el-icon>
-              <span>{{ entry.label }}</span>
-            </button>
           </div>
-        </el-card>
-      </div>
-    </section>
-
-    <!-- C 数据总览 -->
-    <section class="home-section">
-      <div class="metric-grid">
-        <div v-for="card in overviewCards" :key="card.key" class="metric">
-          <div class="metric-icon"><el-icon><component :is="overviewIcon(card.key)"/></el-icon></div>
-          <div class="metric-body">
-            <span>{{ card.label }}</span>
-            <div class="metric-value">
-              <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
+        </section>
+
+        <section class="panel-card hazard-card">
+          <div class="panel-title">报警统计</div>
+          <div class="hazard-summary three-col">
+            <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
+              <span class="hazard-dot" :style="{ background: item.color }"></span>
+              <span>{{ item.label }}</span>
+              <strong :style="{ color: item.color }">{{ item.value }}项</strong>
             </div>
           </div>
-        </div>
-      </div>
-      <div class="chart-grid">
-        <el-card shadow="never" class="water-panel chart-card trend-card">
-          <template #header>
-            <div class="card-title">
-              <el-icon><TrendCharts /></el-icon><span>报警趋势</span>
-              <span class="muted">近 {{ trendDays }} 天 · 按四个报警等级分别统计</span>
-            </div>
-          </template>
-          <div ref="trendChartRef" class="chart" />
-        </el-card>
-        <el-card shadow="never" class="water-panel chart-card">
-          <template #header>
-            <div class="card-title"><el-icon><PieChart /></el-icon><span>报警分类占比</span></div>
-          </template>
-          <div ref="warningPieRef" class="chart" />
-        </el-card>
-        <el-card shadow="never" class="water-panel chart-card">
-          <template #header>
-            <div class="card-title"><el-icon><DataAnalysis /></el-icon><span>设备分类占比</span></div>
-          </template>
-          <div ref="devicePieRef" class="chart" />
-        </el-card>
+          <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
+        </section>
       </div>
-    </section>
+    </div>
+
+    <a href="https://deerflow.tech" target="_blank" class="deerflow-mark" title="Created By Deerflow">
+      <span class="deerflow-diamond">◆</span>
+      <span class="deerflow-text">Deerflow</span>
+    </a>
   </div>
 </template>
 
-<script setup>
-import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import * as echarts from 'echarts'
-import {
-  Bell, BellFilled, Connection, Cpu, DataAnalysis, Finished, Grid, HomeFilled,
-  MapLocation, OfficeBuilding, PieChart, Refresh, Tickets, TrendCharts,
-  Warning, WarningFilled
-} from '@element-plus/icons-vue'
-import { getAlarmLedgerDashboard, getAlarmStats, getDeviceGisAllPoints, getDeviceLedgerStatistics,
-  getWaterFacilityFeatures, getWaterFacilityOverview, listAbnormalDevices, listAlarmMapPoints,
-  listRealtimeAlarms } from '@/api/pipeNetwork/waterSupply'
-import { listWorkOrder, workOrderStatusCount } from '@/api/pipeNetwork/ops/workOrder'
-import { normalizeGeoJson, normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
-import { normalizeDeviceGisPoints } from '@/utils/waterSupplyUiModel'
-import { gisPointStatusColors, gisPointStatusLabels, gisPointStatusTagTypes } from '@/utils/gisMapColors'
-import { buildFacilityMapModel } from '@/utils/waterFacilityGisModel'
-import GisMapCanvas from '@/components/GisMapCanvas.vue'
-import {
-  HOME_QUICK_ENTRIES, buildHomeGisOverview, buildHomeWorkOrderQuery,
-  normalizeAbnormalTodoRows, normalizeAlarmOverviewCards, normalizeAlarmTrend, normalizeDeviceSummary,
-  normalizeHomeKpis, normalizePieRatio, normalizeRealtimeAlarmRows, realtimeAlarmJumpTarget,
-  normalizeWorkOrderRows, normalizeWorkOrderStatusStat
-} from '@/utils/waterHomeModel'
-
-const router = useRouter()
-
-const loading = ref(true)
-const loadError = ref('')
-const trendDays = 7
-const quickEntries = HOME_QUICK_ENTRIES
-
-const facilityRows = ref([])
-const ledgerStats = ref({})
-const alarmStats = ref({})
-const alarmDashboard = ref({})
-const alarmRows = ref([])
-const workOrderRows = ref([])
-const workOrderStat = ref({})
-const abnormalTodoRows = ref([])
-const overviewMapPoints = ref([])
-const overviewMapLines = ref([])
-const overviewDeviceRaw = ref([])
-const overviewMapCenter = { lng: 110.3965, lat: 28.4638 }
-const overviewMapZoom = 15
-const overviewMapError = ref('')
-const overviewLoaded = ref(false)
-const overviewDetailOpen = ref(false)
-const overviewDetail = ref(null)
-const overviewMapRef = ref(null)
-const selectedDevice = ref(null)
-
-const homeKpis = ref([])
-const overviewCards = ref([])
-const trendData = ref({ dates: [], levels: [] })
-const warningRatio = ref([])
-const deviceRatio = ref([])
-
-const trendChartRef = ref(null)
-const warningPieRef = ref(null)
-const devicePieRef = ref(null)
-let trendChart = null
-let warningPie = null
-let devicePie = null
-
-const KPI_ICON = {
-  facility: OfficeBuilding,
-  device: Cpu,
-  online: Connection,
-  alarm: BellFilled
-}
-
-const OVERVIEW_ICON = {
-  alarmTotal: BellFilled,
-  alarmingDevice: WarningFilled,
-  verifyingOrder: Tickets,
-  onlineRate: TrendCharts
-}
-
-const ENTRY_ICON = {
-  abnormal: WarningFilled,
-  alarm: BellFilled,
-  audit: Finished,
-  map: MapLocation
-}
-
-function kpiIcon(key) { return KPI_ICON[key] || OfficeBuilding }
-function overviewIcon(key) { return OVERVIEW_ICON[key] || DataAnalysis }
-function entryIcon(key) { return ENTRY_ICON[key] || Grid }
-function levelType(level) {
-  const meta = { 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' }
-  return meta[Number(level)] || 'info'
-}
-function orderType(status) {
-  const meta = {
-    0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning',
-    4: 'primary', 5: 'success', 6: 'warning', 7: 'info'
-  }
-  return meta[Number(status)] || 'info'
-}
-function dotByOrderType(type) {
-  const map = { danger: 'dot-danger', warning: 'dot-warning', primary: 'dot-primary', success: 'dot-success', info: 'dot-info' }
-  return map[type] || 'dot-info'
-}
-function abnormalDot(row) {
-  if (Number(row.alarmStatus) === 1) return 'dot-alarm'
-  if (Number(row.onlineStatus) === 0) return 'dot-offline'
-  return 'dot-warning'
-}
-function abnormalType(row) {
-  if (Number(row.alarmStatus) === 1) return 'danger'
-  if (Number(row.onlineStatus) === 0) return 'info'
-  return 'warning'
+<style scoped lang="scss">
+.gas-home-screen {
+  min-height: 100vh;
+  color: #fff;
+  background:
+    linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
+    radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
+    #081425;
+  position: relative;
 }
 
-async function settle(fn, fallback = null) {
-  try {
-    return await fn()
-  } catch (error) {
-    console.warn('供水首页数据片段加载失败', error)
-    if (fallback !== null) return fallback
-    throw error
-  }
+.gas-home-screen::before {
+  content: '';
+  position: absolute;
+  inset: 0;
+  pointer-events: none;
+  z-index: 0;
+  background:
+    radial-gradient(circle at 28% 32%, rgba(56, 216, 255, 0.06) 0%, transparent 45%),
+    radial-gradient(circle at 72% 58%, rgba(22, 109, 255, 0.05) 0%, transparent 40%),
+    radial-gradient(circle at 50% 85%, rgba(56, 216, 255, 0.04) 0%, transparent 35%);
+  animation: bgPulse 8s ease-in-out infinite alternate;
 }
 
-async function loadAll() {
-  loading.value = true
-  loadError.value = ''
-  const problems = []
-  const recordProblem = label => () => { problems.push(label) }
-
-  const [overview, ledger, alarm, dashboard, realtime, abnormal, orderStat, orders, deviceGis] = await Promise.allSettled([
-    settle(() => getWaterFacilityOverview(), []).catch(recordProblem('设施概览')),
-    settle(() => getDeviceLedgerStatistics(), {}).catch(recordProblem('设备统计')),
-    settle(() => getAlarmStats(), {}).catch(recordProblem('报警统计')),
-    settle(() => getAlarmLedgerDashboard(trendDays), {}).catch(recordProblem('报警态势')),
-    settle(() => listRealtimeAlarms(), []).catch(recordProblem('实时报警')),
-    settle(() => listAbnormalDevices({ pageNum: 1, pageSize: 50 }), { records: [], total: 0 }).catch(recordProblem('异常待办')),
-    settle(() => workOrderStatusCount(buildHomeWorkOrderQuery()), { total: 0, counts: [] }).catch(recordProblem('工单统计')),
-    settle(() => listWorkOrder(buildHomeWorkOrderQuery({ pageSize: 20 })), { records: [], total: 0 }).catch(recordProblem('工单列表')),
-    settle(() => getDeviceGisAllPoints(), { features: [] }).catch(recordProblem('设备点位'))
-  ])
-
-  const overviewData = unwrapResponse(overview.value || [])
-  facilityRows.value = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
-  ledgerStats.value = unwrapResponse(ledger.value || {})
-  alarmStats.value = unwrapResponse(alarm.value || {})
-  alarmDashboard.value = unwrapResponse(dashboard.value || {})
-  alarmRows.value = normalizeRealtimeAlarmRows(unwrapResponse(realtime.value || []), 20)
-  const abnormalPage = normalizePage(abnormal.value || { records: [], total: 0 })
-  abnormalTodoRows.value = normalizeAbnormalTodoRows(abnormalPage.records, 20)
-  workOrderStat.value = normalizeWorkOrderStatusStat(unwrapResponse(orderStat.value || { total: 0, counts: [] }))
-  const orderList = unwrapResponse(orders.value || { records: [] })
-  workOrderRows.value = normalizeWorkOrderRows(orderList?.records || [], 20)
-
-  const gisData = unwrapResponse(deviceGis.value || {})
-  overviewDeviceRaw.value = Array.isArray(gisData) ? gisData : (gisData?.features || [])
-  await loadOverviewMap(recordProblem)
-
-  homeKpis.value = normalizeHomeKpis({ facilityRows: facilityRows.value, ledgerStats: ledgerStats.value, alarmStats: alarmStats.value })
-  overviewCards.value = normalizeAlarmOverviewCards({
-    dashboard: alarmDashboard.value,
-    deviceStats: ledgerStats.value,
-    workOrderStat: workOrderStat.value
-  })
-  trendData.value = normalizeAlarmTrend(alarmDashboard.value?.trend || [])
-  warningRatio.value = normalizePieRatio(alarmDashboard.value?.warningTypeRatio || [], '未分类', 6)
-  deviceRatio.value = normalizePieRatio(alarmDashboard.value?.deviceTypeRatio || [], '未分类', 6)
-
-  if (problems.length) loadError.value = problems.join(';')
-  loading.value = false
-  await nextTick()
-  renderCharts()
+@keyframes bgPulse {
+  0% { opacity: 0.6; }
+  100% { opacity: 1; }
 }
 
-function selectAbnormal(row) {
-  selectedDevice.value = normalizeDeviceSummary(row)
+.screen-header {
+  height: 72px;
+  padding: 0 24px;
+  display: grid;
+  grid-template-columns: 320px 1fr 320px;
+  align-items: center;
+  position: relative;
+  z-index: 1;
+  background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
+  border-bottom: 1px solid rgba(93, 194, 255, 0.28);
 }
 
-async function loadOverviewMap(recordProblem) {
-  overviewMapError.value = ''
-  const [facilityRes, alarmRes] = await Promise.all([
-    settle(() => getWaterFacilityFeatures(), { features: [] }).catch(recordProblem('设施GIS点位')),
-    settle(() => listAlarmMapPoints(), []).catch(recordProblem('报警点位'))
-  ])
-  const facilityModel = buildFacilityMapModel(normalizeGeoJson(facilityRes))
-  const overviewModel = buildHomeGisOverview({
-    facilityPoints: facilityModel.points,
-    facilityLines: facilityModel.lines,
-    devicePoints: normalizeDeviceGisPoints(overviewDeviceRaw.value),
-    alarmPoints: (Array.isArray(alarmRes) ? alarmRes : [])
-  })
-  overviewMapPoints.value = overviewModel.points
-  overviewMapLines.value = overviewModel.lines
-  overviewLoaded.value = true
-  await nextTick()
-  overviewMapRef.value?.fitBounds()
+.screen-header::after {
+  content: '';
+  position: absolute;
+  bottom: -1px;
+  left: 5%;
+  width: 90%;
+  height: 1px;
+  background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.7), rgba(22, 109, 255, 0.7), transparent);
+  animation: headerGlow 4s ease-in-out infinite alternate;
 }
 
-function handleOverviewMapReady() {
-  nextTick(() => overviewMapRef.value?.fitBounds())
+@keyframes headerGlow {
+  0% { opacity: 0.4; }
+  100% { opacity: 1; }
 }
 
-function openOverviewPoint(point) {
-  overviewDetail.value = point || null
-  overviewDetailOpen.value = Boolean(point)
+.header-time {
+  font-size: 18px;
+  font-weight: 700;
+  letter-spacing: 1.5px;
+  color: #ddf6ff;
+  font-variant-numeric: tabular-nums;
+  text-shadow: 0 0 18px rgba(56, 216, 255, 0.35);
 }
 
-function openRealtimeAlarm(item) {
-  const target = realtimeAlarmJumpTarget(item)
-  goPage(target.path, target.query)
+.header-title-wrap {
+  text-align: center;
 }
 
-function resolveRoutePath(suffix) {
-  const matched = router.getRoutes().find(item => item.path.endsWith(suffix))
-  return matched ? matched.path : ''
+.header-title {
+  font-size: 30px;
+  font-weight: 800;
+  letter-spacing: 3px;
+  background: linear-gradient(180deg, #e8f6ff 0%, #8fd6ff 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
 }
 
-function goPage(suffix, query = {}) {
-  const path = resolveRoutePath(suffix)
-  if (!path) {
-    console.warn(`未找到供水入口路由: ${suffix}`)
-    return
-  }
-  router.push({ path, query }).catch(() => {})
+.header-subtitle {
+  margin-top: 4px;
+  color: rgba(196, 234, 255, 0.65);
+  font-size: 11px;
+  letter-spacing: 3px;
+  text-transform: uppercase;
 }
 
-function gisStatusLabel(statusKey) {
-  return gisPointStatusLabels[statusKey] || '正常'
+.header-tags {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
 }
 
-function gisStatusTagType(statusKey) {
-  return gisPointStatusTagTypes[statusKey] || 'success'
+.header-tag {
+  padding: 6px 14px;
+  border: 1px solid rgba(117, 206, 255, 0.35);
+  border-radius: 16px;
+  color: #d8f5ff;
+  background: rgba(13, 77, 128, 0.45);
+  font-size: 12px;
+  letter-spacing: 1px;
+  transition: all 0.3s;
+  cursor: default;
 }
 
-function overviewKindLabel(kind) {
-  return { device: '监测设备', alarm: '报警', facility: '供水设施' }[kind] || '点位'
+.header-tag:hover {
+  border-color: rgba(117, 206, 255, 0.7);
+  background: rgba(13, 77, 128, 0.65);
+  box-shadow: 0 0 16px rgba(56, 216, 255, 0.15);
 }
 
-function renderCharts() {
-  renderTrendChart()
-  warningPie?.dispose()
-  warningPie = renderPieChart(warningPieRef.value, warningRatio.value, '报警数')
-  devicePie?.dispose()
-  devicePie = renderPieChart(devicePieRef.value, deviceRatio.value, '设备数')
+.screen-body {
+  height: calc(100vh - 72px);
+  padding: 12px;
+  display: grid;
+  grid-template-columns: 360px 1fr 360px;
+  gap: 12px;
+  position: relative;
+  z-index: 1;
 }
 
-function renderTrendChart() {
-  if (!trendChartRef.value) return
-  trendChart?.dispose()
-  trendChart = echarts.init(trendChartRef.value)
-  const { dates, levels } = trendData.value
-  const palette = ['#f56c6c', '#e6a23c', '#409eff', '#67c23a']
-  trendChart.setOption({
-    tooltip: { trigger: 'axis' },
-    legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
-    grid: { left: 34, right: 14, top: 18, bottom: 48, containLabel: true },
-    xAxis: {
-      type: 'category',
-      data: dates,
-      axisLine: { lineStyle: { color: '#cbd5e1' } },
-      axisLabel: { color: '#64748b' }
-    },
-    yAxis: {
-      type: 'value',
-      minInterval: 1,
-      splitLine: { lineStyle: { color: '#eef2f7' } },
-      axisLabel: { color: '#64748b' }
-    },
-    color: palette,
-    series: levels.map((level, index) => ({
-      name: level.text || `级${index + 1}`,
-      type: 'bar',
-      stack: 'alarm',
-      barWidth: 26,
-      data: level.data,
-      itemStyle: { borderRadius: index === levels.length - 1 ? [5, 5, 0, 0] : 0 }
-    }))
-  })
+.side-panel,
+.center-panel {
+  min-height: 0;
 }
 
-function renderPieChart(container, data, unit) {
-  if (!container) return null
-  const total = data.reduce((sum, item) => sum + item.count, 0)
-  const chart = echarts.init(container)
-  chart.setOption({
-    tooltip: { trigger: 'item', formatter: `{b}:{c} ${unit}({d}%)` },
-    legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
-    title: {
-      text: String(total),
-      subtext: unit,
-      left: 'center',
-      top: '32%',
-      textStyle: { color: '#1e293b', fontSize: 24, fontWeight: 700 },
-      subtextStyle: { color: '#64748b', fontSize: 12 }
-    },
-    color: ['#2563eb', '#14b8a6', '#f59e0b', '#ef4444', '#8b5cf6', '#64748b'],
-    series: [{
-      type: 'pie',
-      radius: ['48%', '68%'],
-      center: ['50%', '43%'],
-      data: data.map(item => ({ name: item.name, value: item.count })),
-      label: { formatter: '{b}\n{d}%', color: '#475569', lineHeight: 17 },
-      itemStyle: { borderColor: '#fff', borderWidth: 3, borderRadius: 8 },
-      emphasis: { scaleSize: 7 }
-    }]
-  })
-  return chart
+.left-panel,
+.right-panel {
+  display: grid;
+  gap: 12px;
 }
 
-function handleResize() {
-  trendChart?.resize()
-  warningPie?.resize()
-  devicePie?.resize()
+.left-panel {
+  grid-template-rows: 220px 220px 1fr;
 }
 
-onMounted(async () => {
-  await loadAll()
-  window.addEventListener('resize', handleResize)
-})
-
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', handleResize)
-  trendChart?.dispose()
-  warningPie?.dispose()
-  devicePie?.dispose()
-})
-</script>
-
-<style lang="scss">
-@use './waterSupplyPageTheme.scss';
-</style>
+.right-panel {
+  grid-template-rows: 250px 260px 1fr;
+}
 
-<style scoped>
-.home-page {
-  background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
+.panel-card {
+  min-height: 0;
+  padding: 14px;
+  position: relative;
+  background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
+  border: 1px solid rgba(65, 158, 219, 0.22);
+  border-radius: 12px;
+  box-shadow: 0 2px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.025);
+  transition: border-color 0.4s;
 }
 
-.heading-actions {
-  display: flex;
-  align-items: center;
-  gap: 10px;
+.panel-card::after {
+  content: '';
+  position: absolute;
+  top: -1px;
+  left: 20px;
+  right: 20px;
+  height: 1px;
+  border-radius: 1px;
+  background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.25), transparent);
+  pointer-events: none;
 }
 
-.page-title-icon {
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #fff;
-  background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
-  border-radius: 8px;
-  box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
+.panel-card:hover {
+  border-color: rgba(65, 158, 219, 0.38);
 }
 
-.load-error {
+.panel-title {
+  position: relative;
+  padding-left: 16px;
   margin-bottom: 14px;
+  color: #8fe5ff;
+  font-size: 18px;
+  font-weight: 800;
+  letter-spacing: 1px;
 }
 
-.home-section {
-  margin-bottom: 20px;
+.panel-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 3px;
+  width: 4px;
+  height: 20px;
+  border-radius: 2px;
+  background: linear-gradient(180deg, #28d7ff, #1776ff);
+  box-shadow: 0 0 10px rgba(40, 215, 255, 0.5);
 }
 
-.metric-grid {
+.base-grid {
   display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.base-card {
+  background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
+}
+
+.base-grid {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
   gap: 12px;
-  margin-bottom: 14px;
 }
 
-.metric {
+.base-item {
   display: flex;
   align-items: center;
   gap: 14px;
-  min-width: 0;
-  padding: 14px 16px;
-  background: #fff;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04);
+  min-height: 72px;
+  padding: 14px;
+  border-radius: 14px;
+  background: linear-gradient(180deg, rgba(20, 57, 89, 0.45), rgba(18, 44, 72, 0.35));
+  border: 1px solid rgba(97, 190, 255, 0.12);
+  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+  cursor: default;
+  position: relative;
+  overflow: hidden;
 }
 
-.metric-icon {
-  position: static;
-  flex: 0 0 42px;
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #2563eb;
-  background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
-  border-radius: 8px;
-  font-size: 22px;
+.base-item::before {
+  content: '';
+  position: absolute;
+  inset: 0;
+  border-radius: 14px;
+  background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.03), transparent 70%);
+  pointer-events: none;
+  transition: opacity 0.35s;
+  opacity: 0;
 }
 
-.metric-body {
-  min-width: 0;
+.base-item:hover {
+  transform: translateY(-2px);
+  border-color: rgba(97, 190, 255, 0.35);
+  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(97, 190, 255, 0.1) inset;
 }
 
-.metric-body span {
-  display: block;
-  overflow: hidden;
-  margin-bottom: 3px;
-  color: #64748b;
-  font-size: 13px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+.base-item:hover::before {
+  opacity: 1;
 }
 
-.metric-value {
+.base-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 16px;
   display: flex;
-  align-items: baseline;
-  gap: 5px;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 48px;
+  position: relative;
+  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s;
 }
 
-.metric-value strong {
-  color: #1e293b;
-  font-size: 24px;
-  line-height: 1.2;
+.base-item:hover .base-icon {
+  transform: scale(1.08);
+  box-shadow: 0 0 28px rgba(56, 216, 255, 0.3);
 }
 
-.metric-value strong.danger { color: #dc2626; }
-.metric-value strong.warning { color: #d97706; }
+.base-icon-svg {
+  width: 26px;
+  height: 26px;
+  color: #fff;
+  transition: transform 0.35s;
+}
 
-.metric-value small {
-  color: #94a3b8;
+.base-item:hover .base-icon-svg {
+  transform: scale(1.1);
 }
 
-.workbench-grid {
-  display: grid;
-  grid-template-columns: minmax(240px, 1fr) minmax(0, 3fr) minmax(240px, 1fr);
-  gap: 12px;
-  align-items: stretch;
+.base-text {
+  min-width: 0;
 }
 
-.workbench-grid--equal {
-  grid-template-columns: repeat(3, minmax(0, 1fr));
+.base-label {
+  color: #c0dff5;
+  font-size: 12px;
+  letter-spacing: 1px;
+  text-transform: uppercase;
 }
 
-.home-panel {
-  min-width: 0;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
+.base-value {
+  margin-top: 4px;
+  color: #fff;
+  font-size: 32px;
+  font-weight: 800;
+  line-height: 1;
+  font-variant-numeric: tabular-nums;
+  letter-spacing: -0.5px;
 }
 
-.map-panel {
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
+.base-value span {
+  margin-left: 4px;
+  color: #90d9ff;
+  font-size: 12px;
+  font-weight: 500;
+  letter-spacing: 0;
 }
 
-.map-panel :deep(.el-card__body) {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  min-height: 0;
+.chart-box {
+  width: 100%;
+  height: calc(100% - 36px);
 }
 
-.home-panel :deep(.el-card__header) {
-  padding: 12px 16px;
-  border-bottom: 1px solid #e1ebf3;
-  background: #fbfdff;
+.table-card,
+.warning-card,
+.hazard-card {
+  overflow: hidden;
 }
 
-.home-panel :deep(.el-card__body) {
-  padding: 12px 14px;
+.table-wrap {
+  height: calc(100% - 38px);
+  overflow: auto;
 }
 
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  color: #1e293b;
-  font-weight: 600;
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  table-layout: fixed;
 }
 
-.card-title :deep(.el-icon) {
-  color: #2563eb;
-  font-size: 18px;
+.data-table thead {
+  background: rgba(24, 122, 198, 0.55);
 }
 
-.card-title .muted {
-  margin-left: auto;
-  font-size: 12px;
-  font-weight: 400;
+.data-table thead th {
+  font-weight: 700;
+  letter-spacing: 0.5px;
+  color: #b9e4ff;
 }
 
-.panel-link {
-  margin-left: auto;
+.data-table th,
+.data-table td {
+  padding: 10px 6px;
+  color: #f4fbff;
+  text-align: center;
+  font-size: 13px;
+  border-bottom: 1px solid rgba(130, 194, 231, 0.1);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
-.panel-links {
-  display: flex;
-  align-items: center;
-  gap: 2px;
-  margin-left: auto;
+.data-table tbody tr {
+  transition: background 0.25s;
 }
 
-.panel-links .panel-link {
-  margin-left: 0;
+.data-table tbody tr:nth-child(odd) {
+  background: rgba(133, 196, 242, 0.10);
 }
 
-.legend-dot {
-  display: inline-block;
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
+.data-table tbody tr:nth-child(even) {
+  background: rgba(133, 196, 242, 0.04);
 }
 
-.compact-list {
-  display: flex;
-  flex-direction: column;
-  gap: 7px;
-  min-height: 220px;
-  max-height: 360px;
-  overflow-y: auto;
-  padding-right: 2px;
+.data-table tbody tr:hover {
+  background: rgba(56, 216, 255, 0.12);
 }
 
-.compact-item {
-  display: grid;
-  grid-template-columns: 10px minmax(0, 1fr) auto;
-  gap: 9px;
-  align-items: center;
-  width: 100%;
-  padding: 9px 10px;
-  text-align: left;
-  background: #fff;
-  border: 1px solid #eef2f7;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.15s ease;
+.compact-table th,
+.compact-table td {
+  padding: 9px 4px;
 }
 
-.compact-item:hover {
-  background: #f6fbff;
-  border-color: #bfdbfe;
+.status-cell.ok {
+  color: #6effc4;
+  position: relative;
 }
 
-.compact-item.active {
-  background: #eef6ff;
-  border-color: #93c5fd;
+.status-cell.warn {
+  color: #ffe16a;
 }
 
-.status-dot {
-  width: 9px;
-  height: 9px;
+.status-cell.off {
+  color: #ff8e8e;
+}
+
+.status-cell::before {
+  content: '';
+  display: inline-block;
+  width: 6px;
+  height: 6px;
   border-radius: 50%;
-  background: #94a3b8;
+  margin-right: 6px;
+  vertical-align: middle;
 }
 
-.dot-alarm { background: #dc2626; }
-.dot-offline { background: #94a3b8; }
-.dot-normal { background: #16a34a; }
-.dot-warning { background: #d97706; }
-.dot-danger { background: #dc2626; }
-.dot-primary { background: #2563eb; }
-.dot-success { background: #16a34a; }
-.dot-info { background: #94a3b8; }
-.dot-workorder { background: #2563eb; }
+.status-cell.ok::before {
+  background: #6effc4;
+  box-shadow: 0 0 6px rgba(110, 255, 196, 0.6);
+}
 
-.compact-main {
-  display: block;
-  min-width: 0;
+.status-cell.warn::before {
+  background: #ffe16a;
+  box-shadow: 0 0 6px rgba(255, 225, 106, 0.6);
 }
 
-.compact-main b,
-.compact-main small {
-  display: block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+.status-cell.off::before {
+  background: #ff8e8e;
+  box-shadow: 0 0 6px rgba(255, 142, 142, 0.6);
 }
 
-.compact-main b {
-  color: #1e293b;
-  font-size: 13px;
-  font-weight: 600;
+.center-panel {
+  position: relative;
 }
 
-.compact-main small {
-  margin-top: 2px;
-  color: #94a3b8;
-  font-size: 12px;
+.map-stage {
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 12px;
+  border: 1px solid rgba(56, 159, 223, 0.2);
+  background: rgba(8, 20, 37, 0.64);
+  box-shadow: 0 0 40px rgba(0, 0, 0, 0.3), inset 0 0 60px rgba(8, 30, 60, 0.3);
 }
 
-.compact-meta {
+.map-stage-top {
+  position: absolute;
+  top: 14px;
+  left: 14px;
+  right: 14px;
+  z-index: 3;
   display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  gap: 3px;
-  min-width: 0;
+  justify-content: space-between;
+  gap: 12px;
+  pointer-events: none;
 }
 
-.compact-meta small {
-  color: #94a3b8;
-  font-size: 11px;
-  white-space: nowrap;
+.summary-strip,
+.map-kpis {
+  display: flex;
+  gap: 10px;
 }
 
-.map-stage {
-  position: relative;
-  flex: 1;
-  min-height: 0;
-  height: auto;
-  overflow: hidden;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  background: #eef6ff;
+.summary-item,
+.map-kpi {
+  min-width: 110px;
+  padding: 10px 14px;
+  border-radius: 10px;
+  background: rgba(8, 31, 54, 0.82);
+  border: 1px solid rgba(79, 177, 238, 0.18);
+  backdrop-filter: blur(8px);
+  transition: all 0.3s;
+}
+
+.summary-label,
+.map-kpi span {
+  display: block;
+  color: #91d7ff;
+  font-size: 12px;
+  letter-spacing: 0.5px;
 }
 
-.map-stage :deep(.gis-map-canvas) {
+.summary-value,
+.map-kpi strong {
+  display: block;
+  margin-top: 4px;
+  color: #fff;
+  font-size: 22px;
+  font-weight: 800;
+  letter-spacing: 1px;
+}
+
+.map-view {
   width: 100%;
   height: 100%;
-  min-height: 0;
 }
 
-.map-inline-alert {
+.map-error {
   position: absolute;
-  left: 16px;
-  bottom: 16px;
-  z-index: 3;
-  width: 340px;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffd3d3;
+  background: rgba(5, 10, 20, 0.72);
+  z-index: 4;
 }
 
-.map-inline-empty {
+.map-panel {
   position: absolute;
-  left: 50%;
-  top: 50%;
-  z-index: 2;
-  transform: translate(-50%, -50%);
+  z-index: 3;
+  width: 190px;
+  padding: 14px;
+  border-radius: 12px;
+  background: rgba(8, 31, 54, 0.82);
+  border: 1px solid rgba(79, 177, 238, 0.18);
+  backdrop-filter: blur(10px);
 }
 
-.map-legend {
-  display: flex;
-  align-items: center;
-  gap: 7px;
-  margin-left: auto;
-  color: #64748b;
-  font-size: 12px;
+.info-panel {
+  left: 18px;
+  bottom: 20px;
 }
 
-.overview-detail {
-  min-height: 240px;
+.point-panel {
+  right: 18px;
+  bottom: 20px;
 }
 
-.detail-hero h3,
-.detail-section h4 {
-  margin: 0 0 8px;
-  color: #1c4b78;
+.mini-title {
+  margin-bottom: 10px;
+  color: #8fe5ff;
+  font-size: 14px;
+  font-weight: 700;
+  letter-spacing: 0.5px;
 }
 
-.detail-hero p {
-  margin: 0 0 10px;
-  color: #678197;
+.mini-row {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 6px;
+  padding: 4px 0;
+  color: #d6efff;
   font-size: 13px;
+  border-bottom: 1px solid rgba(130, 194, 231, 0.06);
 }
 
-.status-tags {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
+.mini-row:last-child {
+  border-bottom: none;
 }
 
-.detail-section {
-  margin-top: 18px;
-  padding-top: 12px;
-  border-top: 1px solid #e5eef7;
+.warning-scroll {
+  height: calc(100% - 38px);
+  overflow-y: auto;
+  padding-right: 4px;
 }
 
-.detail-section dl {
-  display: grid;
-  grid-template-columns: 92px minmax(0, 1fr);
-  gap: 7px 10px;
-  margin: 0;
-  font-size: 13px;
+.warning-scroll::-webkit-scrollbar {
+  width: 5px;
 }
 
-.detail-section dt {
-  color: #71889c;
+.warning-scroll::-webkit-scrollbar-thumb {
+  border-radius: 999px;
+  background: rgba(96, 180, 235, 0.3);
 }
 
-.detail-section dd {
-  margin: 0;
-  color: #2b4c67;
-  overflow-wrap: anywhere;
+.warning-scroll::-webkit-scrollbar-track {
+  background: transparent;
 }
 
-.order-brief {
+.warning-list {
   display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 6px;
-  margin-top: 10px;
-  padding-top: 10px;
-  border-top: 1px solid #eef2f7;
+  gap: 10px;
 }
 
-.order-brief span {
-  color: #64748b;
-  font-size: 12px;
+.warning-item {
+  display: flex;
+  gap: 12px;
+  align-items: stretch;
+  padding: 14px;
+  border-radius: 14px;
+  border: 1px solid rgba(255, 108, 108, 0.1);
+  background: linear-gradient(180deg, rgba(72, 24, 38, 0.42), rgba(49, 19, 31, 0.30));
+  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+  overflow: hidden;
 }
 
-.order-brief b {
-  margin-left: 3px;
-  color: #17324d;
-  font-size: 14px;
+.warning-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 12px;
+  bottom: 12px;
+  width: 3px;
+  border-radius: 0 3px 3px 0;
+  transition: all 0.35s;
+}
+
+.warning-item.danger::before {
+  background: linear-gradient(180deg, #ff5858, #ff3030);
+  box-shadow: 0 0 12px rgba(255, 58, 58, 0.5);
+  animation: dangerPulse 2s ease-in-out infinite;
 }
 
-.device-summary {
+.warning-item.notice::before {
+  background: linear-gradient(180deg, #ffc635, #ff9f1a);
+  box-shadow: 0 0 10px rgba(255, 198, 53, 0.35);
+}
+
+@keyframes dangerPulse {
+  0%, 100% { opacity: 0.6; }
+  50% { opacity: 1; }
+}
+
+.warning-item:hover {
+  transform: translateX(3px);
+}
+
+.warning-item.danger:hover {
+  border-color: rgba(255, 108, 108, 0.25);
+  box-shadow: 0 4px 20px rgba(255, 58, 58, 0.1);
+}
+
+.warning-item.notice:hover {
+  border-color: rgba(255, 201, 92, 0.2);
+  box-shadow: 0 4px 20px rgba(255, 198, 53, 0.08);
+}
+
+.warning-item.notice {
+  border-color: rgba(255, 201, 92, 0.1);
+  background: linear-gradient(180deg, rgba(78, 63, 23, 0.28), rgba(48, 43, 20, 0.18));
+}
+
+.warning-side {
+  width: 52px;
+  flex: 0 0 52px;
   display: flex;
   flex-direction: column;
-  gap: 12px;
+  gap: 8px;
+  align-items: center;
 }
 
-.detail-panel :deep(.el-card__body) {
+.warning-badge {
+  width: 34px;
+  height: 34px;
+  border-radius: 10px;
   display: flex;
-  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  font-weight: 800;
+  color: #fff;
+  transition: transform 0.3s;
+}
+
+.warning-item:hover .warning-badge {
+  transform: scale(1.1);
+}
+
+.warning-tag {
+  width: 100%;
+  padding: 3px 0;
+  border-radius: 999px;
+  text-align: center;
+  font-size: 10px;
+  letter-spacing: 0.5px;
+  color: #dcefff;
+  background: rgba(255, 255, 255, 0.06);
+}
+
+.warning-item.danger .warning-badge {
+  background: rgba(255, 58, 58, 0.28);
+  color: #ff8787;
+}
+
+.warning-item.danger .warning-title {
+  color: #ffd8d8;
 }
 
-.detail-panel .device-summary {
+.warning-item.notice .warning-badge {
+  background: rgba(255, 198, 53, 0.2);
+  color: #ffd45d;
+}
+
+.warning-item.notice .warning-title {
+  color: #fff2c4;
+}
+
+.warning-main {
+  min-width: 0;
   flex: 1;
 }
 
-.summary-top {
+.warning-title-row {
   display: flex;
   align-items: flex-start;
   justify-content: space-between;
-  gap: 10px;
-  flex-wrap: wrap;
+  gap: 8px;
 }
 
-.summary-name {
-  color: #17324d;
-  font-size: 16px;
+.warning-title {
   font-weight: 700;
+  font-size: 16px;
+  line-height: 1.2;
 }
 
-.summary-tags {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: flex-end;
-  gap: 6px;
+.warning-time,
+.warning-code,
+.warning-desc {
+  color: #dcefff;
+  font-size: 12px;
 }
 
-.detail-panel :deep(.el-descriptions__label) {
-  width: 86px;
-  color: #64748b;
-  background: #f8fafc;
+.warning-time {
+  color: #b6dfff;
   white-space: nowrap;
 }
 
-.quick-grid {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 10px;
+.warning-code {
+  margin: 6px 0 4px;
+  color: #9ad8ff;
 }
 
-.quick-entry {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  min-width: 0;
-  padding: 14px 12px;
-  color: #1e293b;
-  background: #fff;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.15s ease;
+.warning-desc {
+  line-height: 1.5;
 }
 
-.quick-entry:hover {
-  background: #f6fbff;
-  border-color: #bfdbfe;
+.hazard-summary {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 10px;
 }
 
-.quick-entry :deep(.el-icon) {
-  flex: 0 0 20px;
-  color: #2563eb;
-  font-size: 20px;
+.hazard-summary.three-col {
+  justify-content: space-between;
 }
 
-.quick-entry span {
-  overflow: hidden;
+.hazard-summary-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
   font-size: 13px;
-  font-weight: 600;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  padding: 8px 12px;
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.03);
+  border: 1px solid rgba(255, 255, 255, 0.04);
+  transition: all 0.3s;
 }
 
-.chart-grid {
-  display: grid;
-  grid-template-columns: 1.5fr 1fr 1fr;
-  gap: 12px;
+.hazard-summary-item:hover {
+  background: rgba(255, 255, 255, 0.06);
+  border-color: rgba(255, 255, 255, 0.08);
 }
 
-.chart-card {
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
+.hazard-dot {
+  width: 8px;
+  height: 24px;
+  display: inline-block;
+  border-radius: 4px;
+  box-shadow: 0 0 10px currentColor;
 }
 
-.chart-card .chart {
-  height: 260px;
+.hazard-chart {
+  height: 160px;
 }
 
-.chart-card :deep(.el-card__header) {
-  padding: 12px 16px;
-  border-bottom: 1px solid #e1ebf3;
-  background: #fbfdff;
+.equipment-wrap {
+  height: calc(100% - 38px);
 }
 
-.chart-card :deep(.el-card__body) {
-  padding: 10px 12px;
+:deep(.BMap_cpyCtrl),
+:deep(.anchorBL) {
+  display: none !important;
 }
 
-@media (max-width: 1200px) {
-  .metric-grid {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-
-  .workbench-grid {
-    grid-template-columns: 1fr;
-  }
-
-  .chart-grid {
-    grid-template-columns: 1fr 1fr;
-  }
-
-  .trend-card {
-    grid-column: 1 / -1;
-  }
+.deerflow-mark {
+  position: fixed;
+  bottom: 14px;
+  right: 18px;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  text-decoration: none;
+  opacity: 0.28;
+  transition: opacity 0.4s;
 }
 
-@media (max-width: 768px) {
-  .page-heading,
-  .heading-actions {
-    align-items: stretch;
-    flex-direction: column;
-  }
+.deerflow-mark:hover {
+  opacity: 0.6;
+}
 
-  .metric-grid,
-  .chart-grid {
-    grid-template-columns: 1fr;
-  }
+.deerflow-diamond {
+  font-size: 10px;
+  color: #8fe5ff;
+}
 
-  .trend-card {
-    grid-column: auto;
-  }
+.deerflow-text {
+  font-size: 11px;
+  letter-spacing: 1px;
+  color: #8fe5ff;
+  font-weight: 500;
+}
 
-  .map-legend {
-    display: none;
+@media (max-width: 1500px) {
+  .screen-body {
+    grid-template-columns: 320px 1fr 320px;
   }
 
-  .panel-links {
-    display: none;
+  .header-title {
+    font-size: 24px;
   }
 
-  .quick-grid {
-    grid-template-columns: 1fr;
+  .summary-item,
+  .map-kpi {
+    min-width: 96px;
   }
 }
 </style>

+ 0 - 520
src/views/subSystem/waterSupply/components/WaterThresholdPage.vue

@@ -1,520 +0,0 @@
-<template>
-  <div class="water-page threshold-page">
-    <div class="page-heading">
-      <div class="page-title">
-        <span class="page-title-icon"><el-icon><Warning /></el-icon></span>
-        <div>
-          <h2>阈值管理</h2>
-          <span class="muted">供水监测设备阈值、分时规则与生效范围统一配置</span>
-        </div>
-      </div>
-      <div class="heading-actions">
-        <el-button type="primary" :icon="Plus" @click="openCreate">新增阈值</el-button>
-        <el-button :icon="Refresh" :loading="loading" @click="loadRules">刷新</el-button>
-      </div>
-    </div>
-
-    <div class="metric-grid">
-      <div class="metric">
-        <div class="metric-icon"><el-icon><Connection /></el-icon></div>
-        <div class="metric-body">
-          <span>供水监测设备</span>
-          <div class="metric-value"><strong>{{ devices.length }}</strong><small>台</small></div>
-        </div>
-      </div>
-      <div class="metric">
-        <div class="metric-icon"><el-icon><Bell /></el-icon></div>
-        <div class="metric-body">
-          <span>阈值规则</span>
-          <div class="metric-value"><strong>{{ page.total }}</strong><small>条</small></div>
-        </div>
-      </div>
-      <div class="metric">
-        <div class="metric-icon"><el-icon><CircleCheck /></el-icon></div>
-        <div class="metric-body">
-          <span>当前页全天规则</span>
-          <div class="metric-value"><strong>{{ allDayCount }}</strong><small>条</small></div>
-        </div>
-      </div>
-      <div class="metric">
-        <div class="metric-icon"><el-icon><Clock /></el-icon></div>
-        <div class="metric-body">
-          <span>当前页分时规则</span>
-          <div class="metric-value"><strong>{{ timedCount }}</strong><small>条</small></div>
-        </div>
-      </div>
-    </div>
-
-    <el-card shadow="never" class="water-panel filter-card">
-      <template #header>
-        <div class="card-title">
-          <el-icon><Search /></el-icon>
-          <span>阈值筛选</span>
-        </div>
-      </template>
-      <el-form :inline="true" :model="query" class="filter-form">
-        <el-form-item label="设备编码">
-          <el-input v-model="query.deviceCode" clearable placeholder="请输入供水设备编码" />
-        </el-form-item>
-        <el-form-item label="预警分类">
-          <el-select v-model="query.warningType" clearable filterable placeholder="展示全部" class="warning-select">
-            <el-option v-for="item in warningTypes" :key="item" :label="item" :value="item" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
-          <el-button @click="resetQuery">展示全部</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-alert
-      v-if="loadError"
-      class="load-error"
-      type="error"
-      show-icon
-      :closable="false"
-      title="阈值加载失败"
-      :description="loadError"
-    >
-      <el-button size="small" type="primary" :loading="loading" @click="loadRules">重试</el-button>
-    </el-alert>
-
-    <el-card shadow="never" class="water-panel table-card">
-      <template #header>
-        <div class="card-title">
-          <el-icon><DataLine /></el-icon>
-          <span>阈值配置清单</span>
-        </div>
-      </template>
-      <el-table :data="page.records" stripe border v-loading="loading">
-        <el-table-column type="index" label="#" width="55" />
-        <el-table-column prop="deviceCode" label="供水设备编码" min-width="190" show-overflow-tooltip />
-        <el-table-column prop="warningType" label="预警分类" min-width="120">
-          <template #default="{ row }">
-            <el-tag effect="light">{{ row.warningType }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="warningCode" label="预警编码" min-width="150" show-overflow-tooltip />
-        <el-table-column label="阈值范围" min-width="160">
-          <template #default="{ row }">
-            <span class="threshold-range">{{ thresholdText(row) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="生效时段" min-width="140">
-          <template #default="{ row }">
-            <el-tag :type="isTimed(row) ? 'warning' : 'success'" effect="plain">
-              {{ periodText(row) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="updateTime" label="更新时间" min-width="170" show-overflow-tooltip />
-        <el-table-column label="操作" width="150" fixed="right">
-          <template #default="{ row }">
-            <el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
-            <el-button
-              link
-              type="danger"
-              :icon="Delete"
-              :loading="deletingId === row.id"
-              :disabled="deletingId !== null && deletingId !== row.id"
-              @click="handleDelete(row)"
-            >删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        v-model:current-page="query.pageNum"
-        v-model:page-size="query.pageSize"
-        :total="page.total"
-        layout="total, sizes, prev, pager, next"
-        :page-sizes="[10, 20, 50]"
-        @change="loadRules"
-      />
-    </el-card>
-
-    <el-drawer
-      v-model="drawerVisible"
-      size="520px"
-      direction="rtl"
-      class="detail-drawer threshold-drawer"
-      :title="form.id ? '编辑阈值' : '新增阈值'"
-    >
-      <el-alert
-        v-if="deviceLoadError"
-        class="load-error"
-        type="error"
-        show-icon
-        :closable="false"
-        title="供水设备加载失败"
-        :description="deviceLoadError"
-      >
-        <el-button size="small" type="primary" @click="loadDevices">重试</el-button>
-      </el-alert>
-      <el-form :model="form" label-width="100px">
-        <el-form-item label="监测设备">
-          <el-select
-            v-if="!form.id"
-            v-model="form.deviceCodes"
-            multiple
-            filterable
-            placeholder="请选择供水监测设备"
-          >
-            <el-option
-              v-for="device in devices"
-              :key="device.equipmentId || device.id"
-              :label="device.equipmentName ? `${device.equipmentName}(${device.equipmentCode})` : device.equipmentCode"
-              :value="device.equipmentCode"
-            />
-          </el-select>
-          <el-select v-else v-model="form.deviceCode" filterable placeholder="请选择供水监测设备">
-            <el-option
-              v-for="device in devices"
-              :key="device.equipmentId || device.id"
-              :label="device.equipmentName ? `${device.equipmentName}(${device.equipmentCode})` : device.equipmentCode"
-              :value="device.equipmentCode"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警分类">
-          <el-select v-model="form.warningType" filterable allow-create placeholder="请选择预警分类">
-            <el-option v-for="item in warningTypes" :key="item" :label="item" :value="item" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预警编码">
-          <el-input v-model="form.warningCode" placeholder="例如 WARN-T5-FLOW" />
-        </el-form-item>
-        <el-form-item label="阈值下限">
-          <el-input-number v-model="form.minValue" :precision="3" placeholder="可留空" />
-        </el-form-item>
-        <el-form-item label="阈值上限">
-          <el-input-number v-model="form.maxValue" :precision="3" placeholder="可留空" />
-        </el-form-item>
-        <el-form-item label="分时生效">
-          <div class="period-range">
-            <el-time-picker v-model="form.periodStart" format="HH:mm" value-format="HH:mm" placeholder="全天开始" />
-            <span>至</span>
-            <el-time-picker v-model="form.periodEnd" format="HH:mm" value-format="HH:mm" placeholder="全天结束" />
-          </div>
-        </el-form-item>
-        <el-form-item label="备注">
-          <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="200" show-word-limit />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="drawerVisible = false">取消</el-button>
-        <el-button type="primary" :loading="saving" @click="saveThreshold">保存</el-button>
-      </template>
-    </el-drawer>
-  </div>
-</template>
-
-<script setup>
-import { computed, onMounted, ref } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { Bell, CircleCheck, Clock, Connection, DataLine, Delete, Edit, Plus, Refresh, Search, Warning } from '@element-plus/icons-vue'
-import { deleteThresholds, listThresholdDevices, listThresholds, saveThresholds, updateThreshold } from '@/api/pipeNetwork/waterSupply'
-import { normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
-import { buildThresholdQuery, buildThresholdSavePayload, buildThresholdUpdatePayload } from '@/utils/waterSupplyUiModel'
-import { getRequestErrorMessage } from '@/utils/waterFacilityQuery'
-
-const warningTypes = ['压力预警', '流量预警', '漏失预警', '设备离线', '水质异常']
-const query = ref({ pageNum: 1, pageSize: 10, deviceCode: '', warningType: undefined })
-const page = ref({ records: [], total: 0 })
-const devices = ref([])
-const loading = ref(false)
-const saving = ref(false)
-const deletingId = ref(null)
-const loadError = ref('')
-const deviceLoadError = ref('')
-const drawerVisible = ref(false)
-const form = ref({})
-
-const allDayCount = computed(() => page.value.records.filter(item => !isTimed(item)).length)
-const timedCount = computed(() => page.value.records.filter(isTimed).length)
-
-const thresholdText = row => row.minValue != null || row.maxValue != null
-  ? `${row.minValue ?? '-'} ~ ${row.maxValue ?? '-'}`
-  : '-'
-const isTimed = row => Boolean(row.periodStart || row.periodEnd)
-const periodText = row => isTimed(row)
-  ? `${row.periodStart || '--:--'} - ${row.periodEnd || '--:--'}`
-  : '全天'
-
-async function loadRules() {
-  loading.value = true
-  try {
-    page.value = normalizePage(await listThresholds(buildThresholdQuery(query.value)))
-    loadError.value = ''
-  } catch (error) {
-    loadError.value = getRequestErrorMessage(error, '请检查服务状态')
-  } finally {
-    loading.value = false
-  }
-}
-
-async function loadDevices() {
-  try {
-    const response = unwrapResponse(await listThresholdDevices())
-    devices.value = Array.isArray(response) ? response : []
-    deviceLoadError.value = ''
-  } catch (error) {
-    deviceLoadError.value = getRequestErrorMessage(error, '请检查服务状态')
-  }
-}
-
-function handleSearch() {
-  query.value.pageNum = 1
-  loadRules()
-}
-
-function resetQuery() {
-  query.value = { pageNum: 1, pageSize: 10, deviceCode: '', warningType: undefined }
-  loadRules()
-}
-
-function openCreate() {
-  form.value = {
-    deviceCodes: [],
-    warningType: '压力预警',
-    warningCode: '',
-    minValue: undefined,
-    maxValue: undefined,
-    periodStart: '',
-    periodEnd: '',
-    remark: ''
-  }
-  drawerVisible.value = true
-}
-
-function openEdit(row) {
-  form.value = { ...row }
-  drawerVisible.value = true
-}
-
-function validateForm() {
-  const current = form.value
-  const selectedDevices = current.id ? [current.deviceCode] : current.deviceCodes
-  if (!selectedDevices?.filter(Boolean).length) return '请选择至少一个供水监测设备'
-  if (!current.warningType?.trim()) return '请选择预警分类'
-  if (!current.warningCode?.trim()) return '请填写预警编码'
-  if (current.minValue == null && current.maxValue == null) return '阈值下限和上限至少填写一项'
-  if (current.minValue != null && current.maxValue != null && Number(current.minValue) > Number(current.maxValue)) return '阈值下限不能大于上限'
-  if (Boolean(current.periodStart) !== Boolean(current.periodEnd)) return '分时开始和结束时间必须同时填写'
-  return ''
-}
-
-async function saveThreshold() {
-  const message = validateForm()
-  if (message) {
-    ElMessage.warning(message)
-    return
-  }
-  saving.value = true
-  try {
-    if (form.value.id) {
-      await updateThreshold(buildThresholdUpdatePayload(form.value))
-    } else {
-      await saveThresholds(buildThresholdSavePayload(form.value))
-    }
-    ElMessage.success('阈值已保存')
-    drawerVisible.value = false
-    await loadRules()
-  } catch (error) {
-    ElMessage.error(getRequestErrorMessage(error, '阈值保存失败,请检查权限或服务状态'))
-  } finally {
-    saving.value = false
-  }
-}
-
-async function handleDelete(row) {
-  try {
-    await ElMessageBox.confirm(`确认删除设备 ${row.deviceCode} 的阈值规则吗?`, '删除确认', { type: 'warning' })
-    deletingId.value = row.id
-    await deleteThresholds([row.id])
-    ElMessage.success('阈值已删除')
-    await loadRules()
-  } catch (error) {
-    if (error !== 'cancel') {
-      ElMessage.error(getRequestErrorMessage(error, '阈值删除失败,请检查权限或服务状态'))
-    }
-  } finally {
-    deletingId.value = null
-  }
-}
-
-onMounted(() => {
-  loadRules()
-  loadDevices()
-})
-</script>
-
-<style lang="scss">
-@use './waterSupplyPageTheme.scss';
-</style>
-
-<style scoped>
-.threshold-page {
-  background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
-}
-
-.heading-actions {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.page-title-icon {
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #fff;
-  background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
-  border-radius: 8px;
-  box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
-}
-
-.metric-grid {
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-}
-
-.metric {
-  display: flex;
-  align-items: center;
-  gap: 14px;
-  min-height: 78px;
-  background: #fff;
-  border: 1px solid #e2e8f0;
-  border-radius: 8px;
-  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04);
-}
-
-.metric-icon {
-  position: static;
-  flex: 0 0 42px;
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #2563eb;
-  background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
-  border-radius: 8px;
-}
-
-.metric-icon :deep(.el-icon) {
-  font-size: 22px;
-}
-
-.metric-body span {
-  display: block;
-  margin-bottom: 3px;
-  overflow: hidden;
-  color: #64748b;
-  font-size: 13px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.metric-value {
-  display: flex;
-  align-items: baseline;
-  gap: 5px;
-}
-
-.metric-value strong {
-  color: #1e293b;
-  font-size: 24px;
-  line-height: 1.2;
-}
-
-.filter-card,
-.table-card {
-  margin-bottom: 16px;
-  border: 1px solid #e2e8f0;
-  border-radius: 8px;
-  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04);
-}
-
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  color: #1e293b;
-  font-weight: 600;
-}
-
-.card-title :deep(.el-icon) {
-  color: #2563eb;
-  font-size: 18px;
-}
-
-.filter-form :deep(.el-form-item) {
-  margin-bottom: 0;
-}
-
-.warning-select {
-  width: 220px;
-}
-
-.load-error {
-  margin-bottom: 16px;
-}
-
-.threshold-range {
-  color: #17324d;
-  font-weight: 600;
-}
-
-.table-card :deep(.el-card__body) {
-  padding-top: 0;
-}
-
-.table-card :deep(.el-table__header th) {
-  color: #475569;
-  background: #f8fafc;
-}
-
-.table-card :deep(.el-pagination) {
-  justify-content: flex-end;
-  margin-top: 16px;
-}
-
-.threshold-drawer :deep(.el-select),
-.threshold-drawer :deep(.el-input-number) {
-  width: 100%;
-}
-
-.period-range {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  width: 100%;
-}
-
-.period-range :deep(.el-date-editor) {
-  flex: 1;
-}
-
-@media (max-width: 1000px) {
-  .metric-grid {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-}
-
-@media (max-width: 650px) {
-  .page-heading,
-  .heading-actions {
-    align-items: stretch;
-    flex-direction: column;
-  }
-
-  .metric-grid {
-    grid-template-columns: 1fr;
-  }
-
-  .threshold-drawer {
-    width: 100% !important;
-  }
-}
-</style>

+ 0 - 11
src/views/subSystem/waterSupply/device/abnormal.vue

@@ -1,11 +0,0 @@
-<script setup>
-import Page from '../components/WaterDevicePage.vue'
-</script>
-
-<template>
-  <Page
-    title="设备异常管理"
-    view="abnormal"
-    subtitle="异常态势、处置跟踪与关联工单管理"
-  />
-</template>

+ 0 - 1
src/views/subSystem/waterSupply/device/ledger.vue

@@ -1 +0,0 @@
-<script setup>import Page from '../components/WaterDevicePage.vue'</script><template><Page title="设备台账管理" view="ledger" /></template>

+ 0 - 1
src/views/subSystem/waterSupply/device/maintain.vue

@@ -1 +0,0 @@
-<script setup>import Page from '../components/WaterDevicePage.vue'</script><template><Page title="设备维修派单" view="dispatch" subtitle="待派单设备队列与责任人派发" /></template>

+ 0 - 1
src/views/subSystem/waterSupply/device/record.vue

@@ -1 +0,0 @@
-<script setup>import Page from '../components/WaterDevicePage.vue'</script><template><Page title="设备维修记录" view="maintain" /></template>

+ 0 - 1
src/views/subSystem/waterSupply/monitorAlarm/audit.vue

@@ -1 +0,0 @@
-<script setup>import Page from '../wjcbj/bjsh.vue'</script><template><Page /></template>

+ 0 - 1
src/views/subSystem/waterSupply/monitorAlarm/threshold.vue

@@ -1 +0,0 @@
-<script setup>import Page from '../wjcbj/yzgl.vue'</script><template><Page /></template>

+ 0 - 1
src/views/subSystem/waterSupply/wjcbj/bjsh.vue

@@ -1 +0,0 @@
-<script setup>import WaterAlarmPage from '../components/WaterAlarmPage.vue'</script><template><WaterAlarmPage title="报警审核" subtitle="审核报警真实性并记录审核意见" mode="audit" /></template>

+ 0 - 1
src/views/subSystem/waterSupply/wjcbj/yzgl.vue

@@ -1 +0,0 @@
-<script setup>import WaterThresholdPage from '../components/WaterThresholdPage.vue'</script><template><WaterThresholdPage /></template>

+ 58 - 42
tests/waterAlarmAuditWorkbench.test.mjs

@@ -1,57 +1,73 @@
 import assert from 'node:assert/strict'
 import { readFile } from 'node:fs/promises'
 import test from 'node:test'
+import { ensureUniqueSubsystemRouteNames } from '../src/router/routeNameUtils.js'
 
-const pagePath = 'src/views/subSystem/waterSupply/components/WaterAlarmPage.vue'
-const apiPath = 'src/api/pipeNetwork/waterSupply.js'
+const page = () => readFile('src/views/subSystem/alarm/index.vue', 'utf8')
 
-test('audit workbench exposes exactly two actions, not an action x result matrix', async () => {
-  const source = await readFile(pagePath, 'utf8')
-
-  assert.match(source, /<el-radio value="transfer">转工单<\/el-radio>/u)
-  assert.match(source, /<el-radio value="release">误报解除<\/el-radio>/u)
-  assert.doesNotMatch(source, /v-model="auditForm\.auditAction"/u, '不应再暴露 auditAction 原始码')
-  assert.doesNotMatch(source, /v-model="auditForm\.auditResult"/u, '不应再暴露 auditResult 原始码')
-  assert.doesNotMatch(source, /label="审核结果"/u, '四个组合已收敛为两个动作')
+test('shared audit page consumes enum options and requires an opinion for either decision', async () => {
+  const source = await page()
+  assert.ok(source.includes("item.key !== 'UNREVIEWED'"))
+  assert.ok(source.includes('v-for="item in reviewChoices"'))
+  assert.ok(source.includes('请填写审核意见'))
+  assert.ok(source.includes('auditStatus: undefined'))
+  assert.ok(source.includes('await formRef.value.validate()'))
+  assert.ok(!source.includes('auditAction'))
 })
 
-test('audit submission maps to backend action codes and requires an opinion on release', async () => {
-  const source = await readFile(pagePath, 'utf8')
-  const submit = source.slice(source.indexOf('async function submitAudit'),
-      source.indexOf('async function submitAudit') + 1000)
+test('queue and statistics use shared REST endpoints with subsystem scope and server paging', async () => {
+  const source = await page()
+  const api = await readFile('src/api/alarmReview.js', 'utf8')
+  for (const path of ['/api/alarm/reviews', '/api/alarm/reviews/statistics']) assert.ok(api.includes(path))
+  assert.ok(source.includes('route.query.rootTypeId'))
+  assert.ok(source.includes('route.query.warning_code'))
+  assert.ok(source.includes('pageNum: pageNum.value, pageSize: pageSize.value'))
+  assert.ok(source.includes('row.auditStatus === unreviewed.value'))
+  assert.ok(!source.includes('仅展示未处理报警'))
+  assert.ok(!source.includes('list-note'))
+})
 
-  assert.match(submit, /auditAction: decision === 'release' \? 2 : 1/u)
-  assert.match(submit, /auditResult: decision === 'release' \? 2 : 1/u)
-  assert.match(submit, /decision === 'release' && !String\(auditOpinion \|\| ''\)\.trim\(\)/u,
-      '误报解除必须留下审核意见')
+test('list and detail omit the entire numeric snapshot for alarms without an actual value', async () => {
+  const source = await page()
+  const summary = await readFile('src/views/subSystem/alarm/AlarmSummary.vue', 'utf8')
+  assert.ok(source.includes('<div v-if="hasActualValue(row)" class="snapshot">'))
+  assert.ok(summary.includes('<p v-if="hasActualValue(alarm)">实际值:'))
 })
 
-test('audit workbench shows work order status counts fed by the backend aggregate', async () => {
-  const source = await readFile(pagePath, 'utf8')
-  const api = await readFile(apiPath, 'utf8')
-
-  assert.match(api, /getAuditOrderStats[^\n]*\/waterSupply\/alarm\/audit\/order-stats/u)
-  const cards = source.slice(source.indexOf('const auditOrderStatCards'),
-      source.indexOf('const pageIcon'))
-  for (const label of ['全部工单', '待处理', '处理中', '待验收', '已完成']) {
-    assert.match(cards, new RegExp(`label: '${label}'`, 'u'), `计数条缺少:${label}`)
-  }
-  assert.match(source, /v-for="card in auditOrderStatCards"/u)
-  assert.match(source, /getAuditOrderStats\(\)/u, '审核模式应加载工单状态计数')
+test('search card uses symmetric vertical padding without trailing form margins', async () => {
+  const source = await page()
+  assert.ok(source.includes('.search-card :deep(.el-card__body){padding:16px 20px}'))
+  assert.ok(source.includes('.search-form :deep(.el-form-item){margin:0}'))
+  assert.ok(source.includes('gap:12px 24px;margin:0'))
 })
 
-test('audit queue reads derived display status instead of raw alarm record status', async () => {
-  const source = await readFile(pagePath, 'utf8')
+test('timeline renders thumbnails with teleported full-size image preview', async () => {
+  const source = await page()
+  const attachments = await readFile('src/views/subSystem/alarm/AlarmAttachments.vue', 'utf8')
+  assert.ok(source.includes('<AlarmAttachments v-if="item.attachUrl"'))
+  assert.ok(attachments.includes(':preview-src-list="previewUrls"'))
+  assert.ok(attachments.includes(':initial-index="index"'))
+  assert.ok(attachments.includes('preview-teleported'))
+  assert.ok(attachments.includes('images.value.map(file => file.url)'))
+  assert.ok(!attachments.includes('<el-link'))
+  assert.ok(attachments.includes('width: 96px; height: 72px;'))
+  assert.ok(source.includes('accept=".jpg,.jpeg,.png"'))
+  assert.ok(!source.includes('PDF'))
+  assert.ok(!source.includes('|pdf'))
+})
 
-  // 队列来源:只读台账接口 + 展示状态=待审核
-  assert.match(source, /const AUDIT_DISPLAY_PENDING = 1/u)
-  assert.match(source, /listAlarmLedger\(\{\.\.\.query\.value, displayStatus: AUDIT_DISPLAY_PENDING\}\)/u)
-  assert.doesNotMatch(source, /listAlarmHistory\(\{\.\.\.query\.value, alarmStatus: 0\}\)/u,
-      '不应再按底层报警记录状态硬过滤队列')
+test('legacy ledger navigates to alarmAudit and no longer implements auditing', async () => {
+  const ledger = await readFile('src/views/subSystem/waterSupply/components/WaterAlarmPage.vue','utf8')
+  assert.ok(ledger.includes("/monitorAlarm/alarmAudit"))
+  assert.ok(!ledger.includes('submitAudit'))
+  assert.ok(!ledger.includes('auditOrderStatCards'))
+  const api = await readFile('src/api/pipeNetwork/waterSupply.js','utf8')
+  assert.ok(!api.includes('/waterSupply/alarm/audit'))
+})
 
-  // 「未处理/已处理」噪声口径不应再出现在审核模式
-  assert.doesNotMatch(source, /<el-option label="未处理" :value="0"\/>/u)
-  assert.doesNotMatch(source, /Number\(row\.alarmStatus\) === 1 \? '已处理' : '未处理'/u)
-  assert.match(source, /<el-tag :type="displayStatusType\(row\.displayStatus\)">/u,
-      '两种模式都应展示后端推导的展示状态')
+test('four identical relative alarmAudit paths retain unique router names', () => {
+  const routes = ensureUniqueSubsystemRouteNames(['waterSupply','drainage','gas','manholeCover'].map(name => ({
+    path: '/subSystem/' + name, children: [{path:'alarmAudit',name:'AlarmAudit'}]
+  })))
+  assert.equal(new Set(routes.map(route => route.children[0].name)).size,4)
 })

+ 0 - 98
tests/waterSupplyThreshold.test.mjs

@@ -1,98 +0,0 @@
-import test from 'node:test'
-import assert from 'node:assert/strict'
-import { readFile } from 'node:fs/promises'
-import {
-  buildThresholdQuery,
-  buildThresholdSavePayload,
-  buildThresholdUpdatePayload
-} from '../src/utils/waterSupplyUiModel.js'
-
-test('threshold page uses the water-only threshold device endpoint', async () => {
-  const source = await readFile(
-    'src/views/subSystem/waterSupply/components/WaterThresholdPage.vue',
-    'utf8'
-  )
-
-  assert.match(source, /listThresholdDevices/u)
-  assert.doesNotMatch(source, /listDeviceLedger/u)
-})
-
-test('threshold drawer follows the shared right-side detail drawer contract', async () => {
-  const source = await readFile(
-    'src/views/subSystem/waterSupply/components/WaterThresholdPage.vue',
-    'utf8'
-  )
-
-  assert.match(source, /<el-drawer\b[^>]*direction="rtl"[^>]*>/u)
-  assert.match(source, /<el-drawer\b[^>]*class="detail-drawer threshold-drawer"[^>]*>/u)
-  assert.match(source, /\.threshold-drawer\s*\{\s*width:\s*100% !important/u)
-})
-
-test('builds the flat batch-save contract expected by the threshold API', () => {
-  const payload = buildThresholdSavePayload({
-    id: 'draft-id',
-    deviceCodes: [' WS-FLOW-1 ', '', 'WS-FLOW-2', 'WS-FLOW-1'],
-    warningType: ' 流量预警 ',
-    warningCode: ' WARN-FLOW ',
-    minValue: '20',
-    maxValue: '40',
-    periodStart: '08:00',
-    periodEnd: '20:00',
-    remark: ' 高峰期阈值 '
-  })
-
-  assert.deepEqual(payload, {
-    deviceCodes: ['WS-FLOW-1', 'WS-FLOW-2'],
-    warningType: '流量预警',
-    warningCode: 'WARN-FLOW',
-    minValue: 20,
-    maxValue: 40,
-    periodStart: '08:00',
-    periodEnd: '20:00',
-    remark: '高峰期阈值'
-  })
-})
-
-test('builds an update payload that preserves the threshold id', () => {
-  const payload = buildThresholdUpdatePayload({
-    id: 'threshold-id',
-    deviceCode: 'WS-FLOW-1',
-    warningType: '流量预警',
-    warningCode: 'WARN-FLOW',
-    minValue: 20,
-    maxValue: 40,
-    periodStart: '08:00',
-    periodEnd: '20:00',
-    createTime: '2026-09-02 10:00:00'
-  })
-
-  assert.deepEqual(payload, {
-    id: 'threshold-id',
-    deviceCode: 'WS-FLOW-1',
-    warningType: '流量预警',
-    warningCode: 'WARN-FLOW',
-    minValue: 20,
-    maxValue: 40,
-    periodStart: '08:00',
-    periodEnd: '20:00'
-  })
-})
-
-test('builds threshold list filters without alarm-only fields', () => {
-  const query = buildThresholdQuery({
-    pageNum: 2,
-    pageSize: 20,
-    deviceCode: 'WS-FLOW-1',
-    warningType: '流量预警',
-    keyword: ' ',
-    alarmLevel: 2,
-    alarmStatus: 0
-  })
-
-  assert.deepEqual(query, {
-    pageNum: 2,
-    pageSize: 20,
-    deviceCode: 'WS-FLOW-1',
-    warningType: '流量预警'
-  })
-})

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini