Procházet zdrojové kódy

feat(drainage): 排水基础数据页面重做

排水管网(psgwgl.vue)
- 数据源改为 pipe_network_base(network_type=1),整页重写为只读
- 查询条件按表字段重设:编码/名称/等级/管养单位/材质/状态/管径区间/建设日期区间
- GIS 地图按 start/end 经纬度画线、起点打点,线条颜色跟随状态字典并配图例
- 列表仅保留详情,详情抽屉内附管段小地图

工程设施(gcssgl.vue)
- 整页重写:地图分布 + 设施类型页签 + 多条件筛选 + 列表 + 详情
- 补充新增/修改/删除/批量删除,表单支持地图选点(BD09 转 WGS84 回填)
- 设施类型、运行状态改字典驱动;设施等级前端固定选项,落库存编码
- 管理单位改为普通输入框;列表与详情按类型展示设计参数

易积水点(yjsdgl.vue)
- 整页重写:地图分布 + 多条件筛选 + 列表 + 增删改 + 详情抽屉
- 关联对象由通用监测设备改为摄像头,新增关联摄像头弹窗(已占用禁选)
- 地图标记与列表「视频」按钮可直接播放关联实时视频,支持多摄像头切换
- 补充积水深度/积水面积/风险等级/责任单位/责任人/联系电话

关联调整
- home.vue、jsqc.vue 去掉 waterloggingPoints 多点位解析,改用单点经纬度与 createTime
- bzyxqk.vue 泵站查询改为 facilityType=2,坐标改读 longitude/latitude
- psfqgl.vue 积水等级接字典,列表按等级着色,排水量/积水量补单位
- api/drainage.js 新增管网/工程设施/易积水点的 map、detail、filterOptions、batch、摄像头接口,
  删除已无调用的旧封装
null před 20 hodinami
rodič
revize
e8bb3aebc5

+ 106 - 27
src/api/drainage.js

@@ -23,24 +23,82 @@ export function batchDeletePartition(ids) {
   return request({ url: '/api/drainage/partition/batch', method: 'delete', data: ids })
 }
 
-// ============ 排水管网 ============
+// ============ 排水管网(旧表 psgw_pipe_network_info,只读分页) ============
+// 仅综合风险区域分析 / 综合风险统计分析 / 管网运行情况 三个页面使用,
+// 它们依赖旧表的 equipmentId 与 points 字段(新表 pipe_network_base 无此两项)。
+// 旧接口的新增/修改/删除/设备绑定端点已移除,不要再新增调用。
 export function getPipeNetworkPage(pageNum, pageSize, params) {
   return request({ url: '/api/drainage/pipeNetwork/page', method: 'get', params: { pageNum, pageSize, ...params } })
 }
-export function addDrainagePipeNetwork(data) {
-  return request({ url: '/api/drainage/pipeNetwork', method: 'post', data })
+
+// ============ 排水管网(pipe_network_base,后端强制 network_type=1) ============
+export function getDrainagePipeNetworkPage(pageNum, pageSize, params = {}) {
+  return request({
+    url: '/api/drainage/pipeNetworkBase/page',
+    method: 'get',
+    params: { pageNum, pageSize, ...params }
+  })
 }
-export function updateDrainagePipeNetwork(data) {
-  return request({ url: '/api/drainage/pipeNetwork', method: 'put', data })
+
+// 地图数据:与分页查询同一套筛选条件,不分页,仅返回起终点带坐标的记录
+export function getDrainagePipeNetworkMap(params = {}) {
+  return request({
+    url: '/api/drainage/pipeNetworkBase/map',
+    method: 'get',
+    params,
+    headers: { 'X-Silent-Error': 'true' }
+  })
 }
-export function deleteDrainagePipeNetwork(id) {
-  return request({ url: '/api/drainage/pipeNetwork/' + id, method: 'delete' })
+
+export function getDrainagePipeNetworkDetail(id) {
+  return request({
+    url: '/api/drainage/pipeNetworkBase/' + id,
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
 }
 
-// ============ 工程设施 ============
-export function getFacilityPage(pageNum, pageSize, params) {
+// 筛选下拉项:材质、管养单位去重值
+export function getDrainagePipeNetworkFilterOptions() {
+  return request({
+    url: '/api/drainage/pipeNetworkBase/filterOptions',
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
+// ============ 工程设施(闸门 / 调蓄设施 / 泵站) ============
+export function getFacilityPage(pageNum, pageSize, params = {}) {
   return request({ url: '/api/drainage/facility/page', method: 'get', params: { pageNum, pageSize, ...params } })
 }
+
+// 地图数据:与分页查询同一套筛选条件,不分页,仅返回带坐标的记录
+export function getFacilityMap(params = {}) {
+  return request({
+    url: '/api/drainage/facility/map',
+    method: 'get',
+    params,
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
+export function getFacilityDetail(id) {
+  return request({
+    url: '/api/drainage/facility/' + id,
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
+// 筛选下拉项:设施类型、等级、管理单位、状态去重值
+export function getFacilityFilterOptions() {
+  return request({
+    url: '/api/drainage/facility/filterOptions',
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
 export function addFacility(data) {
   return request({ url: '/api/drainage/facility', method: 'post', data })
 }
@@ -50,11 +108,42 @@ export function updateFacility(data) {
 export function deleteFacility(id) {
   return request({ url: '/api/drainage/facility/' + id, method: 'delete' })
 }
+export function batchDeleteFacility(ids) {
+  return request({ url: '/api/drainage/facility/batch', method: 'delete', data: ids })
+}
 
 // ============ 易积水点 ============
-export function getWaterloggingPage(pageNum, pageSize, params) {
+export function getWaterloggingPage(pageNum, pageSize, params = {}) {
   return request({ url: '/api/drainage/waterlogging/page', method: 'get', params: { pageNum, pageSize, ...params } })
 }
+
+// 地图数据:与分页查询同一套筛选条件,不分页,仅返回带坐标的记录
+export function getWaterloggingMap(params = {}) {
+  return request({
+    url: '/api/drainage/waterlogging/map',
+    method: 'get',
+    params,
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
+export function getWaterloggingDetail(id) {
+  return request({
+    url: '/api/drainage/waterlogging/' + id,
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
+// 筛选下拉项:所属区域、积水程度去重值
+export function getWaterloggingFilterOptions() {
+  return request({
+    url: '/api/drainage/waterlogging/filterOptions',
+    method: 'get',
+    headers: { 'X-Silent-Error': 'true' }
+  })
+}
+
 export function addWaterlogging(data) {
   return request({ url: '/api/drainage/waterlogging', method: 'post', data })
 }
@@ -64,6 +153,13 @@ export function updateWaterlogging(data) {
 export function deleteWaterlogging(id) {
   return request({ url: '/api/drainage/waterlogging/' + id, method: 'delete' })
 }
+export function batchDeleteWaterlogging(ids) {
+  return request({ url: '/api/drainage/waterlogging/batch', method: 'delete', data: ids })
+}
+// 关联摄像头(整体替换,空数组表示解除全部)
+export function bindWaterloggingCameras(id, cameraIds) {
+  return request({ url: `/api/drainage/waterlogging/${id}/cameras`, method: 'post', data: { cameraIds } })
+}
 
 // ============ 监测设备管理 ============
 let drainageEquipmentTypeIdPromise = null
@@ -188,13 +284,6 @@ export function getBoundEquipmentIds(excludeId) {
   return request({ url: '/api/drainage/partition/boundEquipmentIds', method: 'get', params, headers: { 'X-Silent-Error': 'true' } })
 }
 
-// 查询所有已被关联的设备ID(排水管网)
-export function getPipeNetworkBoundEquipmentIds(excludeId) {
-  const params = {}
-  if (excludeId) params.excludeId = excludeId
-  return request({ url: '/api/drainage/pipeNetwork/boundEquipmentIds', method: 'get', params, headers: { 'X-Silent-Error': 'true' } })
-}
-
 // 查询排水基础数据中已被占用的设备(跨分区、管网、工程设施、易积水点)
 export function getDrainageBoundEquipmentIds(excludeType, excludeId) {
   const params = {}
@@ -208,21 +297,11 @@ export function getDrainageBoundEquipmentIds(excludeType, excludeId) {
   })
 }
 
-// 排水管网关联设备
-export function bindPipeNetworkEquipment(data) {
-  return request({ url: '/api/drainage/pipeNetwork/bindEquipment', method: 'post', data })
-}
-
 // 工程设施关联设备
 export function bindFacilityEquipment(data) {
   return request({ url: '/api/drainage/facility/bindEquipment', method: 'post', data })
 }
 
-// 易积水点关联设备
-export function bindWaterloggingEquipment(data) {
-  return request({ url: '/api/drainage/waterlogging/bindEquipment', method: 'post', data })
-}
-
 // 报警分页列表(复用 AlarmController.list,走 operations.page)
 export function getAlarmDataPage(pageNum, pageSize, params) {
   const query = { pageNum, pageSize, ...params }

+ 21 - 36
src/views/subSystem/drainage/home.vue

@@ -694,19 +694,6 @@ async function loadWaterloggingData() {
     const list = pageData?.records || pageData?.rows || []
     console.log('[大屏调试] 积水数据:', list.length, list.length > 0 ? JSON.stringify(list[0]) : '')
 
-    // 解析waterloggingPoints JSON字符串为数组
-    function parseWaterloggingPoints(raw) {
-      if (!raw) return []
-      if (Array.isArray(raw)) return raw
-      if (typeof raw === 'string') {
-        try {
-          const parsed = JSON.parse(raw)
-          return Array.isArray(parsed) ? parsed : []
-        } catch (e) { return [] }
-      }
-      return []
-    }
-
     // 积水等级判断(用waterloggingLevel字段:0=未积水, 1=轻度, 2=中度, 3=严重)
     const levelMap = [
       { label: '未积水', cls: 'none', color: '#67c23a' },
@@ -719,24 +706,22 @@ async function loadWaterloggingData() {
       return levelMap[lvl] || levelMap[0]
     }
 
-    // 地图点位(从 waterloggingPoints 字段解析坐标)
+    // 地图点位(易积水点为单点要素,直接用经纬度)
     const allMapPoints = []
     list.forEach(item => {
-      const points = parseWaterloggingPoints(item.waterloggingPoints)
+      const lng = Number(item.longitude)
+      const lat = Number(item.latitude)
+      if (!Number.isFinite(lng) || !Number.isFinite(lat) || (lng === 0 && lat === 0)) return
       const lv = getLevelInfo(item)
-      points.forEach(p => {
-        if (p.lng && p.lat) {
-          allMapPoints.push({
-            name: p.name || item.name || '积水点',
-            lng: Number(p.lng),
-            lat: Number(p.lat),
-            color: lv.color,
-            subText: `${lv.label}`,
-            depth: item.waterVolume || item.waterDepth || 0,
-            level: lv.label,
-            levelClass: lv.cls
-          })
-        }
+      allMapPoints.push({
+        name: item.pointName || '积水点',
+        lng,
+        lat,
+        color: lv.color,
+        subText: `${lv.label}`,
+        depth: item.waterVolume || item.waterDepth || 0,
+        level: lv.label,
+        levelClass: lv.cls
       })
     })
     waterMapPoints.value = allMapPoints
@@ -757,19 +742,19 @@ async function loadWaterloggingData() {
     waterRealtimeData.value = list.slice(0, 12).map((p, i) => {
       const lv = getLevelInfo(p)
       return {
-        name: p.name || p.pointName || `积水点-${i + 1}`,
-        time: p.time || p.dataTime || p.updateTime || p.createTime || '',
+        name: p.pointName || `积水点-${i + 1}`,
+        time: p.createTime || p.updateTime || '',
         depth: p.waterVolume || p.waterDepth || p.depth || 0,
         level: lv.label,
         levelClass: lv.cls
       }
     })
 
-    // 设备在线统计 - 获取关联设备
+    // 设备在线统计 - 取易积水点关联的摄像头
     const allEqIds = []
     list.forEach(p => {
-      if (p.equipmentId) {
-        p.equipmentId.split(',').forEach(id => {
+      if (p.cameraIds) {
+        p.cameraIds.split(',').forEach(id => {
           const trimmed = id.trim()
           if (trimmed) allEqIds.push(trimmed)
         })
@@ -984,7 +969,7 @@ async function updateWaterStatChart() {
         const year = d.getFullYear()
         xData.push(`${month}月`)
         const count = allList.filter(item => {
-          const t = item.time || item.dataTime || item.createTime || ''
+          const t = item.createTime || ''
           if (!t) return false
           const itemDate = new Date(t)
           return itemDate.getFullYear() === year && itemDate.getMonth() + 1 === month
@@ -998,7 +983,7 @@ async function updateWaterStatChart() {
         const y = currentYear - i
         xData.push(String(y))
         const count = allList.filter(item => {
-          const t = item.time || item.dataTime || item.createTime || ''
+          const t = item.createTime || ''
           if (!t) return false
           return new Date(t).getFullYear() === y
         }).length
@@ -1022,7 +1007,7 @@ async function doWaterAnalysis() {
     const level = analysisLevel.value
     // 按年份过滤
     let filtered = allList.filter(item => {
-      const t = item.time || item.dataTime || item.createTime || ''
+      const t = item.createTime || ''
       if (!t) return false
       return new Date(t).getFullYear() === year
     })

+ 8 - 15
src/views/subSystem/drainage/jcsj/bzyxqk.vue

@@ -262,7 +262,8 @@ async function loadPumpAlarm(stationCode) {
 async function loadData() {
   try {
     // 1. 先查询设施类型为"泵站"的工程设施,获取关联的泵站ID和坐标
-    const facilityRes = await getFacilityPage(1, 1000, { facilityType: '泵站' })
+    //    facilityType 走字典 drainage_facility_type,2 = 泵站
+    const facilityRes = await getFacilityPage(1, 1000, { facilityType: '2' })
     const facilityPage = facilityRes.code !== undefined ? facilityRes.data : facilityRes
     const facilities = facilityPage.records || []
 
@@ -271,26 +272,18 @@ async function loadData() {
     let earliestDataTime = null
     facilities.forEach(f => {
       if (f.equipmentId) {
-        // 解析设施坐标(来自points字段)
-        let fLng = 0, fLat = 0
-        if (f.points) {
-          try {
-            const pts = typeof f.points === 'string' ? JSON.parse(f.points) : f.points
-            if (Array.isArray(pts) && pts.length > 0) {
-              fLng = Number(pts[0].lng) || 0
-              fLat = Number(pts[0].lat) || 0
-            }
-          } catch (e) { /* ignore */ }
-        }
-        // 记录设施的数据时间(关联时间)
-        const fTime = f.dataTime || f.createTime || ''
+        // 设施为点要素,坐标直接取经纬度字段
+        const fLng = Number(f.longitude) || 0
+        const fLat = Number(f.latitude) || 0
+        // 记录设施的建档时间(原 dataTime 字段已随表结构重建移除)
+        const fTime = f.createTime || ''
         if (fTime && (!earliestDataTime || fTime < earliestDataTime)) {
           earliestDataTime = fTime
         }
         // 分割多个ID
         const ids = f.equipmentId.split(',').map(s => s.trim()).filter(Boolean)
         ids.forEach(id => {
-          pumpIdFacilityMap[id] = { lng: fLng, lat: fLat, facilityName: f.name || '', dataTime: fTime }
+          pumpIdFacilityMap[id] = { lng: fLng, lat: fLat, facilityName: f.facilityName || '', dataTime: fTime }
         })
       }
     })

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 686 - 780
src/views/subSystem/drainage/jcsj/gcssgl.vue


+ 15 - 47
src/views/subSystem/drainage/jcsj/jsqc.vue

@@ -260,22 +260,6 @@ function getWaterloggingLevelInfo(level) {
   return map[level] || map[Number(level)] || { label: '未积水', levelClass: 'none' }
 }
 
-// 安全解析 waterloggingPoints(后端返回 JSON 字符串,需转为数组使用)
-function parseWaterloggingPoints(raw) {
-  if (!raw) return []
-  if (Array.isArray(raw)) return raw
-  if (typeof raw === 'string') {
-    try {
-      const parsed = JSON.parse(raw)
-      return Array.isArray(parsed) ? parsed : []
-    } catch (e) {
-      console.warn('waterloggingPoints JSON 解析失败:', raw, e)
-      return []
-    }
-  }
-  return []
-}
-
 // ==================== API数据加载 ====================
 async function loadWaterloggingData() {
   try {
@@ -284,39 +268,23 @@ async function loadWaterloggingData() {
     const pageData = res && res.code !== undefined ? res.data : res
     const list = Array.isArray(pageData.records) ? pageData.records : (Array.isArray(pageData) ? pageData : [])
 
-    // 构建地图点位:展示每个易积水点的所有子点位
+    // 构建地图点位:易积水点为单点要素,直接用经纬度
     const allMapPoints = []
     list.forEach(p => {
       const levelInfo = getWaterloggingLevelInfo(p.waterloggingLevel)
-      const subPoints = parseWaterloggingPoints(p.waterloggingPoints)
-      if (subPoints.length > 0) {
-        subPoints.forEach(sp => {
-          if (sp.lng && sp.lat) {
-            allMapPoints.push({
-              name: sp.name || p.pointName || '未命名',
-              lng: Number(sp.lng),
-              lat: Number(sp.lat),
-              depth: sp.depth ?? p.waterVolume ?? 0,
-              level: levelInfo.label,
-              levelClass: levelInfo.levelClass,
-              parentId: p.id,
-              parentName: p.pointName
-            })
-          }
-        })
-      } else if (p.longitude && p.latitude) {
-        // 兼容无子点位的旧数据:使用主点坐标
-        allMapPoints.push({
-          name: p.pointName || '未命名',
-          lng: Number(p.longitude),
-          lat: Number(p.latitude),
-          depth: p.waterVolume ?? 0,
-          level: levelInfo.label,
-          levelClass: levelInfo.levelClass,
-          parentId: p.id,
-          parentName: p.pointName
-        })
-      }
+      const lng = Number(p.longitude)
+      const lat = Number(p.latitude)
+      if (!Number.isFinite(lng) || !Number.isFinite(lat) || (lng === 0 && lat === 0)) return
+      allMapPoints.push({
+        name: p.pointName || '未命名',
+        lng,
+        lat,
+        depth: p.waterVolume ?? 0,
+        level: levelInfo.label,
+        levelClass: levelInfo.levelClass,
+        parentId: p.pointId,
+        parentName: p.pointName
+      })
     })
     mapPoints.value = allMapPoints
 
@@ -481,7 +449,7 @@ async function initLineChart() {
       const year = d.getFullYear()
       xData.push(`${month}月`)
       const count = list.filter(item => {
-        const t = item.time || item.dataTime || item.updateTime || item.createTime || ''
+        const t = item.createTime || ''
         if (!t) return false
         const itemDate = new Date(t)
         return itemDate.getFullYear() === year && itemDate.getMonth() + 1 === month

+ 97 - 14
src/views/subSystem/drainage/jcsj/psfqgl.vue

@@ -43,9 +43,24 @@
       <el-table-column label="地点" prop="district" min-width="100" />
       <el-table-column label="分区类型" prop="partitionType" min-width="100" />
       <el-table-column label="数据时间" prop="time" min-width="120" />
-      <el-table-column label="排水量" prop="drainage" min-width="90" />
-      <el-table-column label="积水量" prop="waterlogging" min-width="90" />
-      <el-table-column label="积水等级" prop="level" min-width="80" />
+      <el-table-column :label="`排水量(${DRAINAGE_UNIT})`" prop="drainage" min-width="120" align="right">
+        <template #default="{ row }">
+          <span>{{ formatVolume(row.drainage) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="`积水量(${WATERLOGGING_UNIT})`" prop="waterlogging" min-width="120" align="right">
+        <template #default="{ row }">
+          <span>{{ formatVolume(row.waterlogging) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="积水等级" prop="level" min-width="110" align="center">
+        <template #default="{ row }">
+          <el-tag v-if="hasLevel(row.level)" :type="getLevelTagType(row.level)" effect="light" size="small">
+            {{ getLevelLabel(row.level) }}
+          </el-tag>
+          <span v-else class="text-placeholder">--</span>
+        </template>
+      </el-table-column>
       <el-table-column label="关联设备" min-width="160">
         <template #default="{ row }">
           {{ equipmentNameMap[row.id] || '' }}
@@ -93,7 +108,7 @@
           <el-col :span="12">
             <div class="info-item">
               <span class="info-label">排水量:</span>
-              <span class="info-value">{{ currentRow.drainage }}</span>
+              <span class="info-value">{{ formatVolume(currentRow.drainage) }} {{ DRAINAGE_UNIT }}</span>
             </div>
           </el-col>
         </el-row>
@@ -101,13 +116,18 @@
           <el-col :span="12">
             <div class="info-item">
               <span class="info-label">积水量:</span>
-              <span class="info-value">{{ currentRow.waterlogging }}</span>
+              <span class="info-value">{{ formatVolume(currentRow.waterlogging) }} {{ WATERLOGGING_UNIT }}</span>
             </div>
           </el-col>
           <el-col :span="12">
             <div class="info-item">
               <span class="info-label">积水等级:</span>
-              <span class="info-value">{{ currentRow.level }}</span>
+              <span class="info-value">
+                <el-tag v-if="hasLevel(currentRow.level)" :type="getLevelTagType(currentRow.level)" effect="light" size="small">
+                  {{ getLevelLabel(currentRow.level) }}
+                </el-tag>
+                <span v-else class="text-placeholder">--</span>
+              </span>
             </div>
           </el-col>
         </el-row>
@@ -169,13 +189,13 @@
         </el-row>
         <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="排水量" prop="drainage">
+            <el-form-item :label="`排水量(${DRAINAGE_UNIT})`" prop="drainage">
               <el-input-number v-model="editForm.drainage" :precision="2" :min="0" style="width:100%"
                 placeholder="请输入排水量" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="积水量" prop="waterlogging">
+            <el-form-item :label="`积水量(${WATERLOGGING_UNIT})`" prop="waterlogging">
               <el-input-number v-model="editForm.waterlogging" :precision="2" :min="0" style="width:100%"
                 placeholder="请输入积水量" />
             </el-form-item>
@@ -184,8 +204,10 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="积水等级" prop="level">
-              <el-input-number v-model="editForm.level" :min="1" :max="5" :precision="0" style="width:100%"
-                placeholder="请输入积水等级(1-5)" />
+              <el-select v-model="editForm.level" placeholder="请选择积水等级" clearable style="width:100%">
+                <el-option v-for="item in levelOptions" :key="item.value" :label="item.label"
+                  :value="normalizeLevelValue(item.value)" />
+              </el-select>
             </el-form-item>
           </el-col>
         </el-row>
@@ -260,6 +282,7 @@
 <script setup name="Psfqgl">
 import { ref, computed, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { useDict } from '@/utils/dict'
 import locationIcon from '@/assets/images/location.png'
 import {
   getPartitionPage,
@@ -273,6 +296,61 @@ import {
   getDrainageBoundEquipmentIds
 } from '@/api/drainage'
 
+// ==================== 量纲 + 积水等级字典 ====================
+// 排水量 / 积水量 单位:业务口径若调整,只改这两处,表单、列表、详情同步生效
+const DRAINAGE_UNIT = 'm³'
+const WATERLOGGING_UNIT = 'm³'
+
+// 积水等级字典(sys_dict_type = drainage_waterlogging_level)
+// 与易积水点 waterlogging_level 口径保持一致:0未积水 / 1轻度 / 2中度 / 3严重
+const { drainage_waterlogging_level: levelDict } = useDict('drainage_waterlogging_level')
+
+// 字典表未配置时用内置口径兜底,避免页面无选项
+const LEVEL_FALLBACK = [
+  { label: '未积水', value: '0', elTagType: 'success' },
+  { label: '轻度积水', value: '1', elTagType: 'primary' },
+  { label: '中度积水', value: '2', elTagType: 'warning' },
+  { label: '严重积水', value: '3', elTagType: 'danger' }
+]
+
+const levelOptions = computed(() => {
+  const list = levelDict.value
+  return Array.isArray(list) && list.length ? list : LEVEL_FALLBACK
+})
+
+// 字典值统一成数字,保证 el-select 回显能与后端 Integer 字段匹配
+function normalizeLevelValue(value) {
+  if (value === null || value === undefined || value === '') return value
+  return Number.isNaN(Number(value)) ? value : Number(value)
+}
+
+function hasLevel(value) {
+  return value !== null && value !== undefined && value !== ''
+}
+
+function findLevelOption(value) {
+  const key = String(value)
+  return levelOptions.value.find(item => String(item.value) === key)
+}
+
+function getLevelLabel(value) {
+  const option = findLevelOption(value)
+  return option ? option.label : '--'
+}
+
+// 标签配色由字典 list_class 决定;primary / 空值走默认主题色
+function getLevelTagType(value) {
+  const option = findLevelOption(value)
+  const type = option ? (option.elTagType || option.listClass) : ''
+  return !type || type === 'primary' || type === 'default' ? '' : type
+}
+
+function formatVolume(value) {
+  if (value === null || value === undefined || value === '') return '--'
+  const num = Number(value)
+  return Number.isFinite(num) ? num.toFixed(2) : String(value)
+}
+
 // 地图标记创建(用Label+img代替Marker)
 function createMapMarker(map, bPoint, name, bgColor) {
   const color = bgColor || '#409eff'
@@ -533,7 +611,7 @@ const editForm = ref({
   time: '',
   drainage: 0,
   waterlogging: 0,
-  level: 1,
+  level: 0,
   longitude: null,
   latitude: null,
   points: ''
@@ -541,7 +619,8 @@ const editForm = ref({
 
 const editRules = {
   name: [{ required: true, message: '请输入分区名称', trigger: 'blur' }],
-  time: [{ required: true, message: '请选择数据时间', trigger: 'change' }]
+  time: [{ required: true, message: '请选择数据时间', trigger: 'change' }],
+  level: [{ required: true, message: '请选择积水等级', trigger: 'change' }]
 }
 
 // 编辑弹窗地图相关
@@ -565,7 +644,7 @@ function handleAdd() {
     time: '',
     drainage: 0,
     waterlogging: 0,
-    level: 1,
+    level: 0,
     longitude: null,
     latitude: null,
     points: ''
@@ -584,7 +663,7 @@ function handleEdit(row) {
     time: row.time,
     drainage: row.drainage,
     waterlogging: row.waterlogging,
-    level: row.level,
+    level: hasLevel(row.level) ? normalizeLevelValue(row.level) : 0,
     longitude: row.longitude,
     latitude: row.latitude,
     points: row.points || ''
@@ -941,6 +1020,10 @@ async function submitBind() {
   font-size: 14px;
 }
 
+.text-placeholder {
+  color: #c0c4cc;
+}
+
 .map-section {
   border: 1px solid #e4e7ed;
   border-radius: 4px;

+ 523 - 872
src/views/subSystem/drainage/jcsj/psgwgl.vue

@@ -1,986 +1,637 @@
 <template>
-  <div class="app-container">
-    <!-- GIS 地图展示区 -->
-    <div class="map-section" style="margin-bottom: 16px;">
-      <div class="map-tabs">
-        <div class="map-tab" :class="{ active: mainMapTab === 'all' }" @click="switchMainMapTab('all')">全部</div>
-        <div class="map-tab" :class="{ active: mainMapTab === 'pipeline' }" @click="switchMainMapTab('pipeline')">管线</div>
-        <div class="map-tab" :class="{ active: mainMapTab === 'pipePoint' }" @click="switchMainMapTab('pipePoint')">管点
+  <div class="app-container drainage-network-page">
+    <!-- 管网分布地图 -->
+    <el-card shadow="never" class="map-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title">
+            <span>管网分布</span>
+            <span class="card-sub">按当前筛选条件绘制,共 {{ mapRecords.length }} 条带坐标记录</span>
+          </div>
+          <div class="card-actions">
+            <el-button size="small" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
+            <el-button size="small" :icon="Aim" @click="fitMap">全图</el-button>
+          </div>
         </div>
-        <div class="map-tab" :class="{ active: mainMapTab === 'outlet' }" @click="switchMainMapTab('outlet')">排口</div>
-      </div>
-      <div id="psgwglMainMap" class="map-container" style="height: 350px;"></div>
-    </div>
-
-    <!-- 搜索区域 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="管网名称">
-        <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
-      </el-form-item>
-      <el-form-item label="地点">
-        <el-input v-model="queryParams.area" placeholder="请输入" clearable style="width: 200px" />
-      </el-form-item>
-      <el-form-item label="管网类型">
-        <el-select v-model="queryParams.networkType" placeholder="请选择" clearable style="width: 200px">
-          <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="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="filteredList" border style="width: 100%"
-      @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="监测点名称" prop="name" min-width="160" />
-      <el-table-column label="地点" prop="area" min-width="100" />
-      <el-table-column label="管网类型" prop="networkType" min-width="100" />
-      <el-table-column label="数据时间" prop="time" min-width="120" />
-      <el-table-column label="标记数量" prop="pointCount" min-width="80" />
-      <el-table-column label="关联设备" min-width="160">
-        <template #default="{ row }">
-          {{ equipmentNameMap[row.id] || '' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" width="240" align="center">
-        <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="primary" @click="handleBind(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="pageNum" v-model:page-size="pageSize" :total="total"
-      :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-      style="margin-top: 16px; justify-content: flex-end;" @size-change="loadData" @current-change="loadData" />
-
-    <!-- 查看详情对话框 -->
-    <el-drawer v-model="dialogVisible" title="详情" size="700px" :before-close="handleClose" @opened="onDialogOpened" 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.name }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">地点:</span>
-              <span class="info-value">{{ currentRow.area }}</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.networkType }}</span>
-            </div>
-          </el-col>
-          <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">数据时间:</span>
-              <span class="info-value">{{ currentRow.time }}</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">{{ currentRowPointCount }}</span>
-            </div>
-          </el-col>
-        </el-row>
-      </div>
-
-      <!-- 关联设备 -->
-      <div v-if="viewEquipmentNames.length" class="detail-info" style="margin-top: 12px;">
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <div class="info-item">
-              <span class="info-label">关联设备:</span>
-              <span class="info-value">{{ viewEquipmentNames.join('、') }}</span>
-            </div>
-          </el-col>
-        </el-row>
-      </div>
-
-      <!-- 地图区域 -->
-      <div class="map-section">
-        <div class="map-tabs">
-          <div class="map-tab active">{{ currentRow.networkType || '位置标记' }}</div>
+      </template>
+      <div class="map-wrap">
+        <GisMapCanvas
+          ref="mapRef"
+          :points="mapPoints"
+          :lines="mapLines"
+          :enable-clustering="true"
+          cluster-label="管线起点"
+          @ready="onMapReady"
+          @line-click="handleFeatureClick"
+          @point-click="handleFeatureClick" />
+        <div v-if="!mapLines.length" class="map-empty">
+          {{ loading ? '正在加载…' : '当前筛选条件下没有可绘制的管线段' }}
         </div>
-        <div id="psgwglViewMap" class="map-container"></div>
       </div>
-    </el-drawer>
-
-    <!-- 新增/修改对话框 -->
-    <el-dialog v-model="editDialogVisible" :title="editDialogTitle" width="800px" :before-close="handleEditClose"
-      @opened="onEditDialogOpened">
-      <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="100px">
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="监测点名称" prop="name">
-              <el-input v-model="editForm.name" placeholder="请输入监测点名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="地点" prop="area">
-              <el-input v-model="editForm.area" placeholder="请输入地点" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="管网类型" prop="networkType">
-              <el-select v-model="editForm.networkType" placeholder="请选择管网类型" style="width:100%"
-                @change="onNetworkTypeChange">
-                <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="数据时间" prop="time">
-              <el-date-picker v-model="editForm.time" 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="标记数量">
-              <el-input :model-value="editPoints.length" disabled />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-
-      <!-- 地图标点区域 -->
-      <div class="map-section" style="margin-top: 16px;">
-        <div class="map-tabs">
-          <div class="map-tab active">{{ editForm.networkType || '位置标记' }}</div>
-          <div class="map-tab-action" @click="clearEditMarkers">清除所有标记</div>
-        </div>
-        <div id="psgwglEditMap" class="map-container" style="height: 350px;"></div>
-        <div class="marker-count">
-          已标记 {{ editPoints.length }} 个{{ editForm.networkType ? editMarkerLabel : '标记' }}
-        </div>
+      <div class="map-legend">
+        <span class="legend-title">线条颜色:</span>
+        <span v-for="item in statusDict" :key="item.value" class="legend-item">
+          <i :style="{ background: statusColor(item.value) }"></i>{{ item.label }}
+        </span>
+        <span class="legend-item">
+          <i style="background: #409eff"></i>未知状态
+        </span>
       </div>
+    </el-card>
 
-      <template #footer>
-        <el-button @click="handleEditClose">取 消</el-button>
-        <el-button type="primary" @click="handleEditSubmit">确 定</el-button>
+    <!-- 查询条件 -->
+    <el-card shadow="never" class="filter-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title"><span>查询条件</span></div>
+        </div>
       </template>
-    </el-dialog>
-
-    <!-- 关联设备弹框 -->
-    <el-dialog v-model="bindDialogVisible" title="关联设备" width="700px" @close="closeBindDialog">
-      <el-form :inline="true" :model="bindQueryParams" style="margin-bottom: 12px;">
-        <el-form-item label="设备名称">
-          <el-input v-model="bindQueryParams.equipmentName" placeholder="请输入设备名称" clearable />
+      <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
+        <el-form-item label="设施编码">
+          <el-input v-model="query.networkCode" placeholder="请输入" clearable style="width: 160px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="设施名称">
+          <el-input v-model="query.networkName" placeholder="请输入" clearable style="width: 160px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="等级">
+          <el-select v-model="query.networkLevel" placeholder="请选择" clearable style="width: 140px">
+            <el-option v-for="item in levelDict" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="管养单位">
+          <el-select v-model="query.managementUnit" placeholder="请选择" clearable filterable allow-create
+            style="width: 170px">
+            <el-option v-for="item in filterOptions.managementUnits" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="材质">
+          <el-select v-model="query.material" placeholder="请选择" clearable filterable allow-create style="width: 140px">
+            <el-option v-for="item in filterOptions.materials" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="query.status" placeholder="请选择" clearable style="width: 130px">
+            <el-option v-for="item in statusDict" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="管径(mm)">
+          <div class="range-input">
+            <el-input-number v-model="query.diameterMin" :min="0" :precision="0" :controls="false"
+              placeholder="最小" style="width: 80px" />
+            <span class="range-sep">~</span>
+            <el-input-number v-model="query.diameterMax" :min="0" :precision="0" :controls="false"
+              placeholder="最大" style="width: 80px" />
+          </div>
+        </el-form-item>
+        <el-form-item label="建设日期">
+          <el-date-picker v-model="buildRange" type="daterange" value-format="YYYY-MM-DD"
+            range-separator="-" start-placeholder="开始" end-placeholder="结束" style="width: 230px" />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="handleBindQuery">搜索</el-button>
+          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
         </el-form-item>
       </el-form>
-      <el-table v-loading="bindLoading" :data="bindTableData" border row-key="equipmentId">
-        <el-table-column label="选择" width="55" align="center">
+    </el-card>
+
+    <!-- 管网清单 -->
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title"><span>管网清单</span></div>
+        </div>
+      </template>
+      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
+        @row-dblclick="openDetail">
+        <el-table-column label="设施编码" prop="networkCode" min-width="120" show-overflow-tooltip />
+        <el-table-column label="设施名称" prop="networkName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="等级" width="90" align="center">
           <template #default="{ row }">
-            <el-checkbox :model-value="selectedEquipmentIds.includes(String(row.equipmentId))"
-              :disabled="isEquipmentBound(row)" @change="toggleEquipment(row, $event)" />
+            <el-tag v-if="levelLabel(row.networkLevel)" size="small"
+              :type="levelTagType(row.networkLevel)">{{ levelLabel(row.networkLevel) }}</el-tag>
+            <span v-else class="text-muted">-</span>
           </template>
         </el-table-column>
-        <el-table-column label="设备名称" prop="equipmentName" min-width="120" />
-        <el-table-column label="设备编号" prop="equipmentCode" min-width="120" />
-        <el-table-column label="设备类型" prop="equipmentTypeName" min-width="100" />
-        <el-table-column label="安装位置" prop="installLocation" min-width="120" />
-        <el-table-column label="关联状态" min-width="180">
+        <el-table-column label="管养单位" prop="managementUnit" min-width="140" show-overflow-tooltip />
+        <el-table-column label="材质" prop="material" width="100" show-overflow-tooltip />
+        <el-table-column label="管径(mm)" prop="diameter" width="95" align="right" />
+        <el-table-column label="长度(m)" prop="length" width="95" align="right" />
+        <el-table-column label="位置描述" prop="location" min-width="150" show-overflow-tooltip />
+        <el-table-column label="状态" width="90" align="center">
           <template #default="{ row }">
-            <span v-if="selectedEquipmentIds.includes(String(row.equipmentId))" style="color: #67c23a;">当前选择</span>
-            <span v-else-if="boundEquipmentMap[String(row.equipmentId)]" style="color: #e6a23c;">
-              已关联:{{ boundEquipmentMap[String(row.equipmentId)] }}
-            </span>
-            <span v-else>未关联</span>
+            <el-tag size="small" :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="建设日期" prop="buildDate" width="110" align="center" />
+        <el-table-column label="操作" width="90" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetail(row)">详情</el-button>
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="bindPageNum" v-model:page-size="bindPageSize" :total="bindTotal"
-        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
-        style="margin-top: 12px; justify-content: flex-end;" @size-change="loadBindData" @current-change="loadBindData" />
+      <el-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"
+        class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
+    </el-card>
+
+    <!-- 详情抽屉(只读) -->
+    <el-drawer v-model="detailVisible" title="排水管网详情" size="680px" direction="rtl"
+      @opened="onDetailOpened">
+      <el-descriptions :column="2" border class="detail-desc">
+        <el-descriptions-item label="设施编码">{{ fmt(detailRow.networkCode) }}</el-descriptions-item>
+        <el-descriptions-item label="设施名称">{{ fmt(detailRow.networkName) }}</el-descriptions-item>
+        <el-descriptions-item label="等级">
+          <el-tag v-if="levelLabel(detailRow.networkLevel)" size="small"
+            :type="levelTagType(detailRow.networkLevel)">{{ levelLabel(detailRow.networkLevel) }}</el-tag>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag size="small" :type="statusTagType(detailRow.status)">{{ statusLabel(detailRow.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="管养单位">{{ fmt(detailRow.managementUnit) }}</el-descriptions-item>
+        <el-descriptions-item label="材质">{{ fmt(detailRow.material) }}</el-descriptions-item>
+        <el-descriptions-item label="管径(mm)">{{ fmt(detailRow.diameter) }}</el-descriptions-item>
+        <el-descriptions-item label="长度(m)">{{ fmt(detailRow.length) }}</el-descriptions-item>
+        <el-descriptions-item label="建设日期">{{ fmt(detailRow.buildDate) }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ fmt(detailRow.createTime) }}</el-descriptions-item>
+        <el-descriptions-item label="起点经度">{{ fmt(detailRow.startLongitude) }}</el-descriptions-item>
+        <el-descriptions-item label="起点纬度">{{ fmt(detailRow.startLatitude) }}</el-descriptions-item>
+        <el-descriptions-item label="终点经度">{{ fmt(detailRow.endLongitude) }}</el-descriptions-item>
+        <el-descriptions-item label="终点纬度">{{ fmt(detailRow.endLatitude) }}</el-descriptions-item>
+        <el-descriptions-item label="位置描述" :span="2">{{ fmt(detailRow.location) }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ fmt(detailRow.remark) }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div class="detail-map-section">
+        <div class="detail-map-title">管段位置</div>
+        <div class="detail-map-wrap">
+          <GisMapCanvas v-if="detailVisible" ref="detailMapRef" :points="detailPoints" :lines="detailLines"
+            :enable-clustering="false" :show-marker-name="false" @ready="onDetailMapReady" />
+          <div v-if="!detailLines.length && !detailPoints.length" class="map-empty">该管网未录入起终点坐标</div>
+        </div>
+      </div>
+
       <template #footer>
-        <el-button @click="closeBindDialog">取 消</el-button>
-        <el-button type="primary" @click="submitBind">确 定</el-button>
+        <el-button @click="detailVisible = false">关 闭</el-button>
       </template>
-    </el-dialog>
+    </el-drawer>
   </div>
 </template>
 
 <script setup name="Psgwgl">
-import { ref, computed, nextTick, onMounted } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import locationIcon from '@/assets/images/location.png'
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Aim, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
+import { useDict } from '@/utils/dict'
 import {
-  getPipeNetworkPage,
-  addDrainagePipeNetwork,
-  updateDrainagePipeNetwork,
-  deleteDrainagePipeNetwork,
-  getEquipmentPage,
-  getEquipmentById,
-  bindPipeNetworkEquipment,
-  getDrainageBoundEquipmentIds
+  getDrainagePipeNetworkPage,
+  getDrainagePipeNetworkMap,
+  getDrainagePipeNetworkDetail,
+  getDrainagePipeNetworkFilterOptions
 } from '@/api/drainage'
 
-// 地图标记创建(用Label+img代替Marker)
-function createMapMarker(map, bPoint, name, bgColor) {
-  const color = bgColor || '#409eff'
-  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;">${name || ''}</div>
-    <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
-  </div>`
-  const label = new BMapGL.Label(html, {
-    position: bPoint,
-    offset: new BMapGL.Size(-30, -50)
-  })
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0'
-  })
-  map.addOverlay(label)
-  return label
-}
-
-// 解析坐标数据(兼容JSON字符串和数组)
-function parsePoints(data) {
-  if (!data) return []
-  if (typeof data === 'string') {
-    try { return JSON.parse(data) } catch (e) { return [] }
-  }
-  return Array.isArray(data) ? data : []
-}
-
-// ==================== 顶部主地图 ====================
-let mainMapInstance = null
-const mainMapTab = ref('all')
-
-function switchMainMapTab(tab) {
-  mainMapTab.value = tab
-  renderMainMapMarkers()
-}
-
-function initMainMap() {
-  const container = document.getElementById('psgwglMainMap')
-  if (!container) return
-  try {
-    mainMapInstance = new BMapGL.Map('psgwglMainMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    mainMapInstance.centerAndZoom(centerPoint, 12)
-    mainMapInstance.enableScrollWheelZoom(true)
-    renderMainMapMarkers()
-  } catch (e) {
-    console.error('顶部百度地图初始化失败:', e)
-  }
-}
-
-function renderMainMapMarkers() {
-  if (!mainMapInstance) return
-  mainMapInstance.clearOverlays()
-
-  const allPoints = []
-  const tab = mainMapTab.value
-  // 按 networkType 过滤:管线tab显示networkType='管线'的,管点tab显示'管点',排口tab显示'排口',全部显示所有
-  const networkTypeMap = { pipeline: '管线', pipePoint: '管点', outlet: '排口' }
-  const targetNetworkType = tab === 'all' ? null : networkTypeMap[tab]
-
-  tableData.value.forEach(row => {
-    if (targetNetworkType && row.networkType !== targetNetworkType) return
-    const points = parsePoints(row.points)
-    if (points.length === 0) return
-
-    if (row.networkType === '管线') {
-      // 管线模式:绘制折线 + 节点标记
-      if (points.length >= 2) {
-        const polylinePoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
-        const polyline = new BMapGL.Polyline(polylinePoints, {
-          strokeColor: '#409eff',
-          strokeWeight: 4,
-          strokeOpacity: 0.8
-        })
-        mainMapInstance.addOverlay(polyline)
-      }
-      points.forEach(p => {
-        if (p.lng && p.lat) {
-          const bPoint = new BMapGL.Point(p.lng, p.lat)
-          createMapMarker(mainMapInstance, bPoint, p.name || row.name)
-          allPoints.push(bPoint)
-        }
-      })
-    } else {
-      // 管点/排口模式:只显示标记点
-      points.forEach(p => {
-        if (p.lng && p.lat) {
-          const bPoint = new BMapGL.Point(p.lng, p.lat)
-          createMapMarker(mainMapInstance, bPoint, p.name || row.name)
-          allPoints.push(bPoint)
-        }
-      })
-    }
-  })
-
-  if (allPoints.length > 0) {
-    mainMapInstance.setViewport(allPoints)
-  }
-}
-
-// ==================== 搜索参数 ====================
-const queryParams = ref({
-  name: '',
-  area: '',
-  networkType: ''
+const { pipe_network_status: statusDict, pipe_network_level: levelDict } =
+  useDict('pipe_network_status', 'pipe_network_level')
+
+// ==================== 查询条件 ====================
+const query = ref({
+  networkCode: '',
+  networkName: '',
+  networkLevel: '',
+  managementUnit: '',
+  material: '',
+  status: '',
+  diameterMin: null,
+  diameterMax: null
 })
+const buildRange = ref([])
 
-// ==================== 表格数据 ====================
+// 下拉项(材质、管养单位去重值)
+const filterOptions = ref({ materials: [], managementUnits: [] })
+
+// ==================== 列表 ====================
 const tableData = ref([])
 const total = ref(0)
 const pageNum = ref(1)
 const pageSize = ref(10)
 const loading = ref(false)
 
-// 加载分页数据
-async function loadData() {
+// ==================== 地图 ====================
+const mapRecords = ref([])
+const mapRef = ref(null)
+
+// ==================== 详情 ====================
+const detailVisible = ref(false)
+const detailRow = ref({})
+const detailMapRef = ref(null)
+
+/** 组装请求参数:空值不下发 */
+function buildParams() {
+  const params = {}
+  Object.keys(query.value).forEach((key) => {
+    const value = query.value[key]
+    if (value !== '' && value !== null && value !== undefined) {
+      params[key] = value
+    }
+  })
+  const range = buildRange.value || []
+  if (range[0]) params.buildDateStart = range[0]
+  if (range[1]) params.buildDateEnd = range[1]
+  return params
+}
+
+/** 列表与地图共用一套筛选条件,一起刷新 */
+async function loadAll() {
   loading.value = true
+  const params = buildParams()
   try {
-    const res = await getPipeNetworkPage(pageNum.value, pageSize.value, queryParams.value)
-    // 兼容直接返回Page对象(无code包装)和AjaxResult包装两种格式
-    const pageData = res.code !== undefined ? res.data : res
-    const records = pageData.records || []
-    // 为每条记录计算 pointCount(从 points 字段自动统计)
-    records.forEach(row => {
-      const pts = parsePoints(row.points)
-      row.pointCount = pts.length
-    })
-    tableData.value = records
+    const [pageRes, mapRes] = await Promise.all([
+      getDrainagePipeNetworkPage(pageNum.value, pageSize.value, params),
+      getDrainagePipeNetworkMap(params)
+    ])
+    // 分页接口直接返回 Page 对象,地图接口返回 AjaxResult
+    const pageData = pageRes && pageRes.code !== undefined ? (pageRes.data || {}) : (pageRes || {})
+    tableData.value = pageData.records || []
     total.value = pageData.total || 0
-    renderMainMapMarkers()
-    resolveEquipmentNames()
-  } catch (e) {
-    console.error('加载排水管网数据失败', e)
+    const mapData = mapRes && mapRes.code !== undefined ? (mapRes.data || []) : (mapRes || [])
+    mapRecords.value = Array.isArray(mapData) ? mapData : []
+  } catch (error) {
+    console.error('加载排水管网数据失败', error)
+    ElMessage.error('排水管网数据加载失败,请稍后重试')
+    tableData.value = []
+    mapRecords.value = []
+    total.value = 0
   } finally {
     loading.value = false
   }
 }
 
-// 关联设备名称映射
-const equipmentNameMap = ref({})
-
-async function resolveEquipmentNames() {
-  const map = {}
-  for (const row of tableData.value) {
-    if (row.equipmentId) {
-      const ids = row.equipmentId.split(',')
-      const names = []
-      for (const id of ids) {
-        try {
-          const res = await getEquipmentById(id.trim())
-          const data = res.code !== undefined ? res.data : res
-          const info = data && data.equipmentInfo ? data.equipmentInfo : data
-          if (info && info.equipmentName) names.push(info.equipmentName)
-        } catch (e) { /* skip */ }
-      }
-      map[row.id] = names.join('、')
-    }
+/** 只翻页时不动地图数据 */
+async function loadPage() {
+  loading.value = true
+  try {
+    const res = await getDrainagePipeNetworkPage(pageNum.value, pageSize.value, buildParams())
+    const pageData = res && 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
   }
-  equipmentNameMap.value = map
-}
-
-onMounted(() => {
-  loadData()
-  setTimeout(() => { initMainMap() }, 500)
-})
-
-// 多选
-const selectedIds = ref([])
-function handleSelectionChange(selection) {
-  selectedIds.value = selection.map(item => item.id)
 }
 
-// 表格数据直接用 tableData(已是分页数据)
-const filteredList = computed(() => tableData.value)
-
-// 搜索
 function handleQuery() {
   pageNum.value = 1
-  loadData()
+  loadAll()
 }
 
-// ==================== 查看详情对话框 ====================
-const dialogVisible = ref(false)
-const currentRow = ref({})
-const viewEquipmentNames = ref([])
-let viewMapInstance = null
-
-const currentRowPointCount = computed(() => {
-  return parsePoints(currentRow.value.points).length
-})
-
-async function handleView(row) {
-  currentRow.value = { ...row }
-  dialogVisible.value = true
-  viewEquipmentNames.value = []
-  if (row.equipmentId) {
-    const ids = row.equipmentId.split(',')
-    const names = []
-    for (const id of ids) {
-      try {
-        const res = await getEquipmentById(id.trim())
-        const data = res.code !== undefined ? res.data : res
-        const info = data && data.equipmentInfo ? data.equipmentInfo : data
-        if (info && info.equipmentName) names.push(info.equipmentName)
-      } catch (e) { /* skip */ }
-    }
-    viewEquipmentNames.value = names
+function handleSizeChange() {
+  pageNum.value = 1
+  loadPage()
+}
+
+function resetQuery() {
+  query.value = {
+    networkCode: '',
+    networkName: '',
+    networkLevel: '',
+    managementUnit: '',
+    material: '',
+    status: '',
+    diameterMin: null,
+    diameterMax: null
   }
+  buildRange.value = []
+  pageNum.value = 1
+  loadAll()
 }
 
-function handleClose() {
-  dialogVisible.value = false
-  if (viewMapInstance) {
-    viewMapInstance = null
+async function loadFilterOptions() {
+  try {
+    const res = await getDrainagePipeNetworkFilterOptions()
+    const data = res && res.code !== undefined ? (res.data || {}) : (res || {})
+    filterOptions.value = {
+      materials: data.materials || [],
+      managementUnits: data.managementUnits || []
+    }
+  } catch (error) {
+    // 下拉项拿不到不影响主流程,允许手输
+    console.warn('排水管网筛选下拉项加载失败', error)
   }
 }
 
-function onDialogOpened() {
-  setTimeout(() => {
-    initViewMap()
-  }, 300)
-}
+// ==================== 坐标与地图数据 ====================
+/** 单个端点坐标:非法或 (0,0) 视为无效 */
+function toCoord(lng, lat) {
+  const x = Number(lng)
+  const y = Number(lat)
+  if (!Number.isFinite(x) || !Number.isFinite(y)) return null
+  if (x === 0 && y === 0) return null
+  return { lng: x, lat: y }
+}
+
+/** 一条管网的可绘制坐标序列(起点在前、终点在后) */
+function buildRowPoints(row) {
+  if (!row) return []
+  const points = []
+  const start = toCoord(row.startLongitude, row.startLatitude)
+  const end = toCoord(row.endLongitude, row.endLatitude)
+  if (start) points.push(start)
+  if (end) points.push(end)
+  return points
+}
+
+/** 状态 → 线条颜色,和列表标签口径一致 */
+const STATUS_COLORS = { '0': '#67c23a', '1': '#e6a23c', '2': '#f56c6c' }
+
+function statusColor(value) {
+  return STATUS_COLORS[String(value)] || '#409eff'
+}
+
+const mapLines = computed(() =>
+  mapRecords.value
+    .map((row) => {
+      const points = buildRowPoints(row)
+      if (points.length < 2) return null
+      return {
+        id: row.networkId,
+        name: row.networkName || row.networkCode || '排水管线',
+        points,
+        color: statusColor(row.status),
+        strokeWeight: 5,
+        raw: row
+      }
+    })
+    .filter(Boolean)
+)
+
+const mapPoints = computed(() =>
+  mapRecords.value
+    .map((row) => {
+      const points = buildRowPoints(row)
+      if (!points.length) return null
+      return {
+        id: row.networkId,
+        name: row.networkName || row.networkCode || '起点',
+        lng: points[0].lng,
+        lat: points[0].lat,
+        raw: row
+      }
+    })
+    .filter(Boolean)
+)
+
+function fitMap() {
+  mapRef.value?.fitBounds()
+}
+
+// 地图实例就绪后再贴合视野:百度 SDK 异步加载,可能晚于接口数据返回
+function onMapReady() {
+  nextTick(() => fitMap())
+}
+
+// 数据变化后自动缩放到全部管网
+watch(
+  mapLines,
+  () => {
+    nextTick(() => fitMap())
+  },
+  { flush: 'post' }
+)
+
+// ==================== 详情 ====================
+const detailPoints = computed(() => {
+  const points = buildRowPoints(detailRow.value)
+  return points.map((point, index) => ({
+    ...point,
+    name: index === 0 ? '起点' : '终点'
+  }))
+})
+
+const detailLines = computed(() => {
+  const points = buildRowPoints(detailRow.value)
+  if (points.length < 2) return []
+  return [{ id: 'detail', name: '管段', points, color: '#409eff', strokeWeight: 6 }]
+})
 
-function initViewMap() {
-  const container = document.getElementById('psgwglViewMap')
-  if (!container) return
+/** 地图要素点击:直接打开对应管网详情 */
+function handleFeatureClick(feature) {
+  const row = feature?.raw || feature
+  if (row && row.networkId) openDetail(row)
+}
 
+async function openDetail(row) {
+  detailRow.value = { ...row }
+  detailVisible.value = true
+  // 列表字段已够用,再取一次详情兜底(失败则沿用列表数据)
   try {
-    viewMapInstance = new BMapGL.Map('psgwglViewMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    viewMapInstance.centerAndZoom(centerPoint, 15)
-    viewMapInstance.enableScrollWheelZoom(true)
-    renderViewMarkers()
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    const res = await getDrainagePipeNetworkDetail(row.networkId)
+    const data = res && res.code !== undefined ? res.data : res
+    if (data && typeof data === 'object') {
+      detailRow.value = data
+    }
+  } catch (error) {
+    console.warn('排水管网详情接口未取到数据,使用列表数据展示', error)
   }
 }
 
-function renderViewMarkers() {
-  if (!viewMapInstance) return
-  viewMapInstance.clearOverlays()
-
-  const points = parsePoints(currentRow.value.points)
-
-  points.forEach(point => {
-    const bPoint = new BMapGL.Point(point.lng, point.lat)
-    createMapMarker(viewMapInstance, bPoint, point.name)
+function onDetailOpened() {
+  nextTick(() => {
+    setTimeout(() => detailMapRef.value?.fitBounds(), 200)
   })
-
-  // 管线模式:绘制折线
-  if (currentRow.value.networkType === '管线' && points.length >= 2) {
-    const polylinePoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
-    const polyline = new BMapGL.Polyline(polylinePoints, {
-      strokeColor: '#409eff',
-      strokeWeight: 4,
-      strokeOpacity: 0.8
-    })
-    viewMapInstance.addOverlay(polyline)
-  }
-
-  if (points.length > 0) {
-    const viewPoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
-    viewMapInstance.setViewport(viewPoints)
-  }
 }
 
-// ==================== 新增/修改对话框 ====================
-const editDialogVisible = ref(false)
-const editDialogTitle = ref('新增监测点')
-const editFormRef = ref(null)
-const isEditMode = ref(false)
-const editRowId = ref(null)
-
-const editForm = ref({
-  name: '',
-  area: '',
-  networkType: '',
-  time: '',
-  pointCount: 0,
-  points: []
-})
-
-const editRules = {
-  name: [{ required: true, message: '请输入监测点名称', trigger: 'blur' }],
-  area: [{ required: true, message: '请输入地点', trigger: 'blur' }],
-  networkType: [{ required: true, message: '请选择管网类型', trigger: 'change' }],
-  time: [{ required: true, message: '请选择数据时间', trigger: 'change' }]
+function onDetailMapReady() {
+  nextTick(() => {
+    setTimeout(() => detailMapRef.value?.fitBounds(), 120)
+  })
 }
 
-// 编辑弹窗地图相关
-let editMapInstance = null
-let editPoints = ref([])
-let editMarkerCount = ref(0)
-
-// 标记类型中文名
-const editMarkerLabel = computed(() => {
-  const map = { '管线': '管线节点', '管点': '管点', '排口': '排口' }
-  return map[editForm.value.networkType] || '标记'
-})
-
-// 管网类型变更时,清空已有标记
-function onNetworkTypeChange() {
-  editPoints.value = []
-  editMarkerCount.value = 0
-  renderEditMarkers()
+// ==================== 展示辅助 ====================
+function dictItem(dictRef, value) {
+  const list = dictRef?.value || []
+  return list.find((item) => String(item.value) === String(value))
 }
 
-function handleAdd() {
-  isEditMode.value = false
-  editDialogTitle.value = '新增监测点'
-  editRowId.value = null
-  editForm.value = {
-    name: '',
-    area: '',
-    networkType: '',
-    time: '',
-    pointCount: 0,
-    points: []
-  }
-  editPoints.value = []
-  editMarkerCount.value = 0
-  editDialogVisible.value = true
+function statusLabel(value) {
+  return dictItem(statusDict, value)?.label || '-'
 }
 
-function handleEdit(row) {
-  isEditMode.value = true
-  editDialogTitle.value = '修改监测点'
-  editRowId.value = row.id
-  editForm.value = {
-    name: row.name,
-    area: row.area,
-    networkType: row.networkType,
-    time: row.time,
-    pointCount: row.pointCount || 0,
-    points: []
-  }
-  // 深拷贝标记点数据
-  const pts = parsePoints(row.points)
-  editPoints.value = pts.map(p => ({ ...p }))
-  editMarkerCount.value = editPoints.value.length
-  editDialogVisible.value = true
+function statusTagType(value) {
+  const map = { '0': 'success', '1': 'warning', '2': 'danger' }
+  return map[String(value)] || 'info'
 }
 
-function handleEditClose() {
-  editDialogVisible.value = false
-  if (editMapInstance) {
-    editMapInstance = null
-  }
+function levelLabel(value) {
+  return dictItem(levelDict, value)?.label || ''
 }
 
-function onEditDialogOpened() {
-  setTimeout(() => {
-    initEditMap()
-  }, 300)
+function levelTagType(value) {
+  const item = dictItem(levelDict, value)
+  const listClass = item?.listClass || item?.elTagType || ''
+  // Element Plus 没有 primary 类型,回落为默认样式
+  return ['success', 'warning', 'danger', 'info'].includes(listClass) ? listClass : ''
 }
 
-function initEditMap() {
-  const container = document.getElementById('psgwglEditMap')
-  if (!container) return
+function fmt(value) {
+  if (value === null || value === undefined || value === '') return '-'
+  return value
+}
 
-  try {
-    editMapInstance = new BMapGL.Map('psgwglEditMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    editMapInstance.centerAndZoom(centerPoint, 15)
-    editMapInstance.enableScrollWheelZoom(true)
-
-    // 点击地图添加标记
-    editMapInstance.addEventListener('click', function (e) {
-      addEditMarker(e.latlng)
-    })
+onMounted(() => {
+  loadFilterOptions()
+  loadAll()
+})
+</script>
 
-    // 渲染已有标记
-    renderEditMarkers()
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
+<style scoped lang="scss">
+.drainage-network-page {
+  .card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
   }
-}
 
-function addEditMarker(point) {
-  const networkType = editForm.value.networkType
-  if (!networkType) {
-    ElMessage.warning('请先选择管网类型')
-    return
+  .card-title {
+    display: flex;
+    align-items: baseline;
+    gap: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
   }
 
-  // 管点和排口只能标记一个点位,再次点击则替换位置
-  if ((networkType === '管点' || networkType === '排口') && editPoints.value.length >= 1) {
-    editPoints.value[0] = {
-      name: editPoints.value[0].name,
-      lng: point.lng,
-      lat: point.lat
-    }
-    renderEditMarkers(false)
-    return
+  .card-sub {
+    font-size: 12px;
+    font-weight: 400;
+    color: #909399;
   }
 
-  const count = ++editMarkerCount.value
-  const labelPrefix = networkType === '管线' ? '管线节点' : networkType
-
-  editPoints.value.push({
-    name: `${labelPrefix}${count}`,
-    lng: point.lng,
-    lat: point.lat
-  })
+  .card-actions {
+    display: flex;
+    gap: 8px;
+  }
 
-  // 统一重绘,保证地图与数据完全一致
-  renderEditMarkers(false)
-}
+  .map-card {
+    margin-bottom: 16px;
+  }
 
-function renderEditMarkers(shouldCenter = true) {
-  if (!editMapInstance) return
-  editMapInstance.clearOverlays()
+  .map-wrap {
+    position: relative;
+    height: 400px;
+    border-radius: 4px;
+    overflow: hidden;
+    background: #eef3f8;
 
-  const points = editPoints.value
-  if (shouldCenter) {
-    const center = points.length > 0
-      ? new BMapGL.Point(points[0].lng, points[0].lat)
-      : new BMapGL.Point(110.393, 28.452)
-    editMapInstance.centerAndZoom(center, 15)
+    :deep(.gis-map-canvas) {
+      min-height: 400px;
+    }
   }
 
-  points.forEach((point, index) => {
-    const bPoint = new BMapGL.Point(point.lng, point.lat)
-    const label = createMapMarker(editMapInstance, bPoint, point.name)
-
-    // 右键删除标记
-    label.addEventListener('rightclick', () => {
-      editPoints.value.splice(index, 1)
-      renderEditMarkers(false)
-    })
-  })
+  .map-empty {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #909399;
+    font-size: 13px;
+    background: rgba(255, 255, 255, 0.72);
+    pointer-events: none;
+  }
 
-  // 管线模式:绘制折线
-  if (editForm.value.networkType === '管线' && points.length >= 2) {
-    const polylinePoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
-    const polyline = new BMapGL.Polyline(polylinePoints, {
-      strokeColor: '#409eff',
-      strokeWeight: 4,
-      strokeOpacity: 0.8
-    })
-    editMapInstance.addOverlay(polyline)
+  .map-legend {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 14px;
+    margin-top: 10px;
+    font-size: 12px;
+    color: #606266;
   }
-}
 
-function clearEditMarkers() {
-  editPoints.value = []
-  editMarkerCount.value = 0
-  renderEditMarkers()
-}
+  .legend-title {
+    color: #909399;
+  }
 
-function handleEditSubmit() {
-  editFormRef.value.validate(async valid => {
-    if (!valid) return
-
-    const formData = {
-      name: editForm.value.name,
-      area: editForm.value.area,
-      networkType: editForm.value.networkType,
-      time: editForm.value.time,
-      pointCount: editPoints.value.length,
-      points: JSON.stringify(editPoints.value.map(p => ({ name: p.name, lng: p.lng, lat: p.lat })))
-    }
-    try {
-      if (isEditMode.value) {
-        formData.id = editRowId.value
-        const res = await updateDrainagePipeNetwork(formData)
-        if (res.code === 0 || res.code === 200) {
-          ElMessage.success('修改成功')
-        } else {
-          ElMessage.error(res.msg || '修改失败')
-          return
-        }
-      } else {
-        const res = await addDrainagePipeNetwork(formData)
-        if (res.code === 0 || res.code === 200) {
-          ElMessage.success('新增成功')
-        } else {
-          ElMessage.error(res.msg || '新增失败')
-          return
-        }
-      }
-      editDialogVisible.value = false
-      if (editMapInstance) {
-        editMapInstance = null
-      }
-      loadData()
-    } catch (e) {
-      console.error('提交失败', e)
-      ElMessage.error('操作失败')
-    }
-  })
-}
+  .legend-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
 
-// ==================== 删除功能 ====================
-function handleDelete(row) {
-  ElMessageBox.confirm(`确认删除监测点"${row.name}"吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(async () => {
-    try {
-      const res = await deleteDrainagePipeNetwork(row.id)
-      if (res.code === 0 || res.code === 200) {
-        ElMessage.success('删除成功')
-        loadData()
-      } else {
-        ElMessage.error(res.msg || '删除失败')
-      }
-    } catch (e) {
-      ElMessage.error('删除失败')
+    i {
+      display: inline-block;
+      width: 18px;
+      height: 4px;
+      border-radius: 2px;
     }
-  }).catch(() => { })
-}
+  }
 
-function handleBatchDelete() {
-  if (selectedIds.value.length === 0) return
-  ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条数据吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(async () => {
-    // 排水管网暂无批量删除接口,逐条删除
-    try {
-      await Promise.all(selectedIds.value.map(id => deleteDrainagePipeNetwork(id)))
-      selectedIds.value = []
-      ElMessage.success('删除成功')
-      loadData()
-    } catch (e) {
-      ElMessage.error('删除失败')
-    }
-  }).catch(() => { })
-}
+  .filter-card {
+    margin-bottom: 16px;
+  }
 
-// ==================== 关联设备 ====================
-const bindDialogVisible = ref(false)
-const bindLoading = ref(false)
-const bindTableData = ref([])
-const bindTotal = ref(0)
-const bindPageNum = ref(1)
-const bindPageSize = ref(10)
-const bindQueryParams = ref({ equipmentName: '', equipmentTypeId: 'drainage' })
-const selectedEquipmentIds = ref([])
-const boundEquipmentMap = ref({})
-const currentPartitionId = ref('')
-const bindTableRef = ref(null)
-
-async function handleBind(row) {
-  currentPartitionId.value = row.id
-  bindDialogVisible.value = true
-  bindPageNum.value = 1
-  bindQueryParams.value.equipmentName = ''
-  // 回显当前监测点的全部设备;其他监测点已占用的设备不可再次选择。
-  selectedEquipmentIds.value = row.equipmentId
-    ? String(row.equipmentId).split(',').map(id => id.trim()).filter(Boolean)
-    : []
-  nextTick(() => {
-    loadBindData()
-  })
-}
+  .filter-form {
+    display: flex;
+    flex-wrap: wrap;
+    row-gap: 4px;
 
-function handleBindQuery() {
-  bindPageNum.value = 1
-  loadBindData()
-}
-
-async function loadBindData() {
-  bindLoading.value = true
-  try {
-    const [equipmentResult, boundResult] = await Promise.allSettled([
-      getEquipmentPage(bindPageNum.value, bindPageSize.value, bindQueryParams.value),
-      getDrainageBoundEquipmentIds('pipeNetwork', currentPartitionId.value)
-    ])
-    if (equipmentResult.status === 'rejected') {
-      throw equipmentResult.reason
+    :deep(.el-form-item) {
+      margin-bottom: 12px;
+      margin-right: 16px;
     }
-    const res = equipmentResult.value
-    const pageData = res.code !== undefined ? res.data : res
-    bindTableData.value = pageData.records || []
-    bindTotal.value = pageData.total || 0
-    if (boundResult.status === 'fulfilled') {
-      const boundRes = boundResult.value
-      const boundData = boundRes.code !== undefined ? boundRes.data : boundRes
-      boundEquipmentMap.value = boundData && typeof boundData === 'object' ? boundData : {}
-    } else {
-      boundEquipmentMap.value = {}
-      console.warn('加载设备关联状态失败,设备列表仍正常显示', boundResult.reason)
-    }
-  } catch (e) {
-    bindTableData.value = []
-    bindTotal.value = 0
-    console.error('加载设备列表失败', e)
-  } finally {
-    bindLoading.value = false
   }
-}
 
-function closeBindDialog() {
-  bindDialogVisible.value = false
-  bindTableData.value = []
-  selectedEquipmentIds.value = []
-  boundEquipmentMap.value = {}
-}
-
-function isEquipmentBound(row) {
-  const id = String(row.equipmentId)
-  return Boolean(boundEquipmentMap.value[id]) && !selectedEquipmentIds.value.includes(id)
-}
-
-function toggleEquipment(row, checked) {
-  const id = String(row.equipmentId)
-  if (isEquipmentBound(row)) return
-  if (checked) {
-    if (!selectedEquipmentIds.value.includes(id)) selectedEquipmentIds.value.push(id)
-  } else {
-    selectedEquipmentIds.value = selectedEquipmentIds.value.filter(item => item !== id)
+  .range-input {
+    display: flex;
+    align-items: center;
+    gap: 6px;
   }
-}
 
-async function submitBind() {
-  if (selectedEquipmentIds.value.length === 0) {
-    ElMessage.warning('请选择要关联的设备')
-    return
+  .range-sep {
+    color: #c0c4cc;
   }
-  try {
-    const res = await bindPipeNetworkEquipment({
-      partitionId: currentPartitionId.value,
-      equipmentIds: selectedEquipmentIds.value
-    })
-    if (res.code === 0 || res.code === 200) {
-      ElMessage.success('关联成功')
-      closeBindDialog()
-      loadData()
-    } else {
-      ElMessage.error(res.msg || '关联失败')
+
+  .table-card {
+    .pager {
+      margin-top: 16px;
+      justify-content: flex-end;
     }
-  } catch (e) {
-    console.error('关联设备失败', e)
-    ElMessage.error('关联失败')
   }
-}
-</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;
-}
-
-.map-section {
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-  overflow: hidden;
-}
-
-.map-tabs {
-  display: flex;
-  background: #f5f7fa;
-  border-bottom: 1px solid #e4e7ed;
-}
-
-.map-tab {
-  padding: 8px 24px;
-  font-size: 14px;
-  cursor: pointer;
-  color: #606266;
-  transition: all 0.3s;
-  user-select: none;
-}
-
-.map-tab:hover {
-  color: #409eff;
-}
-
-.map-tab.active {
-  background: #409eff;
-  color: #fff;
-}
+  .text-muted {
+    color: #c0c4cc;
+  }
 
-.map-tab-action {
-  padding: 8px 24px;
-  font-size: 14px;
-  cursor: pointer;
-  color: #f56c6c;
-  transition: all 0.3s;
-  user-select: none;
-  margin-left: auto;
-}
+  .detail-desc {
+    margin-bottom: 16px;
+  }
 
-.map-tab-action:hover {
-  color: #f78989;
-  text-decoration: underline;
-}
+  .detail-map-section {
+    .detail-map-title {
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
 
-.map-container {
-  width: 100%;
-  height: 400px;
-}
+  .detail-map-wrap {
+    position: relative;
+    height: 300px;
+    border-radius: 4px;
+    overflow: hidden;
+    background: #eef3f8;
 
-.marker-count {
-  padding: 8px 16px;
-  font-size: 13px;
-  color: #909399;
-  background: #f5f7fa;
-  border-top: 1px solid #e4e7ed;
+    :deep(.gis-map-canvas) {
+      min-height: 300px;
+    }
+  }
 }
 </style>

+ 978 - 677
src/views/subSystem/drainage/jcsj/yjsdgl.vue

@@ -1,920 +1,1221 @@
 <template>
-  <div class="app-container">
-    <!-- GIS 地图展示区 -->
-    <div class="map-section" style="margin-bottom: 16px;">
-      <div class="map-tabs">
-        <div class="map-tab active">易积水点分布</div>
+  <div class="app-container waterlogging-page">
+    <!-- 易积水点分布 -->
+    <el-card shadow="never" class="map-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title">
+            <span>易积水点分布</span>
+            <span class="card-sub">按当前筛选条件叠加展示,共 {{ mapRecords.length }} 个带坐标点位</span>
+          </div>
+          <div class="card-actions">
+            <el-button size="small" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
+            <el-button size="small" :icon="Aim" @click="fitMap">全图</el-button>
+          </div>
+        </div>
+      </template>
+
+      <div class="map-wrap">
+        <GisMapCanvas ref="mapRef" :points="mapPoints" :lines="[]" :enable-clustering="true"
+          cluster-label="积水点" @ready="onMapReady" @point-click="handleMapPointClick" />
+        <div v-if="!mapPoints.length" class="map-empty">
+          {{ loading ? '正在加载…' : '当前筛选条件下没有可展示的点位(需录入经纬度)' }}
+        </div>
       </div>
-      <div id="yjsdglMainMap" class="map-container" style="height: 350px;"></div>
-    </div>
-
-    <!-- 搜索区域 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="点位名称">
-        <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
-      </el-form-item>
-      <el-form-item label="地点">
-        <el-input v-model="queryParams.area" placeholder="请输入" clearable style="width: 200px" />
-      </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="filteredList" border style="width: 100%"
-      @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="监测点名称" prop="name" min-width="160" />
-      <el-table-column label="地点" prop="area" min-width="100" />
-      <el-table-column label="数据时间" prop="time" min-width="120" />
-      <el-table-column label="易积水点数量" prop="waterloggingCount" min-width="100" />
-      <el-table-column label="积水量" prop="waterVolume" min-width="100" />
-      <el-table-column label="积水程度" min-width="120">
-        <template #default="{ row }">
-          <el-tag :type="getWaterloggingLevelType(row.waterloggingLevel)" size="small">
-            {{ getWaterloggingLevelLabel(row.waterloggingLevel) }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="关联设备" min-width="160">
-        <template #default="{ row }">
-          {{ equipmentNameMap[row.id] || '' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" width="240" align="center">
-        <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="primary" @click="handleBind(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="pageNum" v-model:page-size="pageSize" :total="total"
-      :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
-      style="margin-top: 16px; justify-content: flex-end;" @size-change="loadData" @current-change="loadData" />
-
-    <!-- 查看详情对话框 -->
-    <el-drawer v-model="dialogVisible" title="详情" size="700px" :before-close="handleClose" @opened="onDialogOpened" 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.name }}</span>
+
+      <div class="map-legend">
+        <span class="legend-title">积水程度:</span>
+        <span v-for="item in levelOptions" :key="item.value" class="legend-item">
+          <i :style="{ background: levelColor(item.value) }"></i>{{ item.label }}
+        </span>
+        <span class="legend-tip">点击地图标记可直接查看关联摄像头</span>
+      </div>
+    </el-card>
+
+    <!-- 查询条件 -->
+    <el-card shadow="never" class="filter-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title"><span>查询条件</span></div>
+        </div>
+      </template>
+      <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
+        <el-form-item label="点位编码">
+          <el-input v-model="query.pointCode" placeholder="请输入" clearable style="width: 160px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="点位名称">
+          <el-input v-model="query.pointName" placeholder="请输入" clearable style="width: 160px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="所属区域">
+          <el-input v-model="query.district" placeholder="请输入" clearable style="width: 160px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="积水程度">
+          <el-select v-model="query.waterloggingLevel" placeholder="请选择" clearable style="width: 140px">
+            <el-option v-for="item in levelOptions" :key="item.value" :label="item.label"
+              :value="Number(item.value)" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="风险等级">
+          <el-select v-model="query.riskLevel" placeholder="请选择" clearable style="width: 130px">
+            <el-option v-for="item in riskOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 点位清单 -->
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <div class="card-title"><span>点位清单</span></div>
+        </div>
+      </template>
+
+      <div class="toolbar">
+        <el-button v-hasPermi="['drainage:waterlogging:add']" type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
+        <el-button v-hasPermi="['drainage:waterlogging:remove']" type="danger" :icon="Delete"
+          :disabled="selectedIds.length === 0" @click="handleBatchDelete">
+          删除{{ selectedIds.length ? `(${selectedIds.length})` : '' }}
+        </el-button>
+      </div>
+
+      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
+        @selection-change="handleSelectionChange" @row-dblclick="openDetail">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="点位编码" prop="pointCode" min-width="110" show-overflow-tooltip />
+        <el-table-column label="点位名称" prop="pointName" min-width="140" show-overflow-tooltip />
+        <el-table-column label="所属区域" prop="district" min-width="110" show-overflow-tooltip />
+        <el-table-column label="位置描述" prop="location" min-width="140" show-overflow-tooltip />
+        <el-table-column label="积水程度" width="110" align="center">
+          <template #default="{ row }">
+            <el-tag size="small" :type="levelTagType(row.waterloggingLevel)">{{ levelLabel(row.waterloggingLevel) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="积水量(m³)" width="110" align="right">
+          <template #default="{ row }">{{ formatVolume(row.waterVolume) }}</template>
+        </el-table-column>
+        <el-table-column label="积水深度(cm)" width="115" align="right">
+          <template #default="{ row }">{{ formatNumber(row.waterDepth) }}</template>
+        </el-table-column>
+        <el-table-column label="风险等级" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag size="small" :type="riskTagType(row.riskLevel)">{{ riskLabel(row.riskLevel) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="关联摄像头" min-width="160">
+          <template #default="{ row }">
+            <template v-if="cameraListOf(row).length">
+              <el-tag v-for="cam in cameraListOf(row).slice(0, 2)" :key="cam.videoId" size="small" type="info"
+                class="camera-tag">{{ cam.deviceName }}</el-tag>
+              <span v-if="cameraListOf(row).length > 2" class="text-muted">等 {{ cameraListOf(row).length }} 个</span>
+            </template>
+            <span v-else class="text-muted">未关联</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="260" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDetail(row)">详情</el-button>
+            <el-button link type="primary" :disabled="!cameraListOf(row).length"
+              @click="playCameras(row)">视频</el-button>
+            <el-button v-hasPermi="['drainage:waterlogging:edit']" link type="primary" @click="handleEdit(row)">修改</el-button>
+            <el-button v-hasPermi="['drainage:waterlogging:edit']" link type="primary"
+              @click="openBind(row)">关联摄像头</el-button>
+            <el-button v-hasPermi="['drainage:waterlogging:remove']" link type="danger"
+              @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"
+        :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
+        class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
+    </el-card>
+
+    <!-- 详情抽屉(只读) -->
+    <el-drawer v-model="detailVisible" title="易积水点详情" size="700px" direction="rtl"
+      @opened="onDetailOpened">
+      <el-descriptions :column="2" border class="detail-desc">
+        <el-descriptions-item label="点位编码">{{ fmt(detailRow.pointCode) }}</el-descriptions-item>
+        <el-descriptions-item label="点位名称">{{ fmt(detailRow.pointName) }}</el-descriptions-item>
+        <el-descriptions-item label="所属区域">{{ fmt(detailRow.district) }}</el-descriptions-item>
+        <el-descriptions-item label="积水程度">
+          <el-tag size="small" :type="levelTagType(detailRow.waterloggingLevel)">{{ levelLabel(detailRow.waterloggingLevel) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="积水量(m³)">{{ formatVolume(detailRow.waterVolume) }}</el-descriptions-item>
+        <el-descriptions-item label="积水深度(cm)">{{ formatNumber(detailRow.waterDepth) }}</el-descriptions-item>
+        <el-descriptions-item label="积水面积(m²)">{{ formatNumber(detailRow.waterArea) }}</el-descriptions-item>
+        <el-descriptions-item label="风险等级">
+          <el-tag size="small" :type="riskTagType(detailRow.riskLevel)">{{ riskLabel(detailRow.riskLevel) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="责任单位">{{ fmt(detailRow.dutyUnit) }}</el-descriptions-item>
+        <el-descriptions-item label="责任人">{{ fmt(detailRow.dutyPerson) }}</el-descriptions-item>
+        <el-descriptions-item label="联系电话">{{ fmt(detailRow.dutyPhone) }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ fmt(detailRow.createTime) }}</el-descriptions-item>
+        <el-descriptions-item label="经度">{{ fmt(detailRow.longitude) }}</el-descriptions-item>
+        <el-descriptions-item label="纬度">{{ fmt(detailRow.latitude) }}</el-descriptions-item>
+        <el-descriptions-item label="位置描述" :span="2">{{ fmt(detailRow.location) }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ fmt(detailRow.remark) }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div class="detail-section">
+        <div class="detail-section-title">关联摄像头</div>
+        <div v-if="detailCameras.length" class="camera-cards">
+          <div v-for="cam in detailCameras" :key="cam.videoId" class="camera-card">
+            <div class="camera-card-main">
+              <div class="camera-name">{{ cam.deviceName || cam.deviceCode }}</div>
+              <div class="camera-meta">{{ cam.districtName }} · {{ cam.location || '未填写位置' }}</div>
             </div>
+            <el-button link type="primary" @click="playVideo(cam)">查看视频</el-button>
+          </div>
+        </div>
+        <el-empty v-else description="该点位尚未关联摄像头" :image-size="70" />
+      </div>
+
+      <div class="detail-section">
+        <div class="detail-section-title">点位位置</div>
+        <div class="detail-map-wrap">
+          <GisMapCanvas v-if="detailVisible" ref="detailMapRef" :points="detailPoints" :lines="[]"
+            :enable-clustering="false" :show-marker-name="false" @ready="onDetailMapReady" />
+          <div v-if="!detailPoints.length" class="map-empty">该点位未录入坐标</div>
+        </div>
+      </div>
+
+      <template #footer>
+        <el-button @click="detailVisible = false">关 闭</el-button>
+      </template>
+    </el-drawer>
+
+    <!-- 新增 / 修改 -->
+    <el-dialog v-model="formVisible" :title="formTitle" width="820px" append-to-body
+      :close-on-click-modal="false" @opened="onFormOpened" @closed="onFormClosed">
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="点位编码" prop="pointCode">
+              <el-input v-model="form.pointCode" placeholder="请输入点位编码" maxlength="64" clearable />
+            </el-form-item>
           </el-col>
           <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">地点:</span>
-              <span class="info-value">{{ currentRow.area }}</span>
-            </div>
+            <el-form-item label="点位名称" prop="pointName">
+              <el-input v-model="form.pointName" placeholder="请输入点位名称" maxlength="200" clearable />
+            </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="16">
           <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">数据时间:</span>
-              <span class="info-value">{{ currentRow.time }}</span>
-            </div>
+            <el-form-item label="所属区域" prop="district">
+              <el-select v-model="form.district" placeholder="请选择或输入" clearable filterable allow-create
+                style="width: 100%">
+                <el-option v-for="item in filterOptions.district" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
           </el-col>
           <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">易积水点数量:</span>
-              <span class="info-value">{{ currentRow.waterloggingPoints.length }}</span>
-            </div>
+            <el-form-item label="位置描述" prop="location">
+              <el-input v-model="form.location" placeholder="请输入位置描述" maxlength="500" clearable />
+            </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="16">
           <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">积水量:</span>
-              <span class="info-value">{{ currentRow.waterVolume }}</span>
-            </div>
+            <el-form-item label="积水程度" prop="waterloggingLevel">
+              <el-select v-model="form.waterloggingLevel" placeholder="请选择积水程度" style="width: 100%">
+                <el-option v-for="item in levelOptions" :key="item.value" :label="item.label"
+                  :value="Number(item.value)" />
+              </el-select>
+            </el-form-item>
           </el-col>
           <el-col :span="12">
-            <div class="info-item">
-              <span class="info-label">积水程度:</span>
-              <span class="info-value">
-                <el-tag :type="getWaterloggingLevelType(currentRow.waterloggingLevel)" size="small">
-                  {{ getWaterloggingLevelLabel(currentRow.waterloggingLevel) }}
-                </el-tag>
-              </span>
-            </div>
+            <el-form-item label="积水量(m³)" prop="waterVolume">
+              <el-input-number v-model="form.waterVolume" :min="0" :precision="2" :controls="false"
+                placeholder="请输入积水量" style="width: 100%" />
+            </el-form-item>
           </el-col>
         </el-row>
-      </div>
-
-      <!-- 关联设备 -->
-      <div v-if="viewEquipmentNames.length" class="detail-info" style="margin-top: 12px;">
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <div class="info-item">
-              <span class="info-label">关联设备:</span>
-              <span class="info-value">{{ viewEquipmentNames.join('、') }}</span>
-            </div>
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="积水深度(cm)" prop="waterDepth">
+              <el-input-number v-model="form.waterDepth" :min="0" :precision="2" :controls="false"
+                placeholder="请输入积水深度" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="积水面积(m²)" prop="waterArea">
+              <el-input-number v-model="form.waterArea" :min="0" :precision="2" :controls="false"
+                placeholder="请输入积水面积" style="width: 100%" />
+            </el-form-item>
           </el-col>
         </el-row>
-      </div>
-
-      <!-- 地图区域 -->
-      <div class="map-section">
-        <div id="yjsdglViewMap" class="map-container"></div>
-      </div>
-    </el-drawer>
-
-    <!-- 新增/修改对话框 -->
-    <el-dialog v-model="editDialogVisible" :title="editDialogTitle" width="800px" :before-close="handleEditClose"
-      @opened="onEditDialogOpened">
-      <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="100px">
-        <el-row :gutter="20">
+        <el-row :gutter="16">
           <el-col :span="12">
-            <el-form-item label="监测点名称" prop="name">
-              <el-input v-model="editForm.name" placeholder="请输入监测点名称" />
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="form.riskLevel" placeholder="请选择风险等级" clearable style="width: 100%">
+                <el-option v-for="item in riskOptions" :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="area">
-              <el-input v-model="editForm.area" placeholder="请输入地点" />
+            <el-form-item label="责任单位" prop="dutyUnit">
+              <el-input v-model="form.dutyUnit" placeholder="请输入责任单位" maxlength="200" clearable />
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="16">
           <el-col :span="12">
-            <el-form-item label="数据时间" prop="time">
-              <el-date-picker v-model="editForm.time" type="date" placeholder="请选择日期" value-format="YYYY-MM-DD"
-                style="width: 100%" />
+            <el-form-item label="责任人" prop="dutyPerson">
+              <el-input v-model="form.dutyPerson" placeholder="请输入责任人" maxlength="64" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="易积水点数量">
-              <el-input :model-value="editWaterloggingPoints.length" disabled />
+            <el-form-item label="联系电话" prop="dutyPhone">
+              <el-input v-model="form.dutyPhone" placeholder="请输入联系电话" maxlength="32" clearable />
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <el-row :gutter="16">
           <el-col :span="12">
-            <el-form-item label="积水量" prop="waterVolume">
-              <el-input v-model="editForm.waterVolume" type="number" placeholder="请输入积水量" />
+            <el-form-item label="经度" prop="longitude">
+              <el-input-number v-model="form.longitude" :precision="8" :controls="false" placeholder="地图点选或手输"
+                style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="积水程度" prop="waterloggingLevel">
-              <el-select v-model="editForm.waterloggingLevel" placeholder="请选择积水程度" style="width: 100%">
-                <el-option label="未积水" :value="0" />
-                <el-option label="轻度积水" :value="1" />
-                <el-option label="中度积水" :value="2" />
-                <el-option label="严重积水" :value="3" />
-              </el-select>
+            <el-form-item label="纬度" prop="latitude">
+              <el-input-number v-model="form.latitude" :precision="8" :controls="false" placeholder="地图点选或手输"
+                style="width: 100%" />
             </el-form-item>
           </el-col>
         </el-row>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit
+            placeholder="请输入备注" />
+        </el-form-item>
       </el-form>
 
-      <!-- 地图标点区域 -->
-      <div class="map-section" style="margin-top: 16px;">
-        <div class="map-tabs">
-          <div class="map-tab active">易积水点</div>
-          <div class="map-tab-action" @click="clearEditMarkers">清除所有标记</div>
+      <div class="form-map-section">
+        <div class="form-map-title">
+          <span>位置标注</span>
+          <span class="form-map-tip">在地图上点击可定位,坐标自动回填(内部按 WGS84 存储)</span>
         </div>
-        <div id="yjsdglEditMap" class="map-container" style="height: 350px;"></div>
-        <div class="marker-count">
-          已标记 {{ editWaterloggingPoints.length }} 个易积水点
+        <div class="form-map-wrap">
+          <GisMapCanvas v-if="formVisible" ref="formMapRef" :points="formPoints" :lines="[]"
+            :enable-clustering="false" :show-marker-name="false" @ready="onFormMapReady"
+            @map-click="handleMapPick" />
         </div>
       </div>
 
       <template #footer>
-        <el-button @click="handleEditClose">取 消</el-button>
-        <el-button type="primary" @click="handleEditSubmit">确 定</el-button>
+        <el-button @click="formVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
       </template>
     </el-dialog>
 
-    <!-- 关联设备弹框 -->
-    <el-dialog v-model="bindDialogVisible" title="关联设备" width="700px" @close="closeBindDialog">
-      <el-form :inline="true" :model="bindQueryParams" style="margin-bottom: 12px;">
-        <el-form-item label="设备名称">
-          <el-input v-model="bindQueryParams.equipmentName" placeholder="请输入设备名称" clearable />
+    <!-- 关联摄像头 -->
+    <el-dialog v-model="bindVisible" title="关联摄像头" width="780px" append-to-body :close-on-click-modal="false">
+      <div class="bind-tip">
+        当前点位:<strong>{{ bindRow.pointName }}</strong>
+        <span class="text-muted">(已选 {{ bindSelected.length }} 个)</span>
+      </div>
+      <el-form :inline="true" class="bind-filter">
+        <el-form-item label="摄像头名称">
+          <el-input v-model="bindKeyword" placeholder="请输入名称或编码" clearable style="width: 220px" />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="handleBindQuery">搜索</el-button>
+          <el-button @click="bindSelected = []">清空选择</el-button>
         </el-form-item>
       </el-form>
-      <el-table v-loading="bindLoading" :data="bindTableData" border row-key="equipmentId"
+      <el-table v-loading="bindLoading" :data="bindCandidates" border height="380" row-key="videoId"
         @selection-change="handleBindSelectionChange" ref="bindTableRef">
-        <el-table-column type="selection" width="55" align="center" :reserve-selection="true"
-          :selectable="isEquipmentSelectable" />
-        <el-table-column label="设备名称" prop="equipmentName" min-width="120" />
-        <el-table-column label="设备编号" prop="equipmentCode" min-width="120" />
-        <el-table-column label="设备类型" prop="equipmentTypeName" min-width="100" />
-        <el-table-column label="安装位置" prop="installLocation" min-width="120" />
-        <el-table-column label="关联状态" min-width="180">
+        <el-table-column type="selection" width="50" align="center" :selectable="isCameraSelectable"
+          :reserve-selection="true" />
+        <el-table-column label="摄像头名称" prop="deviceName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip />
+        <el-table-column label="所属片区" prop="districtName" width="100" />
+        <el-table-column label="安装位置" prop="location" min-width="140" show-overflow-tooltip />
+        <el-table-column label="关联状态" min-width="160">
           <template #default="{ row }">
-            <span v-if="selectedEquipmentIds.includes(String(row.equipmentId))" style="color: #67c23a;">当前选择</span>
-            <span v-else-if="boundEquipmentMap[String(row.equipmentId)]" style="color: #e6a23c;">
-              已关联:{{ boundEquipmentMap[String(row.equipmentId)] }}
-            </span>
-            <span v-else>未关联</span>
+            <span v-if="row.pointId === bindRow.pointId" class="status-self">当前点位</span>
+            <span v-else-if="row.pointId" class="status-occupied">已关联:{{ row.pointName || '其他点位' }}</span>
+            <span v-else class="text-muted">未关联</span>
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination v-model:current-page="bindPageNum" v-model:page-size="bindPageSize" :total="bindTotal"
-        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
-        style="margin-top: 12px; justify-content: flex-end;" @size-change="loadBindData" @current-change="loadBindData" />
       <template #footer>
-        <el-button @click="closeBindDialog">取 消</el-button>
-        <el-button type="primary" @click="submitBind">确 定</el-button>
+        <el-button @click="bindVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="bindSubmitting" @click="submitBind">确 定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 摄像头实时视频 -->
+    <el-dialog v-model="playerVisible" :title="playerTitle" width="760px" append-to-body destroy-on-close>
+      <div class="player-wrap">
+        <DrainageLiveTile v-if="playerVideoId" :key="playerVideoId" :video-id="playerVideoId" />
+      </div>
+      <div v-if="playerOptions.length > 1" class="player-switch">
+        <span class="text-muted">该点位共 {{ playerOptions.length }} 个摄像头:</span>
+        <el-button v-for="cam in playerOptions" :key="cam.videoId" size="small"
+          :type="cam.videoId === playerVideoId ? 'primary' : ''" @click="playerVideoId = cam.videoId">
+          {{ cam.deviceName }}
+        </el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="Yjsdgl">
-import { ref, computed, nextTick, onMounted } from 'vue'
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import locationIcon from '@/assets/images/location.png'
+import { Aim, Delete, Plus, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
+import DrainageLiveTile from '@/views/subSystem/drainage/spjc/DrainageLiveTile.vue'
+import { useDict } from '@/utils/dict'
+import { bd09ToWgs84 } from '@/utils/coordTransform'
+import { getDrainageVideoMap } from '@/api/drainageVideo'
 import {
   getWaterloggingPage,
+  getWaterloggingMap,
+  getWaterloggingDetail,
+  getWaterloggingFilterOptions,
   addWaterlogging,
   updateWaterlogging,
   deleteWaterlogging,
-  getEquipmentPage,
-  getEquipmentById,
-  bindWaterloggingEquipment,
-  getDrainageBoundEquipmentIds
+  batchDeleteWaterlogging,
+  bindWaterloggingCameras
 } from '@/api/drainage'
 
-// 地图标记创建(用Label+img代替Marker)
-function createMapMarker(map, bPoint, name) {
-  const html = `<div style="text-align:center;cursor:pointer;">
-    <div style="color:#fff;background:#409eff;border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${name || ''}</div>
-    <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
-  </div>`
-  const label = new BMapGL.Label(html, {
-    position: bPoint,
-    offset: new BMapGL.Size(-30, -50)
-  })
-  label.setStyle({
-    border: 'none',
-    background: 'transparent',
-    padding: '0'
-  })
-  map.addOverlay(label)
-  return label
-}
+const { drainage_waterlogging_level: levelDict, drainage_waterlogging_risk_level: riskDict } =
+  useDict('drainage_waterlogging_level', 'drainage_waterlogging_risk_level')
 
-// 积水程度选项
-const waterloggingLevelOptions = [
-  { value: 0, label: '未积水', type: 'success' },
-  { value: 1, label: '轻度积水', type: 'info' },
-  { value: 2, label: '中度积水', type: 'warning' },
-  { value: 3, label: '严重积水', type: 'danger' }
+/** 积水程度兜底(字典未配置时页面仍可用) */
+const LEVEL_FALLBACK = [
+  { value: '0', label: '未积水' },
+  { value: '1', label: '轻度积水' },
+  { value: '2', label: '中度积水' },
+  { value: '3', label: '严重积水' }
 ]
+const levelOptions = computed(() => (levelDict.value && levelDict.value.length ? levelDict.value : LEVEL_FALLBACK))
 
-function getWaterloggingLevelLabel(level) {
-  const option = waterloggingLevelOptions.find(item => item.value === level)
-  return option ? option.label : '未知'
-}
-
-function getWaterloggingLevelType(level) {
-  const option = waterloggingLevelOptions.find(item => item.value === level)
-  return option ? option.type : 'info'
-}
-
-// 安全解析 waterloggingPoints(后端返回 JSON 字符串,需转为数组使用)
-function parseWaterloggingPoints(raw) {
-  if (!raw) return []
-  if (Array.isArray(raw)) return raw
-  if (typeof raw === 'string') {
-    try {
-      const parsed = JSON.parse(raw)
-      return Array.isArray(parsed) ? parsed : []
-    } catch (e) {
-      console.warn('waterloggingPoints JSON 解析失败:', raw, e)
-      return []
-    }
-  }
-  return []
-}
-
-// ==================== 搜索参数 ====================
-const queryParams = ref({
-  name: '',
-  area: ''
+/** 风险等级兜底 */
+const RISK_FALLBACK = [
+  { value: '0', label: '低风险' },
+  { value: '1', label: '中风险' },
+  { value: '2', label: '高风险' }
+]
+const riskOptions = computed(() => (riskDict.value && riskDict.value.length ? riskDict.value : RISK_FALLBACK))
+
+/** 图例色块,与列表标签语义保持一致 */
+const LEVEL_COLORS = { '0': '#67c23a', '1': '#409eff', '2': '#e6a23c', '3': '#f56c6c' }
+
+// ==================== 查询条件 ====================
+const emptyQuery = () => ({
+  pointCode: '',
+  pointName: '',
+  district: '',
+  waterloggingLevel: null,
+  riskLevel: ''
 })
+const query = ref(emptyQuery())
+const filterOptions = ref({ district: [] })
 
-// ==================== 表格数据 ====================
+// ==================== 列表 ====================
 const tableData = ref([])
 const total = ref(0)
 const pageNum = ref(1)
 const pageSize = ref(10)
 const loading = ref(false)
+const selectedIds = ref([])
 
-// 加载分页数据
-async function loadData() {
-  loading.value = true
+// ==================== 地图 ====================
+const mapRecords = ref([])
+const mapRef = ref(null)
+
+// ==================== 摄像头资源 ====================
+/** 全部视频设备,用于把 cameraIds 翻译成名称、以及关联弹窗候选 */
+const videoResources = ref([])
+const videoMap = ref({})
+
+async function loadVideoResources() {
   try {
-    const res = await getWaterloggingPage(pageNum.value, pageSize.value, queryParams.value)
-    // 兼容直接返回Page对象(无code包装)和AjaxResult包装两种格式
-    const pageData = res.code !== undefined ? res.data : res
-    tableData.value = pageData.records || []
-    total.value = pageData.total || 0
-    renderMainMapMarkers()
-    resolveEquipmentNames()
-  } catch (e) {
-    console.error('加载易积水点数据失败', e)
-  } finally {
-    loading.value = false
+    const res = await getDrainageVideoMap()
+    const data = res && res.code !== undefined ? (res.data || []) : (res || [])
+    videoResources.value = Array.isArray(data) ? data : []
+    const map = {}
+    videoResources.value.forEach((item) => {
+      map[String(item.videoId)] = item
+    })
+    videoMap.value = map
+  } catch (error) {
+    // 摄像头资源拿不到不影响点位本身展示,只是名称退化为 ID
+    console.warn('视频资源加载失败', error)
+    videoResources.value = []
+    videoMap.value = {}
   }
 }
 
-// 关联设备名称映射
-const equipmentNameMap = ref({})
-
-async function resolveEquipmentNames() {
-  const map = {}
-  for (const row of tableData.value) {
-    if (row.equipmentId) {
-      const ids = row.equipmentId.split(',')
-      const names = []
-      for (const id of ids) {
-        try {
-          const res = await getEquipmentById(id.trim())
-          const data = res.code !== undefined ? res.data : res
-          const info = data && data.equipmentInfo ? data.equipmentInfo : data
-          if (info && info.equipmentName) names.push(info.equipmentName)
-        } catch (e) { /* skip */ }
-      }
-      map[row.id] = names.join('、')
-    }
-  }
-  equipmentNameMap.value = map
+function parseCameraIds(value) {
+  if (!value) return []
+  return String(value).split(',').map((id) => id.trim()).filter(Boolean)
 }
 
-onMounted(() => {
-  loadData()
-  setTimeout(() => { initMainMap() }, 500)
-})
+/** 某点位关联的摄像头对象列表(找不到资源时用 ID 兜底) */
+function cameraListOf(row) {
+  return parseCameraIds(row?.cameraIds).map((id) => videoMap.value[id] || { videoId: id, deviceName: id })
+}
 
-// ==================== 顶部主地图 ====================
-let mainMapInstance = null
+// ==================== 详情 ====================
+const detailVisible = ref(false)
+const detailRow = ref({})
+const detailMapRef = ref(null)
 
-function initMainMap() {
-  const container = document.getElementById('yjsdglMainMap')
-  if (!container) return
-  try {
-    mainMapInstance = new BMapGL.Map('yjsdglMainMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    mainMapInstance.centerAndZoom(centerPoint, 12)
-    mainMapInstance.enableScrollWheelZoom(true)
-    renderMainMapMarkers()
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
-  }
-}
+const detailCameras = computed(() => cameraListOf(detailRow.value))
 
-function renderMainMapMarkers() {
-  if (!mainMapInstance) return
-  mainMapInstance.clearOverlays()
+// ==================== 视频播放 ====================
+const playerVisible = ref(false)
+const playerVideoId = ref('')
+const playerTitle = ref('')
+const playerOptions = ref([])
 
-  const allPoints = []
-  tableData.value.forEach(row => {
-    const points = parseWaterloggingPoints(row.waterloggingPoints)
-    points.forEach(p => {
-      if (p.lng && p.lat) {
-        const bPoint = new BMapGL.Point(p.lng, p.lat)
-        createMapMarker(mainMapInstance, bPoint, p.name || row.name)
-        allPoints.push(bPoint)
-      }
-    })
-  })
+function playVideo(camera) {
+  playerOptions.value = [camera]
+  playerVideoId.value = camera.videoId
+  playerTitle.value = camera.deviceName || '实时视频'
+  playerVisible.value = true
+}
 
-  if (allPoints.length > 0) {
-    mainMapInstance.setViewport(allPoints)
+/** 列表/地图点击摄像头入口:有多个时弹窗内切换 */
+function playCameras(row) {
+  const cameras = cameraListOf(row)
+  if (!cameras.length) {
+    ElMessage.warning('该点位尚未关联摄像头')
+    return
   }
+  playerOptions.value = cameras
+  playerVideoId.value = cameras[0].videoId
+  playerTitle.value = `${row.pointName || '易积水点'} · 实时视频`
+  playerVisible.value = true
 }
 
-// 多选
-const selectedIds = ref([])
-function handleSelectionChange(selection) {
-  selectedIds.value = selection.map(item => item.id)
+// ==================== 关联摄像头 ====================
+const bindVisible = ref(false)
+const bindLoading = ref(false)
+const bindSubmitting = ref(false)
+const bindRow = ref({})
+const bindKeyword = ref('')
+const bindSelected = ref([])
+const bindTableRef = ref(null)
+
+const bindCandidates = computed(() => {
+  const keyword = bindKeyword.value.trim().toLowerCase()
+  if (!keyword) return videoResources.value
+  return videoResources.value.filter((item) =>
+    `${item.deviceName || ''} ${item.deviceCode || ''}`.toLowerCase().includes(keyword)
+  )
+})
+
+function isCameraSelectable(row) {
+  // 只允许选择「未关联」或「已关联到当前点位」的摄像头
+  return !row.pointId || row.pointId === bindRow.value.pointId
 }
 
-// 表格数据直接用 tableData(已是分页数据)
-const filteredList = computed(() => tableData.value)
+function openBind(row) {
+  bindRow.value = { ...row }
+  bindKeyword.value = ''
+  bindSelected.value = parseCameraIds(row.cameraIds)
+  bindVisible.value = true
+  nextTick(() => {
+    syncBindSelection()
+  })
+}
 
-// 搜索
-function handleQuery() {
-  pageNum.value = 1
-  loadData()
+function syncBindSelection() {
+  if (!bindTableRef.value) return
+  bindTableRef.value.clearSelection()
+  bindCandidates.value.forEach((item) => {
+    if (bindSelected.value.includes(String(item.videoId))) {
+      bindTableRef.value.toggleRowSelection(item, true)
+    }
+  })
 }
 
-// ==================== 查看详情对话框 ====================
-const dialogVisible = ref(false)
-const currentRow = ref({})
-const viewEquipmentNames = ref([])
-let viewMapInstance = null
+function handleBindSelectionChange(selection) {
+  bindSelected.value = selection.map((item) => String(item.videoId))
+}
 
-async function handleView(row) {
-  currentRow.value = {
-    ...row,
-    // 将 JSON 字符串解析为数组,供模板和地图渲染使用
-    waterloggingPoints: parseWaterloggingPoints(row.waterloggingPoints)
-  }
-  dialogVisible.value = true
-  viewEquipmentNames.value = []
-  if (row.equipmentId) {
-    const ids = row.equipmentId.split(',')
-    const names = []
-    for (const id of ids) {
-      try {
-        const res = await getEquipmentById(id.trim())
-        const data = res.code !== undefined ? res.data : res
-        const info = data && data.equipmentInfo ? data.equipmentInfo : data
-        if (info && info.equipmentName) names.push(info.equipmentName)
-      } catch (e) { /* skip */ }
+async function submitBind() {
+  if (!bindRow.value.pointId) return
+  bindSubmitting.value = true
+  try {
+    const res = await bindWaterloggingCameras(bindRow.value.pointId, bindSelected.value)
+    if (res && (res.code === 0 || res.code === 200)) {
+      ElMessage.success('关联成功')
+      bindVisible.value = false
+      await Promise.all([loadAll(), loadVideoResources()])
+    } else {
+      ElMessage.error(res?.msg || '关联失败')
     }
-    viewEquipmentNames.value = names
+  } catch (error) {
+    console.error('关联摄像头失败', error)
+    ElMessage.error('关联失败')
+  } finally {
+    bindSubmitting.value = false
   }
 }
 
-function handleClose() {
-  dialogVisible.value = false
-  if (viewMapInstance) {
-    viewMapInstance = null
-  }
+// ==================== 数据加载 ====================
+function buildParams() {
+  const params = {}
+  Object.keys(query.value).forEach((key) => {
+    const value = query.value[key]
+    if (value !== '' && value !== null && value !== undefined) {
+      params[key] = value
+    }
+  })
+  return params
 }
 
-function onDialogOpened() {
-  setTimeout(() => {
-    initViewMap()
-  }, 300)
+async function loadAll() {
+  loading.value = true
+  const params = buildParams()
+  try {
+    const [pageRes, mapRes] = await Promise.all([
+      getWaterloggingPage(pageNum.value, pageSize.value, params),
+      getWaterloggingMap(params)
+    ])
+    const pageData = pageRes && pageRes.code !== undefined ? (pageRes.data || {}) : (pageRes || {})
+    tableData.value = pageData.records || []
+    total.value = pageData.total || 0
+    const mapData = mapRes && mapRes.code !== undefined ? (mapRes.data || []) : (mapRes || [])
+    mapRecords.value = Array.isArray(mapData) ? mapData : []
+  } catch (error) {
+    console.error('加载易积水点数据失败', error)
+    ElMessage.error('易积水点数据加载失败,请稍后重试')
+    tableData.value = []
+    mapRecords.value = []
+    total.value = 0
+  } finally {
+    loading.value = false
+  }
 }
 
-function initViewMap() {
-  const container = document.getElementById('yjsdglViewMap')
-  if (!container) return
+async function loadPage() {
+  loading.value = true
+  try {
+    const res = await getWaterloggingPage(pageNum.value, pageSize.value, buildParams())
+    const pageData = res && 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
+  }
+}
 
+async function loadFilterOptions() {
   try {
-    viewMapInstance = new BMapGL.Map('yjsdglViewMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    viewMapInstance.centerAndZoom(centerPoint, 15)
-    viewMapInstance.enableScrollWheelZoom(true)
-    renderViewMarkers()
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    const res = await getWaterloggingFilterOptions()
+    const data = res && res.code !== undefined ? (res.data || {}) : (res || {})
+    filterOptions.value = { district: data.district || [] }
+  } catch (error) {
+    console.warn('易积水点筛选下拉项加载失败', error)
   }
 }
 
-function renderViewMarkers() {
-  if (!viewMapInstance) return
-  viewMapInstance.clearOverlays()
+function handleQuery() {
+  pageNum.value = 1
+  loadAll()
+}
 
-  const points = currentRow.value.waterloggingPoints || []
+function handleSizeChange() {
+  pageNum.value = 1
+  loadPage()
+}
 
-  points.forEach(point => {
-    const bPoint = new BMapGL.Point(point.lng, point.lat)
-    createMapMarker(viewMapInstance, bPoint, point.name)
-  })
+function resetQuery() {
+  query.value = emptyQuery()
+  pageNum.value = 1
+  loadAll()
+}
 
-  if (points.length > 0) {
-    const viewPoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
-    viewMapInstance.setViewport(viewPoints)
-  }
+// ==================== 地图数据 ====================
+function toCoord(lng, lat) {
+  const x = Number(lng)
+  const y = Number(lat)
+  if (!Number.isFinite(x) || !Number.isFinite(y)) return null
+  if (x === 0 && y === 0) return null
+  return { lng: x, lat: y }
 }
 
-// ==================== 新增/修改对话框 ====================
-const editDialogVisible = ref(false)
-const editDialogTitle = ref('新增监测点')
-const editFormRef = ref(null)
-const isEditMode = ref(false)
-const editRowId = ref(null)
+const mapPoints = computed(() =>
+  mapRecords.value
+    .map((row) => {
+      const coord = toCoord(row.longitude, row.latitude)
+      if (!coord) return null
+      return {
+        id: row.pointId,
+        name: row.pointName || row.pointCode || '积水点',
+        ...coord,
+        statusKey: levelStatusKey(row.waterloggingLevel),
+        raw: row
+      }
+    })
+    .filter(Boolean)
+)
 
-const editForm = ref({
-  name: '',
-  area: '',
-  time: '',
-  waterVolume: null,
-  waterloggingLevel: 0
-})
+/** 积水程度 → 地图图标状态(复用 gisMapColors 的状态色) */
+const LEVEL_STATUS_KEY = { '0': 'normal', '1': 'workOrder', '2': 'warning', '3': 'alarm' }
 
-const editRules = {
-  name: [{ required: true, message: '请输入监测点名称', trigger: 'blur' }],
-  area: [{ required: true, message: '请输入地点', trigger: 'blur' }],
-  time: [{ required: true, message: '请选择数据时间', trigger: 'change' }]
+function levelStatusKey(level) {
+  return LEVEL_STATUS_KEY[String(level)] || 'normal'
 }
 
-// 编辑弹窗地图相关
-let editMapInstance = null
-let editWaterloggingPoints = ref([])
-let editWaterloggingMarkerCount = ref(0)
-
-function handleAdd() {
-  isEditMode.value = false
-  editDialogTitle.value = '新增监测点'
-  editRowId.value = null
-  editForm.value = {
-    name: '',
-    area: '',
-    time: '',
-    waterVolume: null,
-    waterloggingLevel: 0
-  }
-  editWaterloggingPoints.value = []
-  editWaterloggingMarkerCount.value = 0
-  editDialogVisible.value = true
+function levelColor(level) {
+  return LEVEL_COLORS[String(level)] || '#909399'
 }
 
-function handleEdit(row) {
-  isEditMode.value = true
-  editDialogTitle.value = '修改监测点'
-  editRowId.value = row.id
-  editForm.value = {
-    name: row.name,
-    area: row.area,
-    time: row.time,
-    waterVolume: row.waterVolume,
-    waterloggingLevel: row.waterloggingLevel ?? 0
-  }
-  // 深拷贝标记点数据(waterloggingPoints 从后端返回为 JSON 字符串,需先解析为数组)
-  editWaterloggingPoints.value = parseWaterloggingPoints(row.waterloggingPoints).map(p => ({ ...p }))
-  editWaterloggingMarkerCount.value = editWaterloggingPoints.value.length
-  editDialogVisible.value = true
+function fitMap() {
+  mapRef.value?.fitBounds()
 }
 
-function handleEditClose() {
-  editDialogVisible.value = false
-  if (editMapInstance) {
-    editMapInstance = null
-  }
+function onMapReady() {
+  nextTick(() => fitMap())
 }
 
-function onEditDialogOpened() {
-  setTimeout(() => {
-    initEditMap()
-  }, 300)
+watch(
+  mapPoints,
+  () => {
+    nextTick(() => fitMap())
+  },
+  { flush: 'post' }
+)
+
+function handleMapPointClick(point) {
+  const row = point?.raw || point
+  if (!row) return
+  if (cameraListOf(row).length) {
+    playCameras(row)
+  } else {
+    ElMessage.info('该点位尚未关联摄像头')
+    openDetail(row)
+  }
 }
 
-function initEditMap() {
-  const container = document.getElementById('yjsdglEditMap')
-  if (!container) return
+// ==================== 详情操作 ====================
+const detailPoints = computed(() => {
+  const coord = toCoord(detailRow.value.longitude, detailRow.value.latitude)
+  if (!coord) return []
+  return [{
+    id: detailRow.value.pointId,
+    name: detailRow.value.pointName || '积水点',
+    ...coord,
+    statusKey: levelStatusKey(detailRow.value.waterloggingLevel)
+  }]
+})
 
+async function openDetail(row) {
+  detailRow.value = { ...row }
+  detailVisible.value = true
   try {
-    editMapInstance = new BMapGL.Map('yjsdglEditMap')
-    const centerPoint = new BMapGL.Point(110.393, 28.452)
-    editMapInstance.centerAndZoom(centerPoint, 15)
-    editMapInstance.enableScrollWheelZoom(true)
-
-    // 点击地图添加标记
-    editMapInstance.addEventListener('click', function (e) {
-      addEditMarker(e.latlng)
-    })
-
-    // 渲染已有标记
-    renderEditMarkers()
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
+    const res = await getWaterloggingDetail(row.pointId)
+    const data = res && res.code !== undefined ? res.data : res
+    if (data && typeof data === 'object') {
+      detailRow.value = data
+    }
+  } catch (error) {
+    console.warn('易积水点详情接口未取到数据,使用列表数据展示', error)
   }
 }
 
-function addEditMarker(point) {
-  // 只能标记一个点位,再次点击重新定位
-  if (editWaterloggingPoints.value.length >= 1) {
-    editWaterloggingPoints.value[0] = {
-      name: editWaterloggingPoints.value[0].name,
-      lng: point.lng,
-      lat: point.lat
-    }
-    renderEditMarkers(false)
-    return
-  }
+function onDetailOpened() {
+  nextTick(() => {
+    setTimeout(() => detailMapRef.value?.fitBounds(), 200)
+  })
+}
 
-  const count = ++editWaterloggingMarkerCount.value
-  editWaterloggingPoints.value.push({
-    name: `易积水点${count}`,
-    lng: point.lng,
-    lat: point.lat
+function onDetailMapReady() {
+  nextTick(() => {
+    setTimeout(() => detailMapRef.value?.fitBounds(), 120)
   })
+}
 
-  // 统一重绘,保证地图与数据完全一致
-  renderEditMarkers(false)
+// ==================== 新增 / 修改 ====================
+const formVisible = ref(false)
+const formTitle = ref('新增易积水点')
+const formRef = ref(null)
+const submitting = ref(false)
+const editingId = ref('')
+const formMapRef = ref(null)
+
+const emptyForm = () => ({
+  pointId: '',
+  pointCode: '',
+  pointName: '',
+  district: '',
+  location: '',
+  longitude: null,
+  latitude: null,
+  waterloggingLevel: 0,
+  waterDepth: null,
+  waterVolume: null,
+  waterArea: null,
+  riskLevel: '',
+  dutyUnit: '',
+  dutyPerson: '',
+  dutyPhone: '',
+  remark: ''
+})
+const form = ref(emptyForm())
+
+const formRules = {
+  pointCode: [{ required: true, message: '请输入点位编码', trigger: 'blur' }],
+  pointName: [{ required: true, message: '请输入点位名称', trigger: 'blur' }],
+  waterloggingLevel: [{ required: true, message: '请选择积水程度', trigger: 'change' }]
 }
 
-function renderEditMarkers(shouldCenter = true) {
-  if (!editMapInstance) return
-  editMapInstance.clearOverlays()
+const formPoints = computed(() => {
+  const coord = toCoord(form.value.longitude, form.value.latitude)
+  if (!coord) return []
+  return [{
+    id: 'form',
+    name: form.value.pointName || '待保存点位',
+    ...coord,
+    statusKey: levelStatusKey(form.value.waterloggingLevel)
+  }]
+})
 
-  const points = editWaterloggingPoints.value
-  if (shouldCenter) {
-    const center = points.length > 0
-      ? new BMapGL.Point(points[0].lng, points[0].lat)
-      : new BMapGL.Point(110.393, 28.452)
-    editMapInstance.centerAndZoom(center, 15)
-  }
+function handleAdd() {
+  editingId.value = ''
+  formTitle.value = '新增易积水点'
+  form.value = emptyForm()
+  formVisible.value = true
+}
 
-  points.forEach((point, index) => {
-    const bPoint = new BMapGL.Point(point.lng, point.lat)
-    const label = createMapMarker(editMapInstance, bPoint, point.name)
+function handleEdit(row) {
+  editingId.value = row.pointId
+  formTitle.value = '修改易积水点'
+  form.value = {
+    ...emptyForm(),
+    ...row,
+    waterloggingLevel: row.waterloggingLevel === null || row.waterloggingLevel === undefined
+      ? 0 : Number(row.waterloggingLevel),
+    longitude: row.longitude === null || row.longitude === undefined ? null : Number(row.longitude),
+    latitude: row.latitude === null || row.latitude === undefined ? null : Number(row.latitude),
+    waterDepth: row.waterDepth === null || row.waterDepth === undefined ? null : Number(row.waterDepth),
+    waterVolume: row.waterVolume === null || row.waterVolume === undefined ? null : Number(row.waterVolume),
+    waterArea: row.waterArea === null || row.waterArea === undefined ? null : Number(row.waterArea)
+  }
+  formVisible.value = true
+}
 
-    // 右键删除标记
-    label.addEventListener('rightclick', () => {
-      editWaterloggingPoints.value.splice(index, 1)
-      renderEditMarkers(false)
-    })
+function onFormOpened() {
+  nextTick(() => {
+    setTimeout(() => formMapRef.value?.fitBounds(), 200)
   })
 }
 
-function clearEditMarkers() {
-  editWaterloggingPoints.value = []
-  editWaterloggingMarkerCount.value = 0
-  renderEditMarkers()
+function onFormClosed() {
+  formRef.value?.resetFields()
+  form.value = emptyForm()
+  editingId.value = ''
 }
 
-function handleEditSubmit() {
-  editFormRef.value.validate(async valid => {
-    if (!valid) return
+function onFormMapReady() {
+  nextTick(() => {
+    setTimeout(() => formMapRef.value?.fitBounds(), 120)
+  })
+}
 
-    const formData = {
-      name: editForm.value.name,
-      area: editForm.value.area,
-      time: editForm.value.time,
-      waterVolume: editForm.value.waterVolume,
-      waterloggingLevel: editForm.value.waterloggingLevel,
-      waterloggingCount: editWaterloggingPoints.value.length,
-      waterloggingPoints: JSON.stringify(editWaterloggingPoints.value.map(p => ({ name: p.name, lng: p.lng, lat: p.lat })))
-    }
+/** 地图点选:百度坐标 → WGS84 后回填,与后端存储口径一致 */
+function handleMapPick(point) {
+  const lng = Number(point?.lng)
+  const lat = Number(point?.lat)
+  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+  const [wgsLng, wgsLat] = bd09ToWgs84(lng, lat)
+  form.value.longitude = Number(wgsLng.toFixed(8))
+  form.value.latitude = Number(wgsLat.toFixed(8))
+}
+
+function buildPayload() {
+  const source = form.value
+  return {
+    pointCode: source.pointCode,
+    pointName: source.pointName,
+    district: source.district || null,
+    location: source.location || null,
+    longitude: source.longitude,
+    latitude: source.latitude,
+    waterloggingLevel: source.waterloggingLevel,
+    waterDepth: source.waterDepth,
+    waterVolume: source.waterVolume,
+    waterArea: source.waterArea,
+    riskLevel: source.riskLevel || null,
+    dutyUnit: source.dutyUnit || null,
+    dutyPerson: source.dutyPerson || null,
+    dutyPhone: source.dutyPhone || null,
+    remark: source.remark || null
+  }
+}
 
+function submitForm() {
+  formRef.value?.validate(async (valid) => {
+    if (!valid) return
+    submitting.value = true
     try {
-      if (isEditMode.value) {
-        formData.id = editRowId.value
-        const res = await updateWaterlogging(formData)
-        if (res.code === 0 || res.code === 200) {
-          ElMessage.success('修改成功')
-        } else {
-          ElMessage.error(res.msg || '修改失败')
-          return
-        }
+      const payload = buildPayload()
+      const res = editingId.value
+        ? await updateWaterlogging({ ...payload, pointId: editingId.value })
+        : await addWaterlogging(payload)
+      if (res && (res.code === 0 || res.code === 200)) {
+        ElMessage.success(editingId.value ? '修改成功' : '新增成功')
+        formVisible.value = false
+        await Promise.all([loadAll(), loadFilterOptions()])
       } else {
-        const res = await addWaterlogging(formData)
-        if (res.code === 0 || res.code === 200) {
-          ElMessage.success('新增成功')
-        } else {
-          ElMessage.error(res.msg || '新增失败')
-          return
-        }
-      }
-      editDialogVisible.value = false
-      if (editMapInstance) {
-        editMapInstance = null
+        ElMessage.error(res?.msg || (editingId.value ? '修改失败' : '新增失败'))
       }
-      loadData()
-    } catch (e) {
-      console.error('提交失败', e)
+    } catch (error) {
+      console.error('保存易积水点失败', error)
       ElMessage.error('操作失败')
+    } finally {
+      submitting.value = false
     }
   })
 }
 
-// ==================== 删除功能 ====================
+// ==================== 删除 ====================
+function handleSelectionChange(selection) {
+  selectedIds.value = selection.map((item) => item.pointId)
+}
+
 function handleDelete(row) {
-  ElMessageBox.confirm(`确认删除监测点"${row.name}"吗?`, '提示', {
+  ElMessageBox.confirm(`确认删除易积水点「${row.pointName}」吗?`, '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
   }).then(async () => {
     try {
-      const res = await deleteWaterlogging(row.id)
-      if (res.code === 0 || res.code === 200) {
+      const res = await deleteWaterlogging(row.pointId)
+      if (res && (res.code === 0 || res.code === 200)) {
         ElMessage.success('删除成功')
-        loadData()
+        if (tableData.value.length === 1 && pageNum.value > 1) pageNum.value -= 1
+        await loadAll()
       } else {
-        ElMessage.error(res.msg || '删除失败')
+        ElMessage.error(res?.msg || '删除失败')
       }
-    } catch (e) {
+    } catch (error) {
+      console.error('删除易积水点失败', error)
       ElMessage.error('删除失败')
     }
-  }).catch(() => { })
+  }).catch(() => {})
 }
 
 function handleBatchDelete() {
-  if (selectedIds.value.length === 0) return
-  ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条数据吗?`, '提示', {
+  const ids = [...selectedIds.value]
+  if (!ids.length) return
+  ElMessageBox.confirm(`确认删除选中的 ${ids.length} 个点位吗?`, '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
   }).then(async () => {
     try {
-      await Promise.all(selectedIds.value.map(id => deleteWaterlogging(id)))
-      selectedIds.value = []
+      const res = await batchDeleteWaterlogging(ids)
+      if (!res || (res.code !== 0 && res.code !== 200)) {
+        ElMessage.error(res?.msg || '删除失败')
+        return
+      }
       ElMessage.success('删除成功')
-      loadData()
-    } catch (e) {
+      selectedIds.value = []
+      if (tableData.value.length === ids.length && pageNum.value > 1) pageNum.value -= 1
+      await loadAll()
+    } catch (error) {
+      console.error('批量删除易积水点失败', error)
       ElMessage.error('删除失败')
     }
-  }).catch(() => { })
+  }).catch(() => {})
 }
 
-// ==================== 关联设备 ====================
-const bindDialogVisible = ref(false)
-const bindLoading = ref(false)
-const bindTableData = ref([])
-const bindTotal = ref(0)
-const bindPageNum = ref(1)
-const bindPageSize = ref(10)
-const bindQueryParams = ref({ equipmentName: '', equipmentTypeId: 'drainage' })
-const selectedEquipmentIds = ref([])
-const currentPartitionId = ref('')
-const bindTableRef = ref(null)
-const boundEquipmentMap = ref({})
-
-function handleBind(row) {
-  currentPartitionId.value = row.id
-  bindDialogVisible.value = true
-  bindPageNum.value = 1
-  bindQueryParams.value.equipmentName = ''
-  if (row.equipmentId) {
-    selectedEquipmentIds.value = row.equipmentId.split(',').map(id => id.trim()).filter(Boolean)
-  } else {
-    selectedEquipmentIds.value = []
-  }
-  nextTick(() => {
-    loadBindData()
-  })
+// ==================== 展示辅助 ====================
+function dictItem(dictRef, value) {
+  const list = dictRef?.value || []
+  return list.find((item) => String(item.value) === String(value))
 }
 
-function handleBindQuery() {
-  bindPageNum.value = 1
-  loadBindData()
+function levelLabel(value) {
+  if (value === null || value === undefined || value === '') return '-'
+  const fallback = LEVEL_FALLBACK.find((item) => item.value === String(value))
+  return dictItem(levelDict, value)?.label || fallback?.label || '-'
 }
 
-async function loadBindData() {
-  bindLoading.value = true
-  try {
-    const [equipmentResult, boundResult] = await Promise.allSettled([
-      getEquipmentPage(bindPageNum.value, bindPageSize.value, bindQueryParams.value),
-      getDrainageBoundEquipmentIds('waterlogging', currentPartitionId.value)
-    ])
-    if (equipmentResult.status === 'rejected') {
-      throw equipmentResult.reason
-    }
-    const res = equipmentResult.value
-    const pageData = res.code !== undefined ? res.data : res
-    bindTableData.value = pageData.records || []
-    bindTotal.value = pageData.total || 0
-    if (boundResult.status === 'fulfilled') {
-      const boundRes = boundResult.value
-      const boundData = boundRes.code !== undefined ? boundRes.data : boundRes
-      boundEquipmentMap.value = boundData && typeof boundData === 'object' ? boundData : {}
-    } else {
-      boundEquipmentMap.value = {}
-      console.warn('加载设备关联状态失败,设备列表仍正常显示', boundResult.reason)
-    }
-    await nextTick()
-    if (selectedEquipmentIds.value.length > 0 && bindTableRef.value) {
-      bindTableData.value.forEach(row => {
-        if (selectedEquipmentIds.value.includes(row.equipmentId)) {
-          bindTableRef.value.toggleRowSelection(row, true)
-        }
-      })
-    }
-  } catch (e) {
-    bindTableData.value = []
-    bindTotal.value = 0
-    console.error('加载设备列表失败', e)
-  } finally {
-    bindLoading.value = false
-  }
+function levelTagType(value) {
+  const item = dictItem(levelDict, value)
+  const listClass = item?.listClass || item?.elTagType || ''
+  if (['success', 'warning', 'danger', 'info'].includes(listClass)) return listClass
+  const fallback = { '0': 'success', '1': 'info', '2': 'warning', '3': 'danger' }
+  return fallback[String(value)] || 'info'
 }
 
-function handleBindSelectionChange(selection) {
-  selectedEquipmentIds.value = selection.map(item => String(item.equipmentId))
+function riskLabel(value) {
+  if (value === null || value === undefined || value === '') return '-'
+  const fallback = RISK_FALLBACK.find((item) => item.value === String(value))
+  return dictItem(riskDict, value)?.label || fallback?.label || '-'
 }
 
-function isEquipmentSelectable(row) {
-  const id = String(row.equipmentId)
-  return !boundEquipmentMap.value[id] || selectedEquipmentIds.value.includes(id)
+function riskTagType(value) {
+  const item = dictItem(riskDict, value)
+  const listClass = item?.listClass || item?.elTagType || ''
+  if (['success', 'warning', 'danger', 'info'].includes(listClass)) return listClass
+  const fallback = { '0': 'success', '1': 'warning', '2': 'danger' }
+  return fallback[String(value)] || 'info'
 }
 
-function closeBindDialog() {
-  bindDialogVisible.value = false
-  bindTableData.value = []
-  selectedEquipmentIds.value = []
-  boundEquipmentMap.value = {}
+function formatVolume(value) {
+  if (value === null || value === undefined || value === '') return '--'
+  const num = Number(value)
+  return Number.isFinite(num) ? num.toFixed(2) : '--'
 }
 
-async function submitBind() {
-  if (selectedEquipmentIds.value.length === 0) {
-    ElMessage.warning('请选择要关联的设备')
-    return
-  }
-  try {
-    const res = await bindWaterloggingEquipment({
-      partitionId: currentPartitionId.value,
-      equipmentIds: selectedEquipmentIds.value
-    })
-    if (res.code === 0 || res.code === 200) {
-      ElMessage.success('关联成功')
-      closeBindDialog()
-      loadData()
-    } else {
-      ElMessage.error(res.msg || '关联失败')
-    }
-  } catch (e) {
-    console.error('关联设备失败', e)
-    ElMessage.error('关联失败')
-  }
+function formatNumber(value) {
+  if (value === null || value === undefined || value === '') return '--'
+  const num = Number(value)
+  return Number.isFinite(num) ? String(num) : '--'
+}
+
+function fmt(value) {
+  if (value === null || value === undefined || value === '') return '-'
+  return value
 }
+
+onMounted(() => {
+  loadFilterOptions()
+  loadVideoResources()
+  loadAll()
+})
 </script>
 
 <style scoped>
-.app-container {
-  padding: 20px;
+.waterlogging-page {
+  padding: 16px;
 }
 
-.search-form {
-  margin-bottom: 16px;
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
 }
 
-.toolbar {
-  margin-bottom: 16px;
+.card-title {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.card-sub {
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.card-actions {
+  display: flex;
+  gap: 8px;
 }
 
-.detail-info {
+.map-card,
+.filter-card,
+.table-card {
   margin-bottom: 16px;
 }
 
-.info-item {
+.map-wrap {
+  position: relative;
+  height: 360px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.map-empty {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #909399;
+  font-size: 13px;
+  background: rgba(255, 255, 255, 0.72);
+  pointer-events: none;
+}
+
+.map-legend {
   display: flex;
   align-items: center;
+  flex-wrap: wrap;
+  gap: 16px;
+  padding: 8px 4px 0;
+  font-size: 12px;
+  color: #606266;
+}
+
+.legend-title {
+  color: #909399;
+}
+
+.legend-item {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.legend-item i {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+}
+
+.legend-tip {
+  margin-left: auto;
+  color: #c0c4cc;
+}
+
+.filter-form {
+  margin-bottom: -18px;
+}
+
+.toolbar {
   margin-bottom: 12px;
-  line-height: 32px;
 }
 
-.info-label {
+.camera-tag {
+  margin-right: 4px;
+}
+
+.text-muted {
   color: #909399;
-  font-size: 14px;
-  white-space: nowrap;
+  font-size: 12px;
+}
+
+.pager {
+  margin-top: 16px;
+  justify-content: flex-end;
 }
 
-.info-value {
+.detail-desc {
+  margin-bottom: 16px;
+}
+
+.detail-section {
+  margin-top: 20px;
+}
+
+.detail-section-title {
+  margin-bottom: 10px;
+  font-size: 14px;
+  font-weight: 600;
   color: #303133;
+}
+
+.camera-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.camera-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  background: #fafafa;
+}
+
+.camera-name {
   font-size: 14px;
+  color: #303133;
 }
 
-.map-section {
+.camera-meta {
+  margin-top: 2px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.detail-map-wrap {
+  position: relative;
+  height: 260px;
   border: 1px solid #e4e7ed;
   border-radius: 4px;
   overflow: hidden;
 }
 
-.map-tabs {
-  display: flex;
-  background: #f5f7fa;
-  border-bottom: 1px solid #e4e7ed;
+.form-map-section {
+  margin-top: 4px;
 }
 
-.map-tab {
-  padding: 8px 24px;
+.form-map-title {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+  margin-bottom: 8px;
   font-size: 14px;
-  cursor: default;
-  color: #fff;
-  background: #409eff;
-  user-select: none;
+  font-weight: 600;
+  color: #303133;
+}
+
+.form-map-tip {
+  font-size: 12px;
+  font-weight: 400;
+  color: #909399;
 }
 
-.map-tab-action {
-  padding: 8px 24px;
+.form-map-wrap {
+  height: 300px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.bind-tip {
+  margin-bottom: 12px;
   font-size: 14px;
-  cursor: pointer;
-  color: #f56c6c;
-  transition: all 0.3s;
-  user-select: none;
-  margin-left: auto;
+  color: #303133;
 }
 
-.map-tab-action:hover {
-  color: #f78989;
-  text-decoration: underline;
+.bind-filter {
+  margin-bottom: -8px;
 }
 
-.map-container {
-  width: 100%;
-  height: 400px;
+.status-self {
+  color: #67c23a;
 }
 
-.marker-count {
-  padding: 8px 16px;
-  font-size: 13px;
-  color: #909399;
-  background: #f5f7fa;
-  border-top: 1px solid #e4e7ed;
+.status-occupied {
+  color: #e6a23c;
+}
+
+.player-wrap {
+  height: 420px;
+  background: #101820;
+}
+
+.player-switch {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 12px;
 }
 </style>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů