Преглед на файлове

feat:燃气管网-基础数据管理

null преди 2 месеца
родител
ревизия
46cc1ef2dc

+ 19 - 4
src/api/pipeNetwork/hazard.js

@@ -22,11 +22,26 @@ export function submitHazard(hazardId) {
 export function getHazardMapPoints() {
   return request({ url: '/api/hazard/map-points', method: 'get' })
 }
-export function getHazardStatisticsByType() {
-  return request({ url: '/api/hazard/statistics/type', method: 'get' })
+export function getHazardStatisticsByType(params) {
+  return request({ url: '/api/hazard/statistics/type', method: 'get', params })
 }
-export function getHazardStatisticsByStatus() {
-  return request({ url: '/api/hazard/statistics/status', method: 'get' })
+export function getHazardStatisticsByStatus(params) {
+  return request({ url: '/api/hazard/statistics/status', method: 'get', params })
+}
+export function getHazardStatisticsOverview(params) {
+  return request({ url: '/api/hazard/statistics/overview', method: 'get', params })
+}
+export function getHazardStatisticsByLevel(params) {
+  return request({ url: '/api/hazard/statistics/level', method: 'get', params })
+}
+export function getHazardStatisticsByRegion(params) {
+  return request({ url: '/api/hazard/statistics/region', method: 'get', params })
+}
+export function getHazardStatisticsTypeTimeline(params) {
+  return request({ url: '/api/hazard/statistics/type-timeline', method: 'get', params })
+}
+export function getHazardStatisticsStatusTimeline(params) {
+  return request({ url: '/api/hazard/statistics/status-timeline', method: 'get', params })
 }
 
 // ============ 监测预警 ============

+ 46 - 27
src/views/subSystem/gas/gasOperamon/czspjk.vue

@@ -5,7 +5,7 @@
     <div class="page-header">
       <div class="header-left">
         <el-icon><VideoCamera /></el-icon>
-        <span class="title">燃气场站视频监控中心</span>
+        <span class="title">沅陵县燃气场站视频监控中心</span>
         <div class="stats-badge">
           <span class="stat-item online"><i class="dot"></i>在线 {{ onlineCount }}</span>
           <span class="stat-item offline"><i class="dot"></i>离线 {{ offlineCount }}</span>
@@ -30,8 +30,8 @@
       <!-- 左侧场站列表 -->
       <div class="station-list-panel">
         <div class="list-header">
-          <span>燃气场站列表</span>
-          <span class="total-num">共 {{ filteredStations.length }} 场站</span>
+          <span>沅陵县燃气场站</span>
+          <span class="total-num">共 {{ filteredStations.length }} 场站</span>
         </div>
         <div class="station-list">
           <div
@@ -172,7 +172,7 @@
 
         <!-- 未选中状态 -->
         <div v-else class="empty-video">
-          <el-empty description="请从左侧选择燃气场站查看监控视频" :image-size="120" />
+          <el-empty description="请从左侧选择沅陵县燃气场站查看监控视频" :image-size="120" />
         </div>
       </div>
     </div>
@@ -223,24 +223,25 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
 import { VideoCamera, VideoCameraFilled, Search, Refresh, FullScreen, Download, WindPower, Connection,  Camera } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 
-// 模拟视频流地址(使用测试视频流)
+// 模拟视频流地址
 const mockVideoUrl = 'https://vjs.zencdn.net/v/oceans.mp4'
 
-// 模拟燃气场站数据(含视频信息)
+// 沅陵县燃气场站数据
 const mockStations = ref([
   {
-    id: 1, name: '浦东天然气门站', address: '浦东新区世纪大道888号',
-    methane: 28, methaneThreshold: 20, inPressure: 385, outPressure: 235, temperature: 22.5,
+    id: 1, name: '沅陵天然气门站', address: '沅陵县太常片区辰州北路888号',
+    methane: 28, methaneThreshold: 20, inPressure: 385, outPressure: 235, temperature: 26.5,
     alertStatus: true, videoStatus: 'online', videoUrl: mockVideoUrl,
     cameras: [
       { id: 1, name: '大门出入口', url: mockVideoUrl },
-      { id: 2, name: '储罐区', url: mockVideoUrl },
-      { id: 3, name: '调压区', url: mockVideoUrl }
+      { id: 2, name: '工艺装置区', url: mockVideoUrl },
+      { id: 3, name: '调压计量区', url: mockVideoUrl },
+      { id: 4, name: '消防通道', url: mockVideoUrl }
     ]
   },
   {
-    id: 2, name: '浦西调压站', address: '徐汇区漕溪北路200号',
-    methane: 5, methaneThreshold: 20, inPressure: 350, outPressure: 210, temperature: 21.8,
+    id: 2, name: '太常片区调压站', address: '沅陵县太常片区迎宾西路200号',
+    methane: 5, methaneThreshold: 20, inPressure: 350, outPressure: 210, temperature: 25.8,
     alertStatus: false, videoStatus: 'online', videoUrl: mockVideoUrl,
     cameras: [
       { id: 1, name: '工艺区', url: mockVideoUrl },
@@ -248,39 +249,57 @@ const mockStations = ref([
     ]
   },
   {
-    id: 3, name: '南汇燃气门站', address: '南汇区沪南公路168号',
-    methane: 12, methaneThreshold: 20, inPressure: 320, outPressure: 195, temperature: 20.5,
+    id: 3, name: '沅陵城南CNG储配站', address: '沅陵县太常片区建设南路168号',
+    methane: 12, methaneThreshold: 20, inPressure: 420, outPressure: 280, temperature: 24.5,
     alertStatus: false, videoStatus: 'online', videoUrl: mockVideoUrl,
     cameras: [
       { id: 1, name: '场站全景', url: mockVideoUrl },
-      { id: 2, name: '消防通道', url: mockVideoUrl },
-      { id: 3, name: '卸气区', url: mockVideoUrl }
+      { id: 2, name: '储气罐区', url: mockVideoUrl },
+      { id: 3, name: '卸气区', url: mockVideoUrl },
+      { id: 4, name: '消防泵房', url: mockVideoUrl }
     ]
   },
   {
-    id: 4, name: '宝山LNG储配站', address: '宝山区蕴川路500号',
-    methane: 8, methaneThreshold: 20, inPressure: 400, outPressure: 245, temperature: 19.2,
-    alertStatus: false, videoStatus: 'offline', videoUrl: '', alertStatus: false,
+    id: 4, name: '沅陵工业园区调压站', address: '沅陵县太常片区工业大道500号',
+    methane: 3, methaneThreshold: 20, inPressure: 310, outPressure: 185, temperature: 27.2,
+    alertStatus: false, videoStatus: 'offline', videoUrl: '',
     cameras: []
   },
   {
-    id: 5, name: '临港燃气门站', address: '临港新城环湖西二路88号',
-    methane: 18, methaneThreshold: 20, inPressure: 360, outPressure: 225, temperature: 21.0,
+    id: 5, name: '沅陵城北供气站', address: '沅陵县太常片区天宁北路88号',
+    methane: 18, methaneThreshold: 20, inPressure: 340, outPressure: 205, temperature: 25.0,
     alertStatus: false, videoStatus: 'online', videoUrl: mockVideoUrl,
     cameras: [
       { id: 1, name: '主入口', url: mockVideoUrl },
-      { id: 2, name: '生产区', url: mockVideoUrl }
+      { id: 2, name: '调压区', url: mockVideoUrl },
+      { id: 3, name: '值班室', url: mockVideoUrl }
     ]
   },
   {
-    id: 6, name: '金桥调压站', address: '浦东新区金桥路666号',
-    methane: 25, methaneThreshold: 20, inPressure: 370, outPressure: 230, temperature: 22.0,
+    id: 6, name: '沅陵城东门站', address: '沅陵县太常片区辰州东路666号',
+    methane: 25, methaneThreshold: 20, inPressure: 370, outPressure: 230, temperature: 26.0,
     alertStatus: true, videoStatus: 'online', videoUrl: mockVideoUrl,
     cameras: [
-      { id: 1, name: '调压区', url: mockVideoUrl },
-      { id: 2, name: '值班室', url: mockVideoUrl },
-      { id: 3, name: '周边环境', url: mockVideoUrl }
+      { id: 1, name: '大门出入口', url: mockVideoUrl },
+      { id: 2, name: '过滤调压区', url: mockVideoUrl },
+      { id: 3, name: '加臭装置区', url: mockVideoUrl },
+      { id: 4, name: '周边环境', url: mockVideoUrl }
     ]
+  },
+  {
+    id: 7, name: '凉水井镇调压站', address: '沅陵县凉水井镇集镇路18号',
+    methane: 1, methaneThreshold: 20, inPressure: 290, outPressure: 175, temperature: 25.3,
+    alertStatus: false, videoStatus: 'online', videoUrl: mockVideoUrl,
+    cameras: [
+      { id: 1, name: '场站入口', url: mockVideoUrl },
+      { id: 2, name: '工艺区', url: mockVideoUrl }
+    ]
+  },
+  {
+    id: 8, name: '筲箕湾镇供气站', address: '沅陵县筲箕湾镇车站路32号',
+    methane: 6, methaneThreshold: 20, inPressure: 275, outPressure: 165, temperature: 25.7,
+    alertStatus: false, videoStatus: 'offline', videoUrl: '',
+    cameras: []
   }
 ])
 

+ 621 - 0
src/views/subSystem/gas/gasOperamon/gxlljc.vue

@@ -0,0 +1,621 @@
+<!--管线流量监测-->
+<template>
+  <div class="flow-monitor">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-left">
+        <el-icon><TrendCharts /></el-icon>
+        <span class="title">管线流量监测</span>
+        <div class="stats-badge">
+          <span class="stat-item normal"><i class="dot"></i>正常 {{ normalCount }}</span>
+          <span class="stat-item warning"><i class="dot"></i>预警 {{ warningCount }}</span>
+          <span class="stat-item danger"><i class="dot"></i>告警 {{ dangerCount }}</span>
+          <span class="stat-item offline"><i class="dot"></i>离线 {{ offlineCount }}</span>
+        </div>
+      </div>
+      <div class="header-right">
+        <el-input v-model="searchKeyword" placeholder="搜索测点名称/管线" clearable :prefix-icon="Search" style="width:200px" />
+        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+      </div>
+    </div>
+
+    <!-- 主内容区 -->
+    <div class="main-layout">
+      <!-- 左侧:测点列表 -->
+      <div class="point-list-panel">
+        <div class="list-header">
+          <span>流量监测点</span>
+          <span class="total-num">共 {{ filteredPoints.length }} 个测点</span>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="pipeType" placeholder="管线类型" clearable size="small" style="width:100%">
+            <el-option label="高压管线" value="high" />
+            <el-option label="中压管线" value="medium" />
+            <el-option label="低压管线" value="low" />
+          </el-select>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="statusFilter" placeholder="运行状态" clearable size="small" style="width:100%">
+            <el-option label="正常" value="normal" />
+            <el-option label="预警" value="warning" />
+            <el-option label="告警" value="danger" />
+            <el-option label="离线" value="offline" />
+          </el-select>
+        </div>
+        <div class="point-list">
+          <div
+            v-for="point in filteredPoints"
+            :key="point.id"
+            class="point-item"
+            :class="{ active: selectedPoint?.id === point.id, [point.status]: true }"
+            @click="selectPoint(point)"
+          >
+            <div class="point-header">
+              <span class="point-name">{{ point.name }}</span>
+              <el-tag :type="statusTagType(point.status)" size="small">{{ statusLabel(point.status) }}</el-tag>
+            </div>
+            <div class="point-pipeline">{{ point.pipelineName }}</div>
+            <div class="point-value">
+              <span class="val">{{ point.latestFlow }}</span>
+              <span class="unit">Nm³/h</span>
+              <span class="threshold">量程: 0~{{ point.maxFlow }} Nm³/h</span>
+            </div>
+            <div class="point-sub" v-if="point.status !== 'offline'">
+              <span>今日累计 <b>{{ point.todayTotal }}</b> Nm³</span>
+            </div>
+            <div class="point-addr">{{ point.address }}</div>
+            <div class="point-time">{{ point.latestTime }}</div>
+          </div>
+          <el-empty v-if="filteredPoints.length === 0" description="暂无匹配测点" :image-size="50" />
+        </div>
+      </div>
+
+      <!-- 右侧:详情面板 -->
+      <div class="detail-panel">
+        <template v-if="selectedPoint">
+          <div class="detail-header">
+            <div class="detail-title">
+              <span class="point-name">{{ selectedPoint.name }}</span>
+              <el-tag :type="statusTagType(selectedPoint.status)" size="small">{{ statusLabel(selectedPoint.status) }}</el-tag>
+            </div>
+            <div class="detail-meta">
+              <span>所属管线:{{ selectedPoint.pipelineName }}</span>
+              <span>管线类型:{{ pipeTypeLabel(selectedPoint.pipeType) }}</span>
+              <span>计量表型号:TBQZ-{{ selectedPoint.meterModel }}</span>
+              <span>安装位置:{{ selectedPoint.address }}</span>
+              <span>监测单位:沅陵县燃气安全监测中心</span>
+            </div>
+          </div>
+
+          <!-- 读数卡片 -->
+          <div class="gauge-cards">
+            <div class="gauge-card" :class="selectedPoint.status">
+              <div class="gauge-label">瞬时流量</div>
+              <div class="gauge-value">{{ selectedPoint.latestFlow }} <span class="unit">Nm³/h</span></div>
+              <div class="gauge-sub">采集时间 {{ selectedPoint.latestTime }}</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">今日累计</div>
+              <div class="gauge-value high">{{ selectedPoint.todayTotal }} <span class="unit">Nm³</span></div>
+              <div class="gauge-sub">当日0时起累计</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">昨日累计</div>
+              <div class="gauge-value low">{{ selectedPoint.yesterdayTotal }} <span class="unit">Nm³</span></div>
+              <div class="gauge-sub">较昨日 {{ selectedPoint.dodChange }}</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">运行状态</div>
+              <div class="gauge-status" :class="selectedPoint.status">{{ statusLabel(selectedPoint.status) }}</div>
+              <div class="gauge-sub">量程 0~{{ selectedPoint.maxFlow }} Nm³/h</div>
+            </div>
+          </div>
+
+          <!-- 流量趋势图 -->
+          <div class="chart-card">
+            <div class="chart-header">
+              <span>流量趋势(近10天)</span>
+              <div class="chart-legend">
+                <span class="leg-item"><i class="leg-dot flow"></i> 瞬时流量</span>
+                <span class="leg-item"><i class="leg-line max"></i> 上限</span>
+              </div>
+            </div>
+            <div ref="chartRef" class="chart-container"></div>
+          </div>
+
+          <!-- 历史数据表格 -->
+          <div class="table-card">
+            <div class="table-header">
+              <span>历史监测记录</span>
+              <el-pagination
+                v-model:current-page="historyPage"
+                v-model:page-size="historyPageSize"
+                :total="historyData.length"
+                layout="total, prev, pager, next"
+                small
+              />
+            </div>
+            <el-table :data="pagedHistory" border stripe size="small">
+              <el-table-column prop="time" label="采集时间" width="160" />
+              <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="140">
+                <template #default="{ row }">
+                  <span :style="{ color: row.flow > selectedPoint.maxFlow ? '#f56c6c' : '#303133', fontWeight: row.flow > selectedPoint.maxFlow ? '600' : '400' }">
+                    {{ row.flow }}
+                  </span>
+                </template>
+              </el-table-column>
+              <el-table-column prop="dailyTotal" label="日累计(Nm³)" width="130" />
+              <el-table-column prop="temperature" label="温度(°C)" width="100" />
+              <el-table-column prop="pressure" label="压力(kPa)" width="110" />
+              <el-table-column prop="status" label="状态" width="100">
+                <template #default="{ row }">
+                  <el-tag :type="row.flow > selectedPoint.maxFlow ? 'danger' : 'success'" size="small">
+                    {{ row.flow > selectedPoint.maxFlow ? '超限' : '正常' }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="collector" label="采集设备" min-width="140" />
+              <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+            </el-table>
+          </div>
+        </template>
+
+        <div v-else class="empty-detail">
+          <el-empty description="请从左侧选择流量监测点查看详情" :image-size="100" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import { TrendCharts, Search, Refresh } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+
+const searchKeyword = ref('')
+const pipeType = ref('')
+const statusFilter = ref('')
+const selectedPoint = ref(null)
+const historyPage = ref(1)
+const historyPageSize = ref(10)
+
+const chartRef = ref(null)
+let chartInstance = null
+
+// ==================== 沅陵县管线流量测点数据 ====================
+const mockPoints = ref([
+  {
+    id: 1, name: '辰州北路流量计1#', pipelineName: '辰州北路高压段', pipeType: 'high',
+    address: '沅陵县太常片区辰州北路门站出口计量间', lng: 110.3938, lat: 28.4695,
+    meterModel: 'IIIC-150', latestFlow: 1850, maxFlow: 2500, status: 'normal', latestTime: '2026-06-13 15:32:18',
+    todayTotal: '36,420', yesterdayTotal: '38,150', dodChange: '-4.5%'
+  },
+  {
+    id: 2, name: '辰州中路流量计2#', pipelineName: '辰州中路高压段', pipeType: 'high',
+    address: '沅陵县太常片区辰州中路调压站进口侧', lng: 110.3935, lat: 28.4665,
+    meterModel: 'IIIC-150', latestFlow: 2620, maxFlow: 2500, status: 'danger', latestTime: '2026-06-13 15:31:45',
+    todayTotal: '48,920', yesterdayTotal: '42,300', dodChange: '+15.6%'
+  },
+  {
+    id: 3, name: '辰州南路流量计3#', pipelineName: '辰州南路高压段', pipeType: 'high',
+    address: '沅陵县太常片区辰州南路滨江路口计量柜', lng: 110.3934, lat: 28.4575,
+    meterModel: 'IIIC-100', latestFlow: 1120, maxFlow: 2000, status: 'normal', latestTime: '2026-06-13 15:32:05',
+    todayTotal: '22,180', yesterdayTotal: '23,050', dodChange: '-3.8%'
+  },
+  {
+    id: 4, name: '古城北路流量计1#', pipelineName: '古城北路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区古城北路与天宁路交叉口', lng: 110.3977, lat: 28.4741,
+    meterModel: 'IIIC-80', latestFlow: 580, maxFlow: 800, status: 'normal', latestTime: '2026-06-13 15:30:22',
+    todayTotal: '11,350', yesterdayTotal: '12,100', dodChange: '-6.2%'
+  },
+  {
+    id: 5, name: '古城中路流量计2#', pipelineName: '古城中路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区古城中路太常安置区段', lng: 110.3970, lat: 28.4669,
+    meterModel: 'IIIC-80', latestFlow: 820, maxFlow: 800, status: 'danger', latestTime: '2026-06-13 15:29:58',
+    todayTotal: '16,840', yesterdayTotal: '13,500', dodChange: '+24.7%'
+  },
+  {
+    id: 6, name: '古城南路流量计3#', pipelineName: '古城南路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区古城南路近建设路口', lng: 110.3963, lat: 28.4598,
+    meterModel: 'IIIC-80', latestFlow: 420, maxFlow: 800, status: 'normal', latestTime: '2026-06-13 15:31:30',
+    todayTotal: '8,920', yesterdayTotal: '9,450', dodChange: '-5.6%'
+  },
+  {
+    id: 7, name: '迎宾西路流量计1#', pipelineName: '迎宾西路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区迎宾西路县政府对面', lng: 110.3870, lat: 28.4642,
+    meterModel: 'IIIC-65', latestFlow: 310, maxFlow: 500, status: 'normal', latestTime: '2026-06-13 15:28:15',
+    todayTotal: '6,180', yesterdayTotal: '6,540', dodChange: '-5.5%'
+  },
+  {
+    id: 8, name: '迎宾东路流量计2#', pipelineName: '迎宾东路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区迎宾东路商贸城北侧', lng: 110.4054, lat: 28.4630,
+    meterModel: 'IIIC-65', latestFlow: 280, maxFlow: 500, status: 'normal', latestTime: '2026-06-13 15:30:50',
+    todayTotal: '5,650', yesterdayTotal: '5,980', dodChange: '-5.5%'
+  },
+  {
+    id: 9, name: '建设路流量计1#', pipelineName: '建设路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区建设路县政府南侧计量站', lng: 110.3937, lat: 28.4582,
+    meterModel: 'IIIC-80', latestFlow: 0, maxFlow: 800, status: 'offline', latestTime: '2026-06-13 08:15:00',
+    todayTotal: '--', yesterdayTotal: '14,200', dodChange: '--'
+  },
+  {
+    id: 10, name: '建设西路流量计2#', pipelineName: '建设西路中压段', pipeType: 'medium',
+    address: '沅陵县太常片区建设西路工业园计量站', lng: 110.3840, lat: 28.4588,
+    meterModel: 'IIIC-100', latestFlow: 680, maxFlow: 1000, status: 'normal', latestTime: '2026-06-13 15:29:30',
+    todayTotal: '13,520', yesterdayTotal: '14,080', dodChange: '-4.0%'
+  },
+  {
+    id: 11, name: '滨江路流量计1#', pipelineName: '滨江路段低压段', pipeType: 'low',
+    address: '沅陵县太常片区滨江西路住宅区调压柜后', lng: 110.3820, lat: 28.4568,
+    meterModel: 'IIIC-50', latestFlow: 95, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:31:10',
+    todayTotal: '1,850', yesterdayTotal: '1,920', dodChange: '-3.6%'
+  },
+  {
+    id: 12, name: '滨江东路流量计2#', pipelineName: '滨江东路低压段', pipeType: 'low',
+    address: '沅陵县太常片区滨江东路商业街调压柜后', lng: 110.4066, lat: 28.4557,
+    meterModel: 'IIIC-50', latestFlow: 210, maxFlow: 200, status: 'danger', latestTime: '2026-06-13 15:30:40',
+    todayTotal: '4,320', yesterdayTotal: '3,650', dodChange: '+18.4%'
+  },
+  {
+    id: 13, name: '天宁西路流量计1#', pipelineName: '天宁西路低压段', pipeType: 'low',
+    address: '沅陵县太常片区天宁西路太常卫生院旁', lng: 110.3850, lat: 28.4703,
+    meterModel: 'IIIC-50', latestFlow: 75, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:32:00',
+    todayTotal: '1,520', yesterdayTotal: '1,650', dodChange: '-7.9%'
+  },
+  {
+    id: 14, name: '天宁东路流量计2#', pipelineName: '天宁东路低压段', pipeType: 'low',
+    address: '沅陵县太常片区天宁东路末端近滨江路', lng: 110.4088, lat: 28.4690,
+    meterModel: 'IIIC-50', latestFlow: 88, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:31:55',
+    todayTotal: '1,760', yesterdayTotal: '1,840', dodChange: '-4.3%'
+  },
+  {
+    id: 15, name: '建设东路流量计3#', pipelineName: '建设东路高压段', pipeType: 'high',
+    address: '沅陵县太常片区建设东路工业园入口处', lng: 110.4050, lat: 28.4577,
+    meterModel: 'IIIC-150', latestFlow: 2380, maxFlow: 2500, status: 'warning', latestTime: '2026-06-13 15:31:25',
+    todayTotal: '42,150', yesterdayTotal: '38,600', dodChange: '+9.2%'
+  }
+])
+
+const normalCount = computed(() => mockPoints.value.filter(p => p.status === 'normal').length)
+const warningCount = computed(() => mockPoints.value.filter(p => p.status === 'warning').length)
+const dangerCount = computed(() => mockPoints.value.filter(p => p.status === 'danger').length)
+const offlineCount = computed(() => mockPoints.value.filter(p => p.status === 'offline').length)
+
+const filteredPoints = computed(() => {
+  let list = mockPoints.value
+  if (searchKeyword.value) {
+    const kw = searchKeyword.value.toLowerCase()
+    list = list.filter(p => p.name.toLowerCase().includes(kw) || p.pipelineName.toLowerCase().includes(kw))
+  }
+  if (pipeType.value) list = list.filter(p => p.pipeType === pipeType.value)
+  if (statusFilter.value) list = list.filter(p => p.status === statusFilter.value)
+  return list
+})
+
+function statusTagType(status) {
+  const map = { normal: 'success', warning: 'warning', danger: 'danger', offline: 'info' }
+  return map[status] || 'info'
+}
+function statusLabel(status) {
+  const map = { normal: '正常', warning: '预警', danger: '告警', offline: '离线' }
+  return map[status] || '未知'
+}
+function pipeTypeLabel(type) {
+  const map = { high: '高压', medium: '中压', low: '低压' }
+  return map[type] || type
+}
+
+// ==================== 近10天静态流量数据(每天4时段: 6/12/18/23时, 模拟用气高峰) ====================
+function generateHistory(point) {
+  const baseFlow = point.latestFlow > 0 ? point.latestFlow : Math.round(point.maxFlow * 0.5)
+  // 用气高峰因子: 早6点、午12点、晚18点为高峰,23点为低谷
+  const hourFactors = { 6: 1.25, 12: 1.45, 18: 1.35, 23: 0.65 }
+  // 每点预设的日波动序列(10天)
+  const daySeeds = [
+    { 6: 0, 12: 5, 18: 3, 23: -2 }, { 6: -2, 12: 2, 18: 0, 23: -4 },
+    { 6: 3, 12: 8, 18: 5, 23: 0 }, { 6: -1, 12: 4, 18: 2, 23: -3 },
+    { 6: 2, 12: 6, 18: 4, 23: 1 }, { 6: -3, 12: 1, 18: -1, 23: -5 },
+    { 6: 1, 12: 7, 18: 6, 23: -1 }, { 6: 0, 12: 3, 18: 1, 23: -2 },
+    { 6: -2, 12: 5, 18: 3, 23: -3 }, { 6: 4, 12: 9, 18: 6, 23: 0 }
+  ]
+  const data = []
+  const now = new Date()
+  let cumulative = 0
+  const hours = [6, 12, 18, 23]
+  for (let d = 9; d >= 0; d--) {
+    const dayIdx = 9 - d
+    for (const h of hours) {
+      const date = new Date(now)
+      date.setDate(date.getDate() - d)
+      date.setHours(h, 0, 0)
+      const factor = hourFactors[h] || 1
+      const seed = (daySeeds[dayIdx] && daySeeds[dayIdx][h]) || 0
+      const variation = baseFlow * (factor - 1) + seed * (point.maxFlow / 50)
+      const flow = Math.round((baseFlow + variation) * 10) / 10
+      const dailyCumulative = Math.round(Math.abs(flow) * 6)
+      cumulative += dailyCumulative
+      const temp = (24 + (h < 8 ? 1 : h < 14 ? 3 : h < 20 ? 4 : 0)).toFixed(1)
+      const pressure = point.pipeType === 'high' ? (380 + h * 0.5).toFixed(1) :
+                      point.pipeType === 'medium' ? (190 + h * 0.3).toFixed(1) :
+                      (110 + h * 0.2).toFixed(1)
+      data.push({
+        time: date.toISOString().replace('T', ' ').substring(0, 19),
+        flow,
+        dailyTotal: Math.round(cumulative).toLocaleString(),
+        temperature: temp,
+        pressure,
+        collector: `YLLL-${String(point.id).padStart(3, '0')}`,
+        remark: flow > point.maxFlow ? '流量超量程上限' : ''
+      })
+    }
+  }
+  return data
+}
+
+const historyData = ref([])
+const pagedHistory = computed(() => {
+  const start = (historyPage.value - 1) * historyPageSize.value
+  return historyData.value.slice(start, start + historyPageSize.value)
+})
+
+// ==================== 选择测点 ====================
+function selectPoint(point) {
+  selectedPoint.value = point
+  historyPage.value = 1
+  historyData.value = generateHistory(point)
+  nextTick(() => renderChart())
+}
+
+// ==================== ECharts趋势图 ====================
+function renderChart() {
+  if (!chartRef.value || !selectedPoint.value) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
+  const point = selectedPoint.value
+  const times = historyData.value.map(d => d.time.substring(5, 16).replace(' ', '\n'))
+  const flows = historyData.value.map(d => d.flow)
+
+  chartInstance.setOption({
+    tooltip: {
+      trigger: 'axis',
+      formatter: params => {
+        const p = params[0]
+        const val = p.value
+        const status = val > point.maxFlow ? '⚠ 超量程上限' : '✓ 正常'
+        return `${p.axisValue.replace('\n', ' ')}<br/>瞬时流量: <b>${val} Nm³/h</b> ${status}`
+      }
+    },
+    grid: { left: '3%', right: '5%', top: 20, bottom: 30, containLabel: true },
+    xAxis: {
+      type: 'category', data: times, boundaryGap: false,
+      axisLine: { lineStyle: { color: '#ccc' } },
+      axisTick: { show: false },
+      axisLabel: { fontSize: 10, interval: 3 }
+    },
+    yAxis: {
+      type: 'value', name: 'Nm³/h',
+      axisLine: { show: false }, axisTick: { show: false },
+      splitLine: { lineStyle: { color: '#f0f0f0' } },
+      min: 0,
+      max: Math.max(point.maxFlow + 300, ...flows) + 100
+    },
+    series: [
+      {
+        name: '瞬时流量', type: 'line', data: flows,
+        smooth: true, symbol: 'circle', symbolSize: 4,
+        lineStyle: { width: 2, color: '#52c41a' },
+        itemStyle: { color: '#52c41a' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(82,196,26,0.25)' },
+            { offset: 1, color: 'rgba(82,196,26,0.02)' }
+          ])
+        },
+        markLine: {
+          silent: true,
+          symbol: 'none',
+          label: { fontSize: 11, formatter: '{b} {c} Nm³/h' },
+          data: [
+            { name: '量程上限', yAxis: point.maxFlow, lineStyle: { color: '#f56c6c', type: 'dashed', width: 1.5 } }
+          ]
+        }
+      }
+    ]
+  })
+}
+
+function refreshData() {
+  historyData.value = selectedPoint.value ? generateHistory(selectedPoint.value) : []
+  nextTick(() => renderChart())
+}
+
+onMounted(() => {
+  window.addEventListener('resize', () => { try { chartInstance?.resize() } catch {} })
+  // 默认选中第一条数据
+  if (mockPoints.value.length > 0) {
+    selectPoint(mockPoints.value[0])
+  }
+})
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
+</script>
+
+<style scoped>
+.flow-monitor {
+  background: #f0f2f6;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.page-header {
+  background: #fff;
+  padding: 12px 24px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
+}
+.header-left { display: flex; align-items: center; gap: 20px; }
+.header-left .title { font-size: 1.25rem; font-weight: 600; color: #1f2d3d; }
+.stats-badge { display: flex; gap: 20px; }
+.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
+.stat-item .dot { width: 8px; height: 8px; border-radius: 50%; }
+.stat-item.normal { color: #67c23a; } .stat-item.normal .dot { background: #67c23a; }
+.stat-item.warning { color: #e6a23c; } .stat-item.warning .dot { background: #e6a23c; }
+.stat-item.danger { color: #f56c6c; } .stat-item.danger .dot { background: #f56c6c; }
+.stat-item.offline { color: #909399; } .stat-item.offline .dot { background: #909399; }
+.header-right { display: flex; gap: 12px; }
+
+.main-layout {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+  padding: 12px;
+  gap: 12px;
+}
+
+/* 左侧测点列表 */
+.point-list-panel {
+  width: 370px;
+  background: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.list-header {
+  padding: 14px 16px;
+  border-bottom: 1px solid #e9ecef;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-weight: 600;
+}
+.total-num { font-size: 12px; color: #909399; font-weight: 400; }
+.filter-row { padding: 6px 16px; }
+.point-list { flex: 1; overflow-y: auto; padding: 4px 8px 8px; }
+.point-item {
+  padding: 12px;
+  margin-bottom: 6px;
+  border-radius: 12px;
+  background: #fafbfc;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid #e9ecef;
+}
+.point-item:hover { background: #f0f5ff; }
+.point-item.active { background: #ecf5ff; border-color: #409eff; }
+.point-item.danger { border-left: 3px solid #f56c6c; }
+.point-item.warning { border-left: 3px solid #e6a23c; }
+.point-item.offline { opacity: 0.65; }
+.point-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
+.point-name { font-weight: 600; font-size: 14px; flex: 1; }
+.point-pipeline { font-size: 12px; color: #909399; margin-bottom: 6px; }
+.point-value { display: flex; align-items: baseline; gap: 6px; margin-bottom: 4px; }
+.point-value .val { font-size: 24px; font-weight: 700; color: #1f2d3d; }
+.point-value .unit { font-size: 12px; color: #909399; }
+.point-value .threshold { font-size: 11px; color: #c0c4cc; margin-left: auto; }
+.point-sub { font-size: 12px; color: #606266; margin-bottom: 2px; }
+.point-sub b { color: #1f2d3d; }
+.point-addr { font-size: 11px; color: #909399; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.point-time { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
+
+/* 右侧详情面板 */
+.detail-panel {
+  flex: 1;
+  background: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.empty-detail {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.detail-header {
+  padding: 16px 20px;
+  border-bottom: 1px solid #e9ecef;
+}
+.detail-title { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
+.detail-title .point-name { font-size: 18px; font-weight: 600; }
+.detail-meta { display: flex; flex-wrap: wrap; gap: 0 24px; font-size: 13px; color: #909399; }
+
+/* 读数卡片 */
+.gauge-cards {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+  padding: 16px 20px;
+}
+.gauge-card {
+  background: #f8f9fc;
+  border-radius: 12px;
+  padding: 16px;
+  text-align: center;
+}
+.gauge-card.danger { background: #fef0f0; }
+.gauge-card.warning { background: #fdf6ec; }
+.gauge-label { font-size: 12px; color: #909399; margin-bottom: 8px; }
+.gauge-value { font-size: 28px; font-weight: 700; color: #1f2d3d; }
+.gauge-value .unit { font-size: 13px; font-weight: 400; color: #909399; }
+.gauge-value.high { color: #1890ff; }
+.gauge-value.low { color: #52c41a; }
+.gauge-status { font-size: 20px; font-weight: 700; }
+.gauge-status.normal { color: #67c23a; }
+.gauge-status.warning { color: #e6a23c; }
+.gauge-status.danger { color: #f56c6c; }
+.gauge-status.offline { color: #909399; }
+.gauge-sub { font-size: 11px; color: #c0c4cc; margin-top: 6px; }
+
+/* 趋势图 */
+.chart-card {
+  margin: 0 20px 16px;
+  padding: 16px;
+  background: #fafbfc;
+  border-radius: 12px;
+}
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-weight: 600;
+}
+.chart-legend { display: flex; gap: 16px; font-size: 12px; font-weight: 400; }
+.leg-item { display: flex; align-items: center; gap: 4px; }
+.leg-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
+.leg-dot.flow { background: #52c41a; }
+.leg-line { width: 20px; height: 2px; display: inline-block; }
+.leg-line.max { background: repeating-linear-gradient(90deg, #f56c6c 0, #f56c6c 4px, transparent 4px, transparent 7px); }
+.chart-container { width: 100%; height: 300px; }
+
+/* 历史表格 */
+.table-card {
+  margin: 0 20px 16px;
+  padding: 16px;
+  background: #fafbfc;
+  border-radius: 12px;
+  flex: 1;
+  overflow: auto;
+}
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-weight: 600;
+}
+</style>

+ 614 - 0
src/views/subSystem/gas/gasOperamon/gxyljc.vue

@@ -0,0 +1,614 @@
+<!--管线压力监测-->
+<template>
+  <div class="pressure-monitor">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-left">
+        <el-icon><Odometer /></el-icon>
+        <span class="title">管线压力监测</span>
+        <div class="stats-badge">
+          <span class="stat-item normal"><i class="dot"></i>正常 {{ normalCount }}</span>
+          <span class="stat-item warning"><i class="dot"></i>预警 {{ warningCount }}</span>
+          <span class="stat-item danger"><i class="dot"></i>告警 {{ dangerCount }}</span>
+          <span class="stat-item offline"><i class="dot"></i>离线 {{ offlineCount }}</span>
+        </div>
+      </div>
+      <div class="header-right">
+        <el-input v-model="searchKeyword" placeholder="搜索测点名称/管线" clearable :prefix-icon="Search" style="width:200px" />
+        <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
+      </div>
+    </div>
+
+    <!-- 主内容区:左侧测点列表 + 右侧压力趋势 -->
+    <div class="main-layout">
+      <!-- 左侧:测点列表 -->
+      <div class="point-list-panel">
+        <div class="list-header">
+          <span>压力监测点</span>
+          <span class="total-num">共 {{ filteredPoints.length }} 个测点</span>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="pressureLevel" placeholder="压力等级" clearable size="small" style="width:100%">
+            <el-option label="高压" value="high" />
+            <el-option label="中压" value="medium" />
+            <el-option label="低压" value="low" />
+          </el-select>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="statusFilter" placeholder="运行状态" clearable size="small" style="width:100%">
+            <el-option label="正常" value="normal" />
+            <el-option label="预警" value="warning" />
+            <el-option label="告警" value="danger" />
+            <el-option label="离线" value="offline" />
+          </el-select>
+        </div>
+        <div class="point-list">
+          <div
+            v-for="point in filteredPoints"
+            :key="point.id"
+            class="point-item"
+            :class="{ active: selectedPoint?.id === point.id, [point.status]: true }"
+            @click="selectPoint(point)"
+          >
+            <div class="point-header">
+              <span class="point-name">{{ point.name }}</span>
+              <el-tag :type="statusTagType(point.status)" size="small">{{ statusLabel(point.status) }}</el-tag>
+            </div>
+            <div class="point-pipeline">{{ point.pipelineName }}</div>
+            <div class="point-value">
+              <span class="val">{{ point.latestPressure }}</span>
+              <span class="unit">kPa</span>
+              <span class="threshold">阈值: {{ point.minPressure }}~{{ point.maxPressure }} kPa</span>
+            </div>
+            <div class="point-addr">{{ point.address }}</div>
+            <div class="point-time">{{ point.latestTime }}</div>
+          </div>
+          <el-empty v-if="filteredPoints.length === 0" description="暂无匹配测点" :image-size="50" />
+        </div>
+      </div>
+
+      <!-- 右侧:压力详情 -->
+      <div class="detail-panel">
+        <template v-if="selectedPoint">
+          <!-- 测点信息卡片 -->
+          <div class="detail-header">
+            <div class="detail-title">
+              <span class="point-name">{{ selectedPoint.name }}</span>
+              <el-tag :type="statusTagType(selectedPoint.status)" size="small">{{ statusLabel(selectedPoint.status) }}</el-tag>
+            </div>
+            <div class="detail-meta">
+              <span>所属管线:{{ selectedPoint.pipelineName }}</span>
+              <span>压力等级:{{ pressureLevelLabel(selectedPoint.pressureLevel) }}</span>
+              <span>安装位置:{{ selectedPoint.address }}</span>
+              <span>监测单位:沅陵县燃气安全监测中心</span>
+            </div>
+          </div>
+
+          <!-- 实时读数卡片 -->
+          <div class="gauge-cards">
+            <div class="gauge-card" :class="selectedPoint.status">
+              <div class="gauge-label">当前压力</div>
+              <div class="gauge-value">{{ selectedPoint.latestPressure }} <span class="unit">kPa</span></div>
+              <div class="gauge-sub">采集时间 {{ selectedPoint.latestTime }}</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">今日最高</div>
+              <div class="gauge-value high">{{ todayMax }} <span class="unit">kPa</span></div>
+              <div class="gauge-sub">近24小时</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">今日最低</div>
+              <div class="gauge-value low">{{ todayMin }} <span class="unit">kPa</span></div>
+              <div class="gauge-sub">近24小时</div>
+            </div>
+            <div class="gauge-card">
+              <div class="gauge-label">压力状态</div>
+              <div class="gauge-status" :class="selectedPoint.status">{{ statusLabel(selectedPoint.status) }}</div>
+              <div class="gauge-sub">阈值 {{ selectedPoint.minPressure }}~{{ selectedPoint.maxPressure }} kPa</div>
+            </div>
+          </div>
+
+          <!-- 压力趋势图 -->
+          <div class="chart-card">
+            <div class="chart-header">
+              <span>压力趋势(近10天)</span>
+              <div class="chart-legend">
+                <span class="leg-item"><i class="leg-dot pressure"></i> 实测压力</span>
+                <span class="leg-item"><i class="leg-line max"></i> 上限</span>
+                <span class="leg-item"><i class="leg-line min"></i> 下限</span>
+              </div>
+            </div>
+            <div ref="chartRef" class="chart-container"></div>
+          </div>
+
+          <!-- 历史数据表格 -->
+          <div class="table-card">
+            <div class="table-header">
+              <span>历史监测记录</span>
+              <el-pagination
+                v-model:current-page="historyPage"
+                v-model:page-size="historyPageSize"
+                :total="historyData.length"
+                layout="total, prev, pager, next"
+                small
+              />
+            </div>
+            <el-table :data="pagedHistory" border stripe size="small">
+              <el-table-column prop="time" label="采集时间" width="160" />
+              <el-table-column prop="pressure" label="压力值(kPa)" width="120">
+                <template #default="{ row }">
+                  <span :style="{ color: row.pressure > selectedPoint.maxPressure || row.pressure < selectedPoint.minPressure ? '#f56c6c' : '#303133', fontWeight: row.pressure > selectedPoint.maxPressure || row.pressure < selectedPoint.minPressure ? '600' : '400' }">
+                    {{ row.pressure }}
+                  </span>
+                </template>
+              </el-table-column>
+              <el-table-column prop="temperature" label="温度(°C)" width="100" />
+              <el-table-column prop="status" label="状态" width="100">
+                <template #default="{ row }">
+                  <el-tag :type="row.pressure > selectedPoint.maxPressure || row.pressure < selectedPoint.minPressure ? 'danger' : 'success'" size="small">
+                    {{ row.pressure > selectedPoint.maxPressure || row.pressure < selectedPoint.minPressure ? '异常' : '正常' }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="collector" label="采集设备" min-width="140" />
+              <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+            </el-table>
+          </div>
+        </template>
+
+        <!-- 未选中状态 -->
+        <div v-else class="empty-detail">
+          <el-empty description="请从左侧选择压力监测点查看详情" :image-size="100" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
+import { Odometer, Search, Refresh } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+
+// 筛选
+const searchKeyword = ref('')
+const pressureLevel = ref('')
+const statusFilter = ref('')
+
+// 选中测点
+const selectedPoint = ref(null)
+
+// 历史数据分页
+const historyPage = ref(1)
+const historyPageSize = ref(10)
+
+// ECharts实例
+const chartRef = ref(null)
+let chartInstance = null
+
+// ==================== 沅陵县管线压力测点数据 ====================
+const mockPoints = ref([
+  {
+    id: 1, name: '辰州北路高压测点1#', pipelineName: '辰州北路高压段', pressureLevel: 'high',
+    address: '沅陵县太常片区辰州北路与建设路交叉口北侧', lng: 110.3938, lat: 28.4692,
+    latestPressure: 385, minPressure: 240, maxPressure: 420, status: 'normal', latestTime: '2026-06-13 15:32:18'
+  },
+  {
+    id: 2, name: '辰州中路高压测点2#', pipelineName: '辰州中路高压段', pressureLevel: 'high',
+    address: '沅陵县太常片区辰州中路与天宁路交叉口南侧', lng: 110.3935, lat: 28.4658,
+    latestPressure: 432, minPressure: 240, maxPressure: 420, status: 'danger', latestTime: '2026-06-13 15:31:45'
+  },
+  {
+    id: 3, name: '辰州南路高压测点3#', pipelineName: '辰州南路高压段', pressureLevel: 'high',
+    address: '沅陵县太常片区辰州南路近滨江路口', lng: 110.3934, lat: 28.4580,
+    latestPressure: 368, minPressure: 240, maxPressure: 420, status: 'normal', latestTime: '2026-06-13 15:32:05'
+  },
+  {
+    id: 4, name: '古城北路中压测点1#', pipelineName: '古城北路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区古城北路与天宁路交叉口', lng: 110.3977, lat: 28.4741,
+    latestPressure: 218, minPressure: 150, maxPressure: 250, status: 'normal', latestTime: '2026-06-13 15:30:22'
+  },
+  {
+    id: 5, name: '古城中路中压测点2#', pipelineName: '古城中路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区古城中路太常安置区段', lng: 110.3970, lat: 28.4669,
+    latestPressure: 142, minPressure: 150, maxPressure: 250, status: 'warning', latestTime: '2026-06-13 15:29:58'
+  },
+  {
+    id: 6, name: '古城南路中压测点3#', pipelineName: '古城南路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区古城南路近建设路口', lng: 110.3963, lat: 28.4598,
+    latestPressure: 205, minPressure: 150, maxPressure: 250, status: 'normal', latestTime: '2026-06-13 15:31:30'
+  },
+  {
+    id: 7, name: '迎宾西路中压测点1#', pipelineName: '迎宾西路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区迎宾西路县政府西侧', lng: 110.3824, lat: 28.4645,
+    latestPressure: 188, minPressure: 150, maxPressure: 250, status: 'normal', latestTime: '2026-06-13 15:28:15'
+  },
+  {
+    id: 8, name: '迎宾东路中压测点2#', pipelineName: '迎宾东路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区迎宾东路城郊公交站东侧', lng: 110.4054, lat: 28.4630,
+    latestPressure: 198, minPressure: 150, maxPressure: 250, status: 'normal', latestTime: '2026-06-13 15:30:50'
+  },
+  {
+    id: 9, name: '建设路中压测点1#', pipelineName: '建设路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区建设路县政府南侧', lng: 110.3937, lat: 28.4582,
+    latestPressure: 0, minPressure: 150, maxPressure: 250, status: 'offline', latestTime: '2026-06-13 12:05:00'
+  },
+  {
+    id: 10, name: '建设西路中压测点2#', pipelineName: '建设西路中压段', pressureLevel: 'medium',
+    address: '沅陵县太常片区建设西路人行道绿化带', lng: 110.3840, lat: 28.4588,
+    latestPressure: 175, minPressure: 150, maxPressure: 250, status: 'normal', latestTime: '2026-06-13 15:29:30'
+  },
+  {
+    id: 11, name: '滨江路低压测点1#', pipelineName: '滨江路段低压段', pressureLevel: 'low',
+    address: '沅陵县太常片区滨江西路河堤段', lng: 110.3820, lat: 28.4568,
+    latestPressure: 112, minPressure: 80, maxPressure: 150, status: 'normal', latestTime: '2026-06-13 15:31:10'
+  },
+  {
+    id: 12, name: '滨江东路低压测点2#', pipelineName: '滨江东路低压段', pressureLevel: 'low',
+    address: '沅陵县太常片区滨江东路与天宁东路交叉口', lng: 110.4066, lat: 28.4557,
+    latestPressure: 78, minPressure: 80, maxPressure: 150, status: 'warning', latestTime: '2026-06-13 15:30:40'
+  },
+  {
+    id: 13, name: '天宁西路低压测点1#', pipelineName: '天宁西路低压段', pressureLevel: 'low',
+    address: '沅陵县太常片区天宁西路太常卫生院外墙', lng: 110.3850, lat: 28.4703,
+    latestPressure: 128, minPressure: 80, maxPressure: 150, status: 'normal', latestTime: '2026-06-13 15:32:00'
+  },
+  {
+    id: 14, name: '天宁东路低压测点2#', pipelineName: '天宁东路低压段', pressureLevel: 'low',
+    address: '沅陵县太常片区天宁东路末端近滨江路', lng: 110.4088, lat: 28.4690,
+    latestPressure: 136, minPressure: 80, maxPressure: 150, status: 'normal', latestTime: '2026-06-13 15:31:55'
+  },
+  {
+    id: 15, name: '建设东路高压测点4#', pipelineName: '建设东路高压段', pressureLevel: 'high',
+    address: '沅陵县太常片区建设东路工业园入口处', lng: 110.4050, lat: 28.4577,
+    latestPressure: 410, minPressure: 240, maxPressure: 420, status: 'warning', latestTime: '2026-06-13 15:31:25'
+  }
+])
+
+// 统计数据
+const normalCount = computed(() => mockPoints.value.filter(p => p.status === 'normal').length)
+const warningCount = computed(() => mockPoints.value.filter(p => p.status === 'warning').length)
+const dangerCount = computed(() => mockPoints.value.filter(p => p.status === 'danger').length)
+const offlineCount = computed(() => mockPoints.value.filter(p => p.status === 'offline').length)
+
+// 筛选
+const filteredPoints = computed(() => {
+  let list = mockPoints.value
+  if (searchKeyword.value) {
+    const kw = searchKeyword.value.toLowerCase()
+    list = list.filter(p => p.name.toLowerCase().includes(kw) || p.pipelineName.toLowerCase().includes(kw))
+  }
+  if (pressureLevel.value) list = list.filter(p => p.pressureLevel === pressureLevel.value)
+  if (statusFilter.value) list = list.filter(p => p.status === statusFilter.value)
+  return list
+})
+
+function statusTagType(status) {
+  const map = { normal: 'success', warning: 'warning', danger: 'danger', offline: 'info' }
+  return map[status] || 'info'
+}
+function statusLabel(status) {
+  const map = { normal: '正常', warning: '预警', danger: '告警', offline: '离线' }
+  return map[status] || '未知'
+}
+function pressureLevelLabel(level) {
+  const map = { high: '高压 (0.4MPa)', medium: '中压 (0.2MPa)', low: '低压 (0.1MPa)' }
+  return map[level] || level
+}
+
+// ==================== 近10天静态压力数据(每天6个时段: 0/4/8/12/16/20时) ====================
+function generateHistory(point) {
+  const baseP = point.latestPressure > 0 ? point.latestPressure : (point.minPressure + point.maxPressure) / 2
+  // 模拟每日压力波动基准模式: 凌晨低、早高峰高、午间稳、晚高峰高
+  const hourOffsets = [0, 4, 8, 12, 16, 20]
+  const hourFactors = [0.92, 0.95, 1.08, 1.02, 1.06, 0.98]
+  // 每点预设的压力日波动序列(10天,每天6个时段)
+  const daySeeds = [
+    [0, -3, 6, 2, 5, -1], [-2, -5, 4, -1, 3, -4], [1, 2, 8, 4, 6, 0], [-3, -2, 3, 0, 4, -2], [2, 1, 7, 3, 5, 1],
+    [-1, -4, 5, 1, 2, -3], [3, 0, 9, 5, 7, 2], [-4, -1, 2, -2, 1, -5], [0, 3, 6, 2, 4, -1], [2, -2, 5, 3, 6, 0]
+  ]
+  const data = []
+  const now = new Date()
+  for (let d = 9; d >= 0; d--) {
+    const dayIdx = 9 - d
+    for (let hi = 0; hi < 6; hi++) {
+      const h = hourOffsets[hi]
+      const date = new Date(now)
+      date.setDate(date.getDate() - d)
+      date.setHours(h, 0, 0)
+      const variation = baseP * (hourFactors[hi] - 1) + daySeeds[dayIdx][hi] * (point.maxPressure - point.minPressure) / 50
+      const pressure = Math.round((baseP + variation) * 10) / 10
+      const temp = (24 + (h < 6 ? 0 : h < 12 ? 3 : h < 18 ? 4 : 1)).toFixed(1)
+      data.push({
+        time: date.toISOString().replace('T', ' ').substring(0, 19),
+        pressure,
+        temperature: temp,
+        collector: `YLCG-${String(point.id).padStart(3, '0')}`,
+        remark: pressure > point.maxPressure ? '压力超上限' : pressure < point.minPressure ? '压力低于下限' : ''
+      })
+    }
+  }
+  return data
+}
+
+const historyData = ref([])
+const pagedHistory = computed(() => {
+  const start = (historyPage.value - 1) * historyPageSize.value
+  return historyData.value.slice(start, start + historyPageSize.value)
+})
+
+// 今日极值
+const todayMax = computed(() => {
+  const today = historyData.value.filter(d => d.time.startsWith('2026-06-13'))
+  if (!today.length) return '--'
+  return Math.max(...today.map(d => d.pressure))
+})
+const todayMin = computed(() => {
+  const today = historyData.value.filter(d => d.time.startsWith('2026-06-13'))
+  if (!today.length) return '--'
+  return Math.min(...today.map(d => d.pressure))
+})
+
+// ==================== 选择测点 ====================
+function selectPoint(point) {
+  selectedPoint.value = point
+  historyPage.value = 1
+  historyData.value = generateHistory(point)
+  nextTick(() => renderChart())
+}
+
+// ==================== ECharts趋势图 ====================
+function renderChart() {
+  if (!chartRef.value || !selectedPoint.value) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
+  const point = selectedPoint.value
+  const times = historyData.value.map(d => d.time.substring(5, 16).replace(' ', '\n'))
+  const pressures = historyData.value.map(d => d.pressure)
+
+  chartInstance.setOption({
+    tooltip: {
+      trigger: 'axis',
+      formatter: params => {
+        const p = params[0]
+        const val = p.value
+        const status = val > point.maxPressure ? '⚠ 超上限' : val < point.minPressure ? '⚠ 低于下限' : '✓ 正常'
+        return `${p.axisValue.replace('\n', ' ')}<br/>压力: <b>${val} kPa</b> ${status}`
+      }
+    },
+    grid: { left: '3%', right: '5%', top: 20, bottom: 30, containLabel: true },
+    xAxis: {
+      type: 'category', data: times, boundaryGap: false,
+      axisLine: { lineStyle: { color: '#ccc' } },
+      axisTick: { show: false },
+      axisLabel: { fontSize: 10, interval: 5 }
+    },
+    yAxis: {
+      type: 'value', name: 'kPa',
+      axisLine: { show: false }, axisTick: { show: false },
+      splitLine: { lineStyle: { color: '#f0f0f0' } },
+      min: Math.min(point.minPressure - 30, ...pressures) - 10,
+      max: Math.max(point.maxPressure + 30, ...pressures) + 10
+    },
+    series: [
+      {
+        name: '实测压力', type: 'line', data: pressures,
+        smooth: true, symbol: 'circle', symbolSize: 4,
+        lineStyle: { width: 2, color: '#409eff' },
+        itemStyle: { color: '#409eff' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(64,158,255,0.25)' },
+            { offset: 1, color: 'rgba(64,158,255,0.02)' }
+          ])
+        },
+        markLine: {
+          silent: true,
+          symbol: 'none',
+          label: { fontSize: 11, formatter: '{b} {c} kPa' },
+          data: [
+            { name: '上限', yAxis: point.maxPressure, lineStyle: { color: '#f56c6c', type: 'dashed', width: 1.5 } },
+            { name: '下限', yAxis: point.minPressure, lineStyle: { color: '#e6a23c', type: 'dashed', width: 1.5 } }
+          ]
+        }
+      }
+    ]
+  })
+}
+
+function refreshData() {
+  historyData.value = selectedPoint.value ? generateHistory(selectedPoint.value) : []
+  nextTick(() => renderChart())
+}
+
+watch(pressureLevel, () => {})
+watch(statusFilter, () => {})
+
+onMounted(() => {
+  window.addEventListener('resize', () => { try { chartInstance?.resize() } catch {} })
+  // 默认选中第一条数据
+  if (mockPoints.value.length > 0) {
+    selectPoint(mockPoints.value[0])
+  }
+})
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
+</script>
+
+<style scoped>
+.pressure-monitor {
+  background: #f0f2f6;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.page-header {
+  background: #fff;
+  padding: 12px 24px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
+}
+.header-left { display: flex; align-items: center; gap: 20px; }
+.header-left .title { font-size: 1.25rem; font-weight: 600; color: #1f2d3d; }
+.stats-badge { display: flex; gap: 20px; }
+.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
+.stat-item .dot { width: 8px; height: 8px; border-radius: 50%; }
+.stat-item.normal { color: #67c23a; } .stat-item.normal .dot { background: #67c23a; }
+.stat-item.warning { color: #e6a23c; } .stat-item.warning .dot { background: #e6a23c; }
+.stat-item.danger { color: #f56c6c; } .stat-item.danger .dot { background: #f56c6c; }
+.stat-item.offline { color: #909399; } .stat-item.offline .dot { background: #909399; }
+.header-right { display: flex; gap: 12px; }
+
+.main-layout {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+  padding: 12px;
+  gap: 12px;
+}
+
+/* 左侧测点列表 */
+.point-list-panel {
+  width: 360px;
+  background: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.list-header {
+  padding: 14px 16px;
+  border-bottom: 1px solid #e9ecef;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-weight: 600;
+}
+.total-num { font-size: 12px; color: #909399; font-weight: 400; }
+.filter-row { padding: 6px 16px; }
+.point-list { flex: 1; overflow-y: auto; padding: 4px 8px 8px; }
+.point-item {
+  padding: 12px;
+  margin-bottom: 6px;
+  border-radius: 12px;
+  background: #fafbfc;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid #e9ecef;
+}
+.point-item:hover { background: #f0f5ff; }
+.point-item.active { background: #ecf5ff; border-color: #409eff; }
+.point-item.danger { border-left: 3px solid #f56c6c; }
+.point-item.warning { border-left: 3px solid #e6a23c; }
+.point-item.offline { opacity: 0.65; }
+.point-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
+.point-name { font-weight: 600; font-size: 14px; flex: 1; }
+.point-pipeline { font-size: 12px; color: #909399; margin-bottom: 6px; }
+.point-value { display: flex; align-items: baseline; gap: 6px; margin-bottom: 4px; }
+.point-value .val { font-size: 24px; font-weight: 700; color: #1f2d3d; }
+.point-value .unit { font-size: 12px; color: #909399; }
+.point-value .threshold { font-size: 11px; color: #c0c4cc; margin-left: auto; }
+.point-addr { font-size: 11px; color: #909399; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.point-time { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
+
+/* 右侧详情面板 */
+.detail-panel {
+  flex: 1;
+  background: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.empty-detail {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.detail-header {
+  padding: 16px 20px;
+  border-bottom: 1px solid #e9ecef;
+}
+.detail-title { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
+.detail-title .point-name { font-size: 18px; font-weight: 600; }
+.detail-meta { display: flex; flex-wrap: wrap; gap: 0 24px; font-size: 13px; color: #909399; }
+
+/* 读数卡片 */
+.gauge-cards {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+  padding: 16px 20px;
+}
+.gauge-card {
+  background: #f8f9fc;
+  border-radius: 12px;
+  padding: 16px;
+  text-align: center;
+}
+.gauge-card.danger { background: #fef0f0; }
+.gauge-card.warning { background: #fdf6ec; }
+.gauge-label { font-size: 12px; color: #909399; margin-bottom: 8px; }
+.gauge-value { font-size: 28px; font-weight: 700; color: #1f2d3d; }
+.gauge-value .unit { font-size: 13px; font-weight: 400; color: #909399; }
+.gauge-value.high { color: #f56c6c; }
+.gauge-value.low { color: #67c23a; }
+.gauge-status { font-size: 20px; font-weight: 700; }
+.gauge-status.normal { color: #67c23a; }
+.gauge-status.warning { color: #e6a23c; }
+.gauge-status.danger { color: #f56c6c; }
+.gauge-status.offline { color: #909399; }
+.gauge-sub { font-size: 11px; color: #c0c4cc; margin-top: 6px; }
+
+/* 趋势图 */
+.chart-card {
+  margin: 0 20px 16px;
+  padding: 16px;
+  background: #fafbfc;
+  border-radius: 12px;
+}
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-weight: 600;
+}
+.chart-legend { display: flex; gap: 16px; font-size: 12px; font-weight: 400; }
+.leg-item { display: flex; align-items: center; gap: 4px; }
+.leg-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
+.leg-dot.pressure { background: #409eff; }
+.leg-line { width: 20px; height: 2px; display: inline-block; }
+.leg-line.max { background: #f56c6c; background: repeating-linear-gradient(90deg, #f56c6c 0, #f56c6c 4px, transparent 4px, transparent 7px); }
+.leg-line.min { background: #e6a23c; background: repeating-linear-gradient(90deg, #e6a23c 0, #e6a23c 4px, transparent 4px, transparent 7px); }
+.chart-container { width: 100%; height: 300px; }
+
+/* 历史表格 */
+.table-card {
+  margin: 0 20px 16px;
+  padding: 16px;
+  background: #fafbfc;
+  border-radius: 12px;
+  flex: 1;
+  overflow: auto;
+}
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-weight: 600;
+}
+</style>

+ 414 - 310
src/views/subSystem/gas/gasOperamon/ssjcyzt.vue

@@ -14,90 +14,69 @@
         </div>
       </div>
       <div class="header-right">
-        <el-input
-            v-model="searchKeyword"
-            placeholder="搜索设施/地址"
-            clearable
-            :prefix-icon="Search"
-            style="width: 220px"
-        />
+        <el-input v-model="searchKeyword" placeholder="搜索设施/地址" clearable :prefix-icon="Search" style="width:220px" @input="onSearch" />
         <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
     </div>
 
-    <!-- 主内容区:左侧图层控制 + 右侧GIS地图 -->
+    <!-- 主内容区 -->
     <div class="main-layout">
       <!-- 左侧图层控制面板 -->
       <div class="layer-panel">
-        <div class="layer-header">
-          <el-icon></el-icon> 分类图层
-        </div>
+        <div class="layer-header">分类图层</div>
         <div class="layer-tree">
-          <el-checkbox-group v-model="visibleLayers">
-            <div class="layer-group">
-              <div class="group-title">燃气管网</div>
-              <el-checkbox value="highPressure">高压管线</el-checkbox>
-              <el-checkbox value="mediumPressure">中压管线</el-checkbox>
-              <el-checkbox value="lowPressure">低压管线</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">相邻地下空间监测</div>
-              <el-checkbox value="methane">甲烷监测点</el-checkbox>
-              <el-checkbox value="temperature">温湿度监测</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">管线压力/流量监测</div>
-              <el-checkbox value="pressure">压力监测点</el-checkbox>
-              <el-checkbox value="flow">流量监测点</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">燃气场站</div>
-              <el-checkbox value="gateStation">门站</el-checkbox>
-              <el-checkbox value="regulatorStation">调压站</el-checkbox>
-              <el-checkbox value="lngStation">LNG储配站</el-checkbox>
-            </div>
-            <div class="layer-group">
-              <div class="group-title">告警图层</div>
-              <el-checkbox value="alert">实时告警点</el-checkbox>
-            </div>
-          </el-checkbox-group>
+          <div class="layer-group">
+            <div class="group-title">燃气管网</div>
+            <el-checkbox v-model="layers.highPressure" @change="onLayerChange">高压管线</el-checkbox>
+            <el-checkbox v-model="layers.mediumPressure" @change="onLayerChange">中压管线</el-checkbox>
+            <el-checkbox v-model="layers.lowPressure" @change="onLayerChange">低压管线</el-checkbox>
+          </div>
+          <div class="layer-group">
+            <div class="group-title">相邻地下空间监测</div>
+            <el-checkbox v-model="layers.methane" @change="onLayerChange">甲烷监测点</el-checkbox>
+            <el-checkbox v-model="layers.temperature" @change="onLayerChange">温湿度监测</el-checkbox>
+          </div>
+          <div class="layer-group">
+            <div class="group-title">管线压力/流量监测</div>
+            <el-checkbox v-model="layers.pressure" @change="onLayerChange">压力监测点</el-checkbox>
+            <el-checkbox v-model="layers.flow" @change="onLayerChange">流量监测点</el-checkbox>
+          </div>
+          <div class="layer-group">
+            <div class="group-title">燃气场站</div>
+            <el-checkbox v-model="layers.gateStation" @change="onLayerChange">门站</el-checkbox>
+            <el-checkbox v-model="layers.regulatorStation" @change="onLayerChange">调压站</el-checkbox>
+            <el-checkbox v-model="layers.lngStation" @change="onLayerChange">LNG储配站</el-checkbox>
+          </div>
+          <div class="layer-group">
+            <div class="group-title">告警图层</div>
+            <el-checkbox v-model="layers.alert" @change="onLayerChange">实时告警点</el-checkbox>
+          </div>
         </div>
 
         <div class="legend-panel">
           <div class="legend-title">图例说明</div>
-          <div><span class="legend-line high"></span> 高压管线</div>
-          <div><span class="legend-line medium"></span> 中压管线</div>
-          <div><span class="legend-line low"></span> 低压管线</div>
+          <div><span class="legend-line high"></span> 高压管线 (0.4MPa)</div>
+          <div><span class="legend-line medium"></span> 中压管线 (0.2MPa)</div>
+          <div><span class="legend-line low"></span> 低压管线 (0.1MPa)</div>
           <div><span class="legend-point methane"></span> 甲烷监测点</div>
+          <div><span class="legend-point temp"></span> 温湿度监测</div>
           <div><span class="legend-point pressure"></span> 压力监测点</div>
           <div><span class="legend-point flow"></span> 流量监测点</div>
           <div><span class="legend-point station"></span> 燃气场站</div>
-          <div><span class="legend-point alert"></span> 告警设备</div>
+          <div><span class="legend-point alert-l"></span> 告警设备</div>
         </div>
 
         <div class="stats-panel">
           <div class="stats-title">实时统计</div>
-          <div class="stats-row">
-            <span>平均压力:</span>
-            <span class="value">{{ avgPressure }} kPa</span>
-          </div>
-          <div class="stats-row">
-            <span>平均流量:</span>
-            <span class="value">{{ avgFlow }} m³/h</span>
-          </div>
-          <div class="stats-row">
-            <span>最高甲烷浓度:</span>
-            <span class="value alert-value">{{ maxMethane }} ppm</span>
-          </div>
-          <div class="stats-row">
-            <span>设备在线率:</span>
-            <span class="value">{{ onlineRate }}%</span>
-          </div>
+          <div class="stats-row"><span>平均压力:</span><span class="value">{{ avgPressure }} kPa</span></div>
+          <div class="stats-row"><span>平均流量:</span><span class="value">{{ avgFlow }} Nm³/h</span></div>
+          <div class="stats-row"><span>最高甲烷浓度:</span><span class="value alert-value">{{ maxMethane }} ppm</span></div>
+          <div class="stats-row"><span>设备在线率:</span><span class="value">{{ onlineRate }}%</span></div>
         </div>
       </div>
 
-      <!-- 右侧GIS地图区域 -->
+      <!-- 右侧GIS地图 -->
       <div class="gis-container">
         <div class="map-toolbar">
           <el-button-group>
@@ -105,22 +84,15 @@
             <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
             <el-button size="small" :icon="Location" @click="fitBounds">重置视图</el-button>
           </el-button-group>
-          <div class="coordinate-info">
-            <span>坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</span>
-          </div>
+          <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="gasSafetyMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
-          <div v-if="hoverFeature" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoverFeature.name }}</div>
-            <div>类型: {{ hoverFeature.typeName }}</div>
-            <div v-if="hoverFeature.value">实时值: {{ hoverFeature.value }}</div>
-            <div v-if="hoverFeature.status">状态: {{ hoverFeature.status }}</div>
-          </div>
+        <div class="map-wrapper">
+          <div id="gasSafetyBaiduMap" class="map-div"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
 
-        <!-- 详情抽屉 -->
-        <el-drawer v-model="detailDrawerVisible" :title="`设施详情 - ${currentFeature?.name}`" size="450px" direction="rtl">
+        <!-- 设施详情抽屉 -->
+        <el-drawer v-model="detailDrawerVisible" :title="`设施详情 - ${currentFeature?.name || ''}`" size="450px" direction="rtl">
           <div v-if="currentFeature" class="feature-detail">
             <el-descriptions :column="1" border>
               <el-descriptions-item label="设施名称">{{ currentFeature.name }}</el-descriptions-item>
@@ -129,13 +101,12 @@
               <el-descriptions-item label="详细地址">{{ currentFeature.address }}</el-descriptions-item>
               <el-descriptions-item label="经纬度">{{ currentFeature.lng }}, {{ currentFeature.lat }}</el-descriptions-item>
               <el-descriptions-item v-if="currentFeature.pressure" label="当前压力">{{ currentFeature.pressure }} kPa</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.flow" label="当前流量">{{ currentFeature.flow }} m³/h</el-descriptions-item>
+              <el-descriptions-item v-if="currentFeature.flow" label="当前流量">{{ currentFeature.flow }} Nm³/h</el-descriptions-item>
               <el-descriptions-item v-if="currentFeature.methane" label="甲烷浓度">{{ currentFeature.methane }} ppm</el-descriptions-item>
               <el-descriptions-item v-if="currentFeature.temperature" label="温度">{{ currentFeature.temperature }} °C</el-descriptions-item>
+              <el-descriptions-item v-if="currentFeature.humidity" label="湿度">{{ currentFeature.humidity }} %RH</el-descriptions-item>
               <el-descriptions-item label="运行状态">
-                <el-tag :type="currentFeature.status === 'online' ? 'success' : currentFeature.status === 'alert' ? 'danger' : 'info'">
-                  {{ currentFeature.status === 'online' ? '在线' : currentFeature.status === 'alert' ? '告警' : '离线' }}
-                </el-tag>
+                <el-tag :type="statusTagType(currentFeature.status)">{{ statusLabel(currentFeature.status) }}</el-tag>
               </el-descriptions-item>
               <el-descriptions-item label="最后通讯">{{ currentFeature.updateTime }}</el-descriptions-item>
             </el-descriptions>
@@ -147,268 +118,399 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import { ref, computed, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
 
-// 可见图层
-const visibleLayers = ref(['highPressure', 'mediumPressure', 'methane', 'pressure', 'flow', 'gateStation', 'alert'])
+// ==================== 图层状态 ====================
+const layers = reactive({
+  highPressure: true, mediumPressure: true, lowPressure: true,
+  methane: true, temperature: true,
+  pressure: true, flow: true,
+  gateStation: true, regulatorStation: true, lngStation: true,
+  alert: true
+})
 
-// --- 模拟管线数据 ---
-const pipelines = ref([
-  { id: 'P1', name: '张杨路高压管线', type: 'highPressure', pressure: '高压', color: '#f56c6c', lineWidth: 4,
-    coordinates: [[121.48, 31.24], [121.50, 31.245], [121.52, 31.24], [121.54, 31.235]] },
-  { id: 'P2', name: '世纪大道中压管线', type: 'mediumPressure', pressure: '中压', color: '#e6a23c', lineWidth: 3,
-    coordinates: [[121.49, 31.23], [121.51, 31.228], [121.53, 31.225], [121.55, 31.22]] },
-  { id: 'P3', name: '浦东南路低压管线', type: 'lowPressure', pressure: '低压', color: '#67c23a', lineWidth: 2,
-    coordinates: [[121.485, 31.255], [121.50, 31.25], [121.515, 31.248], [121.53, 31.245]] }
-])
+const searchKeyword = ref('')
+const mapError = ref('')
+let mapInstance = null
+let activeInfoWindow = null
+
+// 存储所有覆盖物以便按图层显隐
+const overlayStore = {
+  highPressure: [], mediumPressure: [], lowPressure: [],
+  methane: [], temperature: [],
+  pressure: [], flow: [],
+  gateStation: [], regulatorStation: [], lngStation: [],
+  alert: []
+}
+
+// ==================== 沅陵县太常片区静态数据 ====================
+const MAP_CENTER = { lng: 110.395, lat: 28.470 }
+const MAP_DEFAULT_ZOOM = 14
+
+// --- 管线数据 (polyline paths) —— 沅陵县城北岸城区路网 ---
+const pipelines = [
+  // 高压管线 (红色) —— 沿辰州路南北主干
+  { id: 'P1', name: '辰州北路高压段', type: 'highPressure', pressure: '高压 0.4MPa', color: '#f56c6c', width: 5,
+    path: [[110.3940, 28.4820], [110.3942, 28.4770], [110.3938, 28.4710], [110.3936, 28.4645]] },
+  { id: 'P2', name: '辰州中路高压段', type: 'highPressure', pressure: '高压 0.4MPa', color: '#f56c6c', width: 5,
+    path: [[110.3938, 28.4710], [110.3935, 28.4690], [110.3936, 28.4645]] },
+  { id: 'P3', name: '建设东路高压段', type: 'highPressure', pressure: '高压 0.4MPa', color: '#f56c6c', width: 5,
+    path: [[110.3940, 28.4658], [110.3980, 28.4660], [110.4050, 28.4658]] },
+
+  // 中压管线 (橙色)
+  { id: 'P4', name: '古城北路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3980, 28.4820], [110.3978, 28.4775], [110.3972, 28.4710], [110.3965, 28.4655]] },
+  { id: 'P5', name: '古城南路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3965, 28.4655], [110.3962, 28.4635]] },
+  { id: 'P6', name: '迎宾西路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3830, 28.4695], [110.3870, 28.4700], [110.3940, 28.4695]] },
+  { id: 'P7', name: '迎宾东路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3940, 28.4695], [110.4000, 28.4692], [110.4055, 28.4690], [110.4100, 28.4688]] },
+  { id: 'P8', name: '建设路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3840, 28.4660], [110.3900, 28.4658], [110.3940, 28.4656], [110.4000, 28.4655]] },
+  { id: 'P9', name: '天宁路中压段', type: 'mediumPressure', pressure: '中压 0.2MPa', color: '#e6a23c', width: 4,
+    path: [[110.3840, 28.4760], [110.3900, 28.4755], [110.3975, 28.4765], [110.4050, 28.4750], [110.4090, 28.4752]] },
+
+  // 低压管线 (绿色)
+  { id: 'P10', name: '滨江西路低压段', type: 'lowPressure', pressure: '低压 0.1MPa', color: '#67c23a', width: 3,
+    path: [[110.3800, 28.4638], [110.3860, 28.4640], [110.3930, 28.4638]] },
+  { id: 'P11', name: '滨江东路低压段', type: 'lowPressure', pressure: '低压 0.1MPa', color: '#67c23a', width: 3,
+    path: [[110.3930, 28.4638], [110.4000, 28.4632], [110.4066, 28.4630]] },
+  { id: 'P13', name: '建设西路低压段', type: 'lowPressure', pressure: '低压 0.1MPa', color: '#67c23a', width: 3,
+    path: [[110.3815, 28.4660], [110.3840, 28.4662]] }
+]
 
 // --- 甲烷监测点(相邻地下空间)---
-const methanePoints = ref([
-  { id: 'M1', name: '人民广场甲烷监测点', type: 'methane', typeName: '甲烷监测点', lng: 121.487, lat: 31.239, address: '人民大道200号地下管廊', methane: 28, status: 'alert', updateTime: '2025-04-02 15:23:45', code: 'CH4-001' },
-  { id: 'M2', name: '世纪大道甲烷监测点', type: 'methane', typeName: '甲烷监测点', lng: 121.505, lat: 31.235, address: '世纪大道福山路口', methane: 12, status: 'online', updateTime: '2025-04-02 15:22:30', code: 'CH4-002' },
-  { id: 'M3', name: '张杨路甲烷监测点', type: 'methane', typeName: '甲烷监测点', lng: 121.52, lat: 31.242, address: '张杨路松林路口', methane: 35, status: 'alert', updateTime: '2025-04-02 15:21:15', code: 'CH4-003' }
-])
+const methanePoints = [
+  { id: 'M1', name: '辰州北路地下管廊CH4-1#', type: 'methane', typeName: '甲烷监测点', lng: 110.3940, lat: 28.4770, address: '沅陵县太常片区辰州北路地下综合管廊', methane: 28, status: 'alert', updateTime: '2026-06-13 15:23:45', code: 'CH4-001' },
+  { id: 'M2', name: '古城中路地下空间CH4-2#', type: 'methane', typeName: '甲烷监测点', lng: 110.3972, lat: 28.4710, address: '沅陵县太常片区古城中路太常安置区地下车库', methane: 12, status: 'online', updateTime: '2026-06-13 15:22:30', code: 'CH4-002' },
+  { id: 'M3', name: '迎宾东路地下空间CH4-3#', type: 'methane', typeName: '甲烷监测点', lng: 110.4055, lat: 28.4690, address: '沅陵县太常片区迎宾东路商贸城地下通道', methane: 35, status: 'alert', updateTime: '2026-06-13 15:21:15', code: 'CH4-003' },
+  { id: 'M4', name: '建设路地下管沟CH4-4#', type: 'methane', typeName: '甲烷监测点', lng: 110.3940, lat: 28.4656, address: '沅陵县太常片区建设路县政府南侧地下管沟', methane: 8, status: 'online', updateTime: '2026-06-13 15:24:00', code: 'CH4-004' },
+  { id: 'M5', name: '滨江路河堤管廊CH4-5#', type: 'methane', typeName: '甲烷监测点', lng: 110.3860, lat: 28.4640, address: '沅陵县太常片区滨江西路河堤管廊段', methane: 5, status: 'online', updateTime: '2026-06-13 15:20:30', code: 'CH4-005' },
+  { id: 'M6', name: '天宁东路管沟CH4-6#', type: 'methane', typeName: '甲烷监测点', lng: 110.4088, lat: 28.4752, address: '沅陵县太常片区天宁东路末端综合管沟', methane: 18, status: 'warning', updateTime: '2026-06-13 15:19:45', code: 'CH4-006' }
+]
+
+// --- 温湿度监测点 ---
+const tempHumidityPoints = [
+  { id: 'TH1', name: '辰州中路温湿度1#', type: 'temperature', typeName: '温湿度监测点', lng: 110.3935, lat: 28.4710, address: '沅陵县太常片区辰州中路管廊入口', temperature: 25.8, humidity: 62, status: 'online', updateTime: '2026-06-13 15:25:00', code: 'TH-001' },
+  { id: 'TH2', name: '古城北路温湿度2#', type: 'temperature', typeName: '温湿度监测点', lng: 110.3978, lat: 28.4775, address: '沅陵县太常片区古城北路管廊检查井', temperature: 27.2, humidity: 58, status: 'online', updateTime: '2026-06-13 15:23:30', code: 'TH-002' },
+  { id: 'TH3', name: '建设东路温湿度3#', type: 'temperature', typeName: '温湿度监测点', lng: 110.4050, lat: 28.4658, address: '沅陵县太常片区建设东路工业园管廊', temperature: 29.1, humidity: 55, status: 'warning', updateTime: '2026-06-13 15:22:00', code: 'TH-003' }
+]
 
 // --- 压力监测点 ---
-const pressurePoints = ref([
-  { id: 'PR1', name: '人民路压力监测点', type: 'pressure', typeName: '压力监测点', lng: 121.49, lat: 31.245, address: '人民路与世纪大道路口', pressure: 385, status: 'online', updateTime: '2025-04-02 15:23:00', code: 'PR-001' },
-  { id: 'PR2', name: '龙阳路压力监测点', type: 'pressure', typeName: '压力监测点', lng: 121.51, lat: 31.215, address: '龙阳路白杨路口', pressure: 320, status: 'online', updateTime: '2025-04-02 15:22:00', code: 'PR-002' },
-  { id: 'PR3', name: '东方路压力监测点', type: 'pressure', typeName: '压力监测点', lng: 121.53, lat: 31.248, address: '东方路蓝村路口', pressure: 350, status: 'alert', updateTime: '2025-04-02 15:20:00', code: 'PR-003' }
-])
+const pressurePoints = [
+  { id: 'PR1', name: '辰州北路高压测点1#', type: 'pressure', typeName: '压力监测点', lng: 110.3940, lat: 28.4750, address: '沅陵县太常片区辰州北路与建设路交叉口北侧', pressure: 385, status: 'online', updateTime: '2026-06-13 15:32:18', code: 'PR-001' },
+  { id: 'PR2', name: '辰州中路高压测点2#', type: 'pressure', typeName: '压力监测点', lng: 110.3935, lat: 28.4710, address: '沅陵县太常片区辰州中路与天宁路交叉口南侧', pressure: 432, status: 'danger', updateTime: '2026-06-13 15:31:45', code: 'PR-002' },
+  { id: 'PR3', name: '古城中路中压测点2#', type: 'pressure', typeName: '压力监测点', lng: 110.3972, lat: 28.4710, address: '沅陵县太常片区古城中路太常安置区段', pressure: 142, status: 'warning', updateTime: '2026-06-13 15:29:58', code: 'PR-003' },
+  { id: 'PR4', name: '迎宾西路中压测点1#', type: 'pressure', typeName: '压力监测点', lng: 110.3870, lat: 28.4700, address: '沅陵县太常片区迎宾西路县政府对面', pressure: 205, status: 'online', updateTime: '2026-06-13 15:28:15', code: 'PR-004' },
+  { id: 'PR5', name: '建设路中压测点1#', type: 'pressure', typeName: '压力监测点', lng: 110.3940, lat: 28.4656, address: '沅陵县太常片区建设路县政府南侧', pressure: 0, status: 'offline', updateTime: '2026-06-13 12:05:00', code: 'PR-005' },
+  { id: 'PR6', name: '滨江路低压测点1#', type: 'pressure', typeName: '压力监测点', lng: 110.3830, lat: 28.4638, address: '沅陵县太常片区滨江西路河堤段', pressure: 112, status: 'online', updateTime: '2026-06-13 15:31:10', code: 'PR-006' },
+  { id: 'PR7', name: '建设东路高压测点4#', type: 'pressure', typeName: '压力监测点', lng: 110.4050, lat: 28.4658, address: '沅陵县太常片区建设东路工业园入口处', pressure: 410, status: 'warning', updateTime: '2026-06-13 15:31:25', code: 'PR-007' }
+]
 
 // --- 流量监测点 ---
-const flowPoints = ref([
-  { id: 'F1', name: '世纪大道流量计', type: 'flow', typeName: '流量监测点', lng: 121.50, lat: 31.233, address: '世纪大道浦电路口', flow: 1250, status: 'online', updateTime: '2025-04-02 15:22:00', code: 'FL-001' },
-  { id: 'F2', name: '张杨路流量计', type: 'flow', typeName: '流量监测点', lng: 121.515, lat: 31.244, address: '张杨路松林路', flow: 980, status: 'online', updateTime: '2025-04-02 15:21:00', code: 'FL-002' }
-])
+const flowPoints = [
+  { id: 'F1', name: '辰州北路流量计1#', type: 'flow', typeName: '流量监测点', lng: 110.3940, lat: 28.4760, address: '沅陵县太常片区辰州北路门站出口计量间', flow: 1850, status: 'online', updateTime: '2026-06-13 15:32:18', code: 'FL-001' },
+  { id: 'F2', name: '辰州中路流量计2#', type: 'flow', typeName: '流量监测点', lng: 110.3935, lat: 28.4720, address: '沅陵县太常片区辰州中路调压站进口侧', flow: 2620, status: 'danger', updateTime: '2026-06-13 15:31:45', code: 'FL-002' },
+  { id: 'F3', name: '古城中路流量计2#', type: 'flow', typeName: '流量监测点', lng: 110.3972, lat: 28.4710, address: '沅陵县太常片区古城中路太常安置区段', flow: 820, status: 'danger', updateTime: '2026-06-13 15:29:58', code: 'FL-003' },
+  { id: 'F4', name: '迎宾西路流量计1#', type: 'flow', typeName: '流量监测点', lng: 110.3870, lat: 28.4700, address: '沅陵县太常片区迎宾西路县政府对面', flow: 310, status: 'online', updateTime: '2026-06-13 15:28:15', code: 'FL-004' },
+  { id: 'F5', name: '建设西路流量计2#', type: 'flow', typeName: '流量监测点', lng: 110.3840, lat: 28.4660, address: '沅陵县太常片区建设西路工业园计量站', flow: 680, status: 'online', updateTime: '2026-06-13 15:29:30', code: 'FL-005' },
+  { id: 'F6', name: '滨江东路流量计2#', type: 'flow', typeName: '流量监测点', lng: 110.4066, lat: 28.4630, address: '沅陵县太常片区滨江东路商业街', flow: 210, status: 'danger', updateTime: '2026-06-13 15:30:40', code: 'FL-006' }
+]
 
 // --- 燃气场站 ---
-const stations = ref([
-  { id: 'S1', name: '浦东天然气门站', type: 'gateStation', typeName: '门站', lng: 121.50, lat: 31.238, address: '浦东新区世纪大道888号', status: 'online', updateTime: '2025-04-02 15:23:45', code: 'GS-001' },
-  { id: 'S2', name: '浦西调压站', type: 'regulatorStation', typeName: '调压站', lng: 121.52, lat: 31.23, address: '徐汇区漕溪北路200号', status: 'online', updateTime: '2025-04-02 15:22:30', code: 'RS-001' },
-  { id: 'S3', name: '宝山LNG储配站', type: 'lngStation', typeName: 'LNG储配站', lng: 121.535, lat: 31.25, address: '宝山区蕴川路500号', status: 'online', updateTime: '2025-04-02 15:21:00', code: 'LNG-001' }
-])
+const stations = [
+  { id: 'S1', name: '沅陵天然气门站', type: 'gateStation', typeName: '门站', lng: 110.3940, lat: 28.4770, address: '沅陵县太常片区辰州北路188号', status: 'online', updateTime: '2026-06-13 15:35:00', code: 'GS-001' },
+  { id: 'S2', name: '太常片区调压站', type: 'regulatorStation', typeName: '调压站', lng: 110.3965, lat: 28.4695, address: '沅陵县太常片区迎宾路与古城路交叉口', status: 'online', updateTime: '2026-06-13 15:30:00', code: 'RS-001' },
+  { id: 'S3', name: '沅陵LNG储配站', type: 'lngStation', typeName: 'LNG储配站', lng: 110.4030, lat: 28.4670, address: '沅陵县太常片区建设东路工业园末端', status: 'online', updateTime: '2026-06-13 15:28:00', code: 'LNG-001' }
+]
 
-// 统计数据
+// ==================== 统计数据 ====================
 const pipelineCount = computed(() => 48.6)
-const monitorCount = computed(() => methanePoints.value.length + pressurePoints.value.length + flowPoints.value.length)
-const stationCount = computed(() => stations.value.length)
+const monitorCount = computed(() => methanePoints.length + tempHumidityPoints.length + pressurePoints.length + flowPoints.length)
+const stationCount = computed(() => stations.length)
 const alertCount = computed(() => {
-  const alerts = [...methanePoints.value, ...pressurePoints.value].filter(f => f.status === 'alert').length
-  return alerts
+  const all = [...methanePoints, ...tempHumidityPoints, ...pressurePoints, ...flowPoints, ...stations]
+  return all.filter(f => f.status === 'alert' || f.status === 'danger').length
 })
 const avgPressure = computed(() => {
-  const pressures = pressurePoints.value.map(p => p.pressure)
-  return Math.round(pressures.reduce((a, b) => a + b, 0) / pressures.length)
+  const online = pressurePoints.filter(p => p.status !== 'offline' && p.pressure > 0)
+  return online.length ? Math.round(online.reduce((a, b) => a + b.pressure, 0) / online.length) : '--'
 })
 const avgFlow = computed(() => {
-  const flows = flowPoints.value.map(f => f.flow)
-  return Math.round(flows.reduce((a, b) => a + b, 0) / flows.length)
+  const online = flowPoints.filter(f => f.status !== 'offline' && f.flow > 0)
+  return online.length ? Math.round(online.reduce((a, b) => a + b.flow, 0) / online.length) : '--'
 })
-const maxMethane = computed(() => Math.max(...methanePoints.value.map(m => m.methane)))
+const maxMethane = computed(() => Math.max(...methanePoints.map(m => m.methane)))
 const onlineRate = computed(() => {
-  const allDevices = [...methanePoints.value, ...pressurePoints.value, ...flowPoints.value, ...stations.value]
+  const allDevices = [...methanePoints, ...tempHumidityPoints, ...pressurePoints, ...flowPoints, ...stations]
   const online = allDevices.filter(d => d.status === 'online').length
   return Math.round((online / allDevices.length) * 100)
 })
 
-// GIS画布相关
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
-const searchKeyword = ref('')
-const currentLng = ref(121.505)
-const currentLat = ref(31.235)
-const hoverFeature = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
+// 搜索过滤
+const filteredFeatures = computed(() => {
+  const all = [
+    ...methanePoints.map(p => ({ ...p, category: 'methane' })),
+    ...tempHumidityPoints.map(p => ({ ...p, category: 'temperature' })),
+    ...pressurePoints.map(p => ({ ...p, category: 'pressure' })),
+    ...flowPoints.map(p => ({ ...p, category: 'flow' })),
+    ...stations.map(p => ({ ...p, category: 'station' }))
+  ]
+  if (!searchKeyword.value) return all
+  const kw = searchKeyword.value.toLowerCase()
+  return all.filter(f => f.name.toLowerCase().includes(kw) || f.address.toLowerCase().includes(kw))
+})
+
+function onSearch() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  const found = filteredFeatures.value[0]
+  if (found) {
+    const pt = new BMapGL.Point(found.lng, found.lat)
+    mapInstance.panTo(pt)
+    openFeatureInfo(found)
+  }
+}
+
+// ==================== 状态映射 ====================
+function statusTagType(status) {
+  const map = { online: 'success', warning: 'warning', danger: 'danger', alert: 'danger', offline: 'info' }
+  return map[status] || 'info'
+}
+function statusLabel(status) {
+  const map = { online: '在线', warning: '预警', danger: '告警', alert: '告警', offline: '离线' }
+  return map[status] || status
+}
+
+// ==================== 地图交互 ====================
+const currentLng = ref(MAP_CENTER.lng)
+const currentLat = ref(MAP_CENTER.lat)
 const currentFeature = ref(null)
 const detailDrawerVisible = ref(false)
 
-// 坐标转换
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
+function zoomIn() {
+  if (!mapInstance) return
+  mapInstance.zoomIn()
+}
+function zoomOut() {
+  if (!mapInstance) return
+  mapInstance.zoomOut()
+}
+function fitBounds() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  closeInfoWindow()
+}
+function refreshData() {
+  // 重新渲染所有覆盖物
+  clearAllOverlays()
+  renderAllOverlays()
 }
 
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0)
-    ctx.lineTo(x, mapHeight)
-    ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y)
-    ctx.lineTo(mapWidth, y)
-    ctx.stroke()
-  }
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('世纪大道', 350, 120)
-  ctx.fillText('张杨路', 450, 280)
-  ctx.fillText('龙阳路', 200, 370)
+// ==================== 百度地图初始化 ====================
+function waitForBMapGL() {
+  if (typeof BMapGL !== 'undefined') { initMap(); return }
+  let attempts = 0
+  const timer = setInterval(() => {
+    attempts++
+    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
+    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
+  }, 200)
 }
 
-// 绘制管线
-function drawPipelines() {
-  pipelines.value.forEach(pipeline => {
-    if (!visibleLayers.value.includes(pipeline.type)) return
-    const points = pipeline.coordinates.map(coord => worldToCanvas(coord[0], coord[1]))
-    if (points.length < 2) return
-    ctx.beginPath()
-    ctx.moveTo(points[0].x, points[0].y)
-    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y)
-    ctx.strokeStyle = pipeline.color
-    ctx.lineWidth = pipeline.lineWidth * mapZoom
-    ctx.stroke()
-  })
+function closeInfoWindow() {
+  if (activeInfoWindow) {
+    try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}
+    activeInfoWindow = null
+  }
 }
 
-// 绘制点要素
-function drawPoints(points, iconColor, size = 8, shape = 'circle') {
-  points.forEach(point => {
-    if (!visibleLayers.value.includes(point.type)) return
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    if (x < -30 || x > mapWidth + 30 || y < -30 || y > mapHeight + 30) return
-
-    let color = iconColor
-    if (point.status === 'alert') color = '#f56c6c'
-    if (point.status === 'offline') color = '#909399'
-
-    ctx.beginPath()
-    if (shape === 'circle') ctx.arc(x, y, size, 0, 2 * Math.PI)
-    else if (shape === 'rect') ctx.fillRect(x - size/2, y - size/2, size, size)
-    else if (shape === 'triangle') {
-      ctx.moveTo(x, y - size)
-      ctx.lineTo(x + size, y + size/2)
-      ctx.lineTo(x - size, y + size/2)
-      ctx.closePath()
-    }
-    ctx.fillStyle = color
-    ctx.fill()
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 2
-    ctx.stroke()
-
-    if (point.status === 'alert') {
-      ctx.beginPath()
-      ctx.arc(x, y, size + 4, 0, 2 * Math.PI)
-      ctx.fillStyle = 'rgba(245,108,108,0.3)'
-      ctx.fill()
+function initMap() {
+  const el = document.getElementById('gasSafetyBaiduMap')
+  if (!el) { mapError.value = '地图容器未找到。'; return }
+
+  try {
+    mapInstance = new BMapGL.Map('gasSafetyBaiduMap', { enableMapClick: true })
+  } catch (e) {
+    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    return
+  }
+
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  mapInstance.enableScrollWheelZoom(true)
+  mapInstance.setMinZoom(12)
+  mapInstance.setMaxZoom(18)
+
+  // 鼠标移动更新坐标
+  mapInstance.addEventListener('mousemove', (e) => {
+    if (e && e.latlng) {
+      currentLng.value = e.latlng.lng
+      currentLat.value = e.latlng.lat
     }
   })
+
+  // 点击空白关闭信息窗
+  mapInstance.addEventListener('click', () => {
+    closeInfoWindow()
+  })
+
+  renderAllOverlays()
 }
 
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawPipelines()
-  drawPoints(methanePoints.value, '#9b59b6', 10, 'circle')
-  drawPoints(pressurePoints.value, '#409eff', 8, 'rect')
-  drawPoints(flowPoints.value, '#67c23a', 8, 'triangle')
-  drawPoints(stations.value, '#e6a23c', 12, 'circle')
+// ==================== 覆盖物渲染 ====================
+function clearAllOverlays() {
+  closeInfoWindow()
+  Object.values(overlayStore).forEach(arr => {
+    arr.forEach(ov => { try { mapInstance.removeOverlay(ov) } catch (_) {} })
+    arr.length = 0
+  })
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-function refreshData() { renderMap() }
-
-function updateCoordinates(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  const px = (mouseX - offsetX) / mapZoom
-  const py = (mouseY - offsetY) / mapZoom
-  const lng = bounds.minLng + (px / mapWidth) * (bounds.maxLng - bounds.minLng)
-  const lat = bounds.maxLat - (py / mapHeight) * (bounds.maxLat - bounds.minLat)
-  currentLng.value = lng
-  currentLat.value = lat
+function getLayerKey(category) {
+  const map = { methane: 'methane', temperature: 'temperature', pressure: 'pressure', flow: 'flow', gateStation: 'gateStation', regulatorStation: 'regulatorStation', lngStation: 'lngStation' }
+  return map[category] || category
 }
 
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-
-  const allFeatures = [...methanePoints.value, ...pressurePoints.value, ...flowPoints.value, ...stations.value]
-  allFeatures.forEach(feature => {
-    const { x, y } = worldToCanvas(feature.lng, feature.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = feature }
+// 绘制管线
+function renderPipelines() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  pipelines.forEach(pipe => {
+    const pts = pipe.path.map(([lng, lat]) => new BMapGL.Point(lng, lat))
+    const polyline = new BMapGL.Polyline(pts, {
+      strokeColor: pipe.color,
+      strokeWeight: pipe.width,
+      strokeOpacity: 0.85
+    })
+    polyline.addEventListener('click', () => {
+      closeInfoWindow()
+      const html = `<div style="font-size:13px;line-height:1.8;"><b>${pipe.name}</b><br/>压力等级:${pipe.pressure}</div>`
+      activeInfoWindow = new BMapGL.InfoWindow(html, { width: 200, height: 60 })
+      const mid = Math.floor(pts.length / 2)
+      mapInstance.openInfoWindow(activeInfoWindow, pts[mid])
+    })
+    mapInstance.addOverlay(polyline)
+    overlayStore[pipe.type].push(polyline)
   })
-  if (hit) { currentFeature.value = hit; detailDrawerVisible.value = true }
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-
-  const allFeatures = [...methanePoints.value, ...pressurePoints.value, ...flowPoints.value, ...stations.value]
-  allFeatures.forEach(feature => {
-    const { x, y } = worldToCanvas(feature.lng, feature.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = feature }
+// 绘制点位标记
+function drawPointMarkers(points, category, color, size = 10, shape = 'circle', iconText = '') {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  const layerKey = getLayerKey(category)
+  points.forEach(p => {
+    const pt = new BMapGL.Point(p.lng, p.lat)
+    let displayColor = color
+    if (p.status === 'alert' || p.status === 'danger') displayColor = '#f56c6c'
+    if (p.status === 'warning') displayColor = '#e6a23c'
+    if (p.status === 'offline') displayColor = '#909399'
+
+    const r = size
+    const html = `<div style="width:${r*2}px;height:${r*2}px;border-radius:${shape === 'circle' ? '50%' : shape === 'rect' ? '3px' : '0'};background:${displayColor};border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3);cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:bold;">${iconText}</div>`
+
+    const label = new BMapGL.Label(html, {
+      position: pt,
+      offset: new BMapGL.Size(-r, -r)
+    })
+    label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
+
+    label.addEventListener('click', (e) => {
+      if (e && e.domEvent) e.domEvent.stopPropagation()
+      openFeatureInfo(p)
+    })
+
+    mapInstance.addOverlay(label)
+    overlayStore[layerKey].push(label)
+
+    // 告警点额外显示脉冲圈
+    if ((p.status === 'alert' || p.status === 'danger') && layers.alert) {
+      const pulseHtml = `<div style="width:${r*3}px;height:${r*3}px;border-radius:50%;background:rgba(245,108,108,0.3);border:2px solid rgba(245,108,108,0.6);animation:pulse 1.5s infinite;"></div>`
+      const pulseLabel = new BMapGL.Label(pulseHtml, {
+        position: pt,
+        offset: new BMapGL.Size(-r * 1.5, -r * 1.5)
+      })
+      pulseLabel.setStyle({ border: 'none', background: 'transparent', padding: '0' })
+      mapInstance.addOverlay(pulseLabel)
+      overlayStore.alert.push(pulseLabel)
+    }
   })
-  if (hit) {
-    hoverFeature.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else { hoverFeature.value = null }
 }
 
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
-  })
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
-    renderMap()
+function renderAllOverlays() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  clearAllOverlays()
+
+  // 管线
+  if (layers.highPressure || layers.mediumPressure || layers.lowPressure) {
+    renderPipelines()
+  }
+
+  // 点位
+  drawPointMarkers(methanePoints, 'methane', '#9b59b6', 10, 'circle', 'CH4')
+  drawPointMarkers(tempHumidityPoints, 'temperature', '#2db7f5', 9, 'circle', '温')
+  drawPointMarkers(pressurePoints, 'pressure', '#409eff', 9, 'rect', 'P')
+  drawPointMarkers(flowPoints, 'flow', '#67c23a', 9, 'rect', 'F')
+  drawPointMarkers(stations.filter(s => s.type === 'gateStation'), 'gateStation', '#e6a23c', 13, 'circle', '门')
+  drawPointMarkers(stations.filter(s => s.type === 'regulatorStation'), 'regulatorStation', '#e6a23c', 12, 'circle', '调')
+  drawPointMarkers(stations.filter(s => s.type === 'lngStation'), 'lngStation', '#e6a23c', 14, 'circle', 'LNG')
+
+  applyLayerVisibility()
+}
+
+// ==================== 图层显隐 ====================
+function applyLayerVisibility() {
+  Object.entries(overlayStore).forEach(([key, overlays]) => {
+    const visible = layers[key]
+    overlays.forEach(ov => {
+      try {
+        if (visible) { ov.show() } else { ov.hide() }
+      } catch (_) {}
+    })
   })
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
+
+  // 告警脉冲圈只在告警图层开启时显示
+  overlayStore.alert.forEach(ov => {
+    try {
+      if (layers.alert) { ov.show() } else { ov.hide() }
+    } catch (_) {}
   })
 }
 
-watch(() => visibleLayers.value, () => renderMap(), { deep: true })
+function onLayerChange() {
+  if (!mapInstance) return
+  applyLayerVisibility()
+}
+
+// ==================== 详情交互 ====================
+function openFeatureInfo(feature) {
+  closeInfoWindow()
+  currentFeature.value = feature
+  detailDrawerVisible.value = true
+
+  const pt = new BMapGL.Point(feature.lng, feature.lat)
+  let infoHtml = `<div style="font-size:13px;line-height:1.8;max-width:240px;"><b>${feature.name}</b><br/>类型:${feature.typeName}<br/>`
+  if (feature.pressure) infoHtml += `压力:${feature.pressure} kPa<br/>`
+  if (feature.flow) infoHtml += `流量:${feature.flow} Nm³/h<br/>`
+  if (feature.methane) infoHtml += `甲烷浓度:${feature.methane} ppm<br/>`
+  if (feature.temperature) infoHtml += `温度:${feature.temperature} °C<br/>`
+  if (feature.humidity) infoHtml += `湿度:${feature.humidity} %RH<br/>`
+  infoHtml += `状态:${statusLabel(feature.status)}<br/>地址:${feature.address}</div>`
+
+  activeInfoWindow = new BMapGL.InfoWindow(infoHtml, { width: 240, height: 150 })
+  mapInstance.openInfoWindow(activeInfoWindow, pt)
+  mapInstance.panTo(pt)
+}
 
 onMounted(() => {
-  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
+  nextTick(() => {
+    waitForBMapGL()
+  })
+})
+
+onBeforeUnmount(() => {
+  try { mapInstance?.destroy() } catch (_) {}
+  mapInstance = null
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
 </script>
 
 <style scoped>
@@ -420,15 +522,16 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   overflow: hidden;
 }
 .page-header {
-  background: white;
+  background: #fff;
   padding: 12px 24px;
   display: flex;
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
 }
 .header-left { display: flex; align-items: center; gap: 20px; }
-.header-left .title { font-size: 1.3rem; font-weight: 600; }
+.header-left .title { font-size: 1.25rem; font-weight: 600; color: #1f2d3d; }
 .stats-badge { display: flex; gap: 20px; }
 .stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
 .stat-item.critical { color: #f56c6c; }
@@ -446,40 +549,46 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   padding: 12px;
   gap: 12px;
 }
+
+/* 左侧面板 */
 .layer-panel {
   width: 260px;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   padding: 16px;
   display: flex;
   flex-direction: column;
   gap: 16px;
   overflow-y: auto;
+  flex-shrink: 0;
 }
-.layer-header { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
+.layer-header { font-weight: 600; font-size: 16px; }
 .layer-group { margin-bottom: 12px; }
 .group-title { font-weight: 500; margin-bottom: 8px; font-size: 13px; color: #606266; }
-.layer-group .el-checkbox { display: block; margin-left: 8px; margin-bottom: 6px; }
+.layer-group :deep(.el-checkbox) { display: block; margin-left: 8px; margin-bottom: 6px; }
 .legend-panel { border-top: 1px solid #e9ecef; padding-top: 12px; }
-.legend-title { font-weight: 500; margin-bottom: 8px; }
-.legend-line { display: inline-block; width: 30px; height: 3px; margin-right: 8px; }
+.legend-title { font-weight: 600; margin-bottom: 8px; }
+.legend-line { display: inline-block; width: 30px; height: 3px; margin-right: 8px; vertical-align: middle; }
 .legend-line.high { background: #f56c6c; }
 .legend-line.medium { background: #e6a23c; }
 .legend-line.low { background: #67c23a; }
-.legend-point { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; }
+.legend-point { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; vertical-align: middle; }
+.legend-point.temp { background: #2db7f5; }
 .legend-point.methane { background: #9b59b6; }
 .legend-point.pressure { background: #409eff; border-radius: 2px; }
-.legend-point.flow { background: #67c23a; clip-path: polygon(50% 0%, 0% 100%, 100% 100%); border-radius: 0; }
+.legend-point.flow { background: #67c23a; border-radius: 2px; }
 .legend-point.station { background: #e6a23c; }
-.legend-point.alert { background: #f56c6c; }
+.legend-point.alert-l { background: #f56c6c; }
 .stats-panel { background: #f5f7fa; border-radius: 8px; padding: 12px; }
 .stats-title { font-weight: 600; margin-bottom: 8px; }
 .stats-row { display: flex; justify-content: space-between; font-size: 13px; margin: 6px 0; }
-.alert-value { color: #f56c6c; font-weight: bold; }
+.stats-row .value { font-weight: 600; }
+.alert-value { color: #f56c6c; }
 
+/* 右侧GIS */
 .gis-container {
   flex: 1;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
@@ -491,20 +600,15 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   display: flex;
   justify-content: space-between;
   align-items: center;
+  flex-shrink: 0;
 }
 .coordinate-info { font-size: 12px; color: #909399; }
-.map-wrapper { flex: 1; position: relative; cursor: crosshair; }
-canvas { width: 100%; height: 100%; display: block; }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
-  border-radius: 10px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
+.map-wrapper { flex: 1; position: relative; overflow: hidden; }
+.map-div { width: 100%; height: 100%; }
+.map-error {
+  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
+  color: #f56c6c; font-size: 14px; background: #fff; padding: 16px 24px;
+  border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);
 }
-.hover-title { font-weight: 600; margin-bottom: 4px; }
 .feature-detail { padding: 8px; }
-</style>
+</style>

+ 344 - 299
src/views/subSystem/gas/gasOperamon/xldxkjkrqtndjc.vue

@@ -13,15 +13,7 @@
         </div>
       </div>
       <div class="header-right">
-        <el-date-picker
-            v-model="dateRange"
-            type="datetimerange"
-            range-separator="至"
-            start-placeholder="开始时间"
-            end-placeholder="结束时间"
-            style="width: 280px"
-            size="default"
-        />
+        <el-input v-model="searchKeyword" placeholder="搜索点位/地址" clearable :prefix-icon="Search" style="width:200px" />
         <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
       </div>
@@ -37,7 +29,7 @@
       <div class="kpi-card">
         <div class="kpi-value">{{ avgConcentration }}<span class="unit">%VOL</span></div>
         <div class="kpi-label">平均浓度</div>
-        <div class="kpi-trend">较昨日 ↑0.02%</div>
+        <div class="kpi-trend">监测点位 {{ totalPoints }} 个</div>
       </div>
       <div class="kpi-card">
         <div class="kpi-value">{{ onlineRate }}<span class="unit">%</span></div>
@@ -47,36 +39,32 @@
       <div class="kpi-card">
         <div class="kpi-value">{{ coverageRate }}<span class="unit">%</span></div>
         <div class="kpi-label">管网覆盖率</div>
-        <div class="kpi-trend">监测点位 {{ totalPoints }} 个</div>
+        <div class="kpi-trend">覆盖 {{ coveredPipelines }} 条管线</div>
       </div>
     </div>
 
-    <!-- 主内容区:左侧点位列表 + 右侧地图与详情 -->
+    <!-- 主内容区 -->
     <div class="main-layout">
       <!-- 左侧监测点位列表 -->
       <div class="point-list-panel">
         <div class="list-header">
           <span>监测点位列表</span>
-          <el-input
-              v-model="searchKeyword"
-              placeholder="搜索点位/地址"
-              clearable
-              :prefix-icon="Search"
-              style="width: 160px"
-              size="small"
-          />
+          <span class="total-num">共 {{ filteredPoints.length }} 个</span>
+        </div>
+        <div class="filter-row">
+          <el-select v-model="alertFilter" placeholder="告警等级" clearable size="small" style="width:100%">
+            <el-option label="严重告警" value="critical" />
+            <el-option label="预警" value="warning" />
+            <el-option label="正常" value="normal" />
+          </el-select>
         </div>
         <div class="point-list">
           <div
-              v-for="point in paginatedPoints"
-              :key="point.id"
-              class="point-item"
-              :class="{
-              selected: selectedPoint?.id === point.id,
-              critical: point.alertLevel === 'critical',
-              warning: point.alertLevel === 'warning'
-            }"
-              @click="selectPoint(point)"
+            v-for="point in paginatedPoints"
+            :key="point.id"
+            class="point-item"
+            :class="{ selected: selectedPoint?.id === point.id, critical: point.alertLevel === 'critical', warning: point.alertLevel === 'warning' }"
+            @click="selectPoint(point)"
           >
             <div class="point-header">
               <span class="point-name">{{ point.name }}</span>
@@ -84,25 +72,16 @@
                 {{ point.alertLevel === 'critical' ? '严重' : point.alertLevel === 'warning' ? '预警' : '正常' }}
               </el-tag>
             </div>
-            <div class="point-address">
-              <el-icon><Location /></el-icon> {{ point.address }}
-            </div>
-            <div class="point-concentration" :class="{ 'high': point.methane > 1 }">
-              甲烷浓度: {{ point.methane }} %VOL
-            </div>
-            <div class="point-device">
-              <el-icon><Monitor /></el-icon> 设备: {{ point.deviceModel }} | 电量: {{ point.battery }}%
+            <div class="point-address">{{ point.address }}</div>
+            <div class="point-concentration" :class="{ high: point.methane > 1.0 }">
+              甲烷: <b>{{ point.methane }}</b> %VOL
             </div>
+            <div class="point-device">设备: {{ point.deviceModel }} | 电量: {{ point.battery }}% | {{ point.updateTime }}</div>
           </div>
+          <el-empty v-if="filteredPoints.length === 0" description="暂无匹配点位" :image-size="50" />
         </div>
         <div class="pagination-area">
-          <el-pagination
-              small
-              layout="prev, pager, next"
-              :total="filteredPoints.length"
-              :page-size="pageSize"
-              v-model:current-page="currentPage"
-          />
+          <el-pagination small layout="prev, pager, next" :total="filteredPoints.length" :page-size="pageSize" v-model:current-page="currentPage" />
         </div>
       </div>
 
@@ -112,35 +91,30 @@
           <el-button-group>
             <el-button size="small" :icon="ZoomIn" @click="zoomIn">放大</el-button>
             <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
-            <el-button size="small" :icon="Location" @click="fitBounds">图</el-button>
+            <el-button size="small" :icon="Location" @click="fitBounds">重置视图</el-button>
           </el-button-group>
           <div class="legend">
             <span><i class="legend-dot critical-dot"></i>严重告警 (>1%VOL)</span>
-            <span><i class="legend-dot warning-dot"></i>预警 (0.5%-1%)</span>
+            <span><i class="legend-dot warning-dot"></i>预警 (0.5~1%)</span>
             <span><i class="legend-dot normal-dot"></i>正常 (<0.5%)</span>
             <span><i class="legend-dot offline-dot"></i>离线设备</span>
           </div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="methaneMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
-          <div v-if="hoverPoint" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoverPoint.name }}</div>
-            <div>甲烷浓度: {{ hoverPoint.methane }} %VOL</div>
-            <div>设备状态: {{ hoverPoint.deviceStatus === 'online' ? '在线' : '离线' }}</div>
-            <div>更新时间: {{ hoverPoint.updateTime }}</div>
-          </div>
+        <div class="map-wrapper">
+          <div id="methaneBaiduMap" class="map-div"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
 
-        <!-- 点位详情侧边栏 -->
-        <el-drawer v-model="detailDrawerVisible" :title="`监测点位详情 - ${currentPoint?.name}`" size="480px" direction="rtl">
+        <!-- 点位详情抽屉 -->
+        <el-drawer v-model="detailDrawerVisible" :title="`监测点位详情 - ${currentPoint?.name || ''}`" size="480px" direction="rtl">
           <div v-if="currentPoint" class="point-detail">
-            <!-- 基本信息 -->
-            <el-descriptions :column="1" border>
+            <el-descriptions :column="1" border size="small">
               <el-descriptions-item label="点位名称">{{ currentPoint.name }}</el-descriptions-item>
               <el-descriptions-item label="点位编号">{{ currentPoint.code }}</el-descriptions-item>
               <el-descriptions-item label="详细地址">{{ currentPoint.address }}</el-descriptions-item>
               <el-descriptions-item label="经纬度">{{ currentPoint.lng }}, {{ currentPoint.lat }}</el-descriptions-item>
-              <el-descriptions-item label="所属管网">{{ currentPoint.pipelineName }}</el-descriptions-item>
+              <el-descriptions-item label="所属管线">{{ currentPoint.pipelineName }}</el-descriptions-item>
+              <el-descriptions-item label="空间类型">{{ currentPoint.spaceType }}</el-descriptions-item>
               <el-descriptions-item label="风险等级">
                 <el-tag :type="currentPoint.alertLevel === 'critical' ? 'danger' : currentPoint.alertLevel === 'warning' ? 'warning' : 'success'">
                   {{ currentPoint.alertLevel === 'critical' ? '严重' : currentPoint.alertLevel === 'warning' ? '预警' : '正常' }}
@@ -148,11 +122,10 @@
               </el-descriptions-item>
             </el-descriptions>
 
-            <!-- 监测数据 -->
             <div class="data-section">
               <div class="section-title">实时监测数据</div>
               <div class="data-cards">
-                <div class="data-card" :class="{ warning: currentPoint.methane > 1 }">
+                <div class="data-card" :class="{ danger: currentPoint.methane > 1.0, warning: currentPoint.methane > 0.5 && currentPoint.methane <= 1.0 }">
                   <div class="data-label">甲烷浓度</div>
                   <div class="data-value">{{ currentPoint.methane }} <span class="unit">%VOL</span></div>
                 </div>
@@ -162,12 +135,11 @@
                 </div>
                 <div class="data-card">
                   <div class="data-label">环境湿度</div>
-                  <div class="data-value">{{ currentPoint.humidity }} <span class="unit">%</span></div>
+                  <div class="data-value">{{ currentPoint.humidity }} <span class="unit">%RH</span></div>
                 </div>
               </div>
             </div>
 
-            <!-- 监测设备信息 -->
             <div class="device-section">
               <div class="section-title">监测设备信息</div>
               <el-descriptions :column="1" border size="small">
@@ -186,19 +158,15 @@
               </el-descriptions>
             </div>
 
-            <!-- 历史浓度趋势 -->
             <div class="trend-section">
-              <div class="section-title">历史浓度趋势(最近24小时)</div>
-              <div class="trend-chart">
-                <v-chart :option="trendChartOption" autoresize />
-              </div>
+              <div class="section-title">浓度趋势(最近24小时)</div>
+              <div ref="trendChartRef" class="trend-chart"></div>
             </div>
 
-            <!-- 告警记录 -->
             <div class="alert-section" v-if="currentPoint.alertRecords?.length">
               <div class="section-title">告警记录</div>
               <el-timeline>
-                <el-timeline-item v-for="record in currentPoint.alertRecords" :key="record.id" :timestamp="record.time" :type="record.type">
+                <el-timeline-item v-for="record in currentPoint.alertRecords" :key="record.id" :timestamp="record.time" :type="record.type === 'danger' ? 'danger' : 'warning'">
                   {{ record.content }}
                 </el-timeline-item>
               </el-timeline>
@@ -211,57 +179,114 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
-import { Warning, Refresh, Download, Search, Location, Monitor, ZoomIn, ZoomOut } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
+import { Warning, Refresh, Download, Search, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
+// ==================== 沅陵县太常片区相邻地下空间甲烷监测数据 ====================
+const MAP_CENTER = { lng: 110.395, lat: 28.470 }
+const MAP_DEFAULT_ZOOM = 14
 
-// 模拟监测点位数据
 const mockPoints = ref([
-  { id: 1, code: 'MP-001', name: '人民广场监测点', address: '人民大道200号地下管廊', lng: 121.487, lat: 31.239,
-    methane: 0.32, temperature: 18.5, humidity: 65, pipelineName: '人民路中压管线', alertLevel: 'normal',
-    deviceCode: 'GAS-1001', deviceModel: 'GT-2000', manufacturer: '华盾科技', installDate: '2024-03-15',
-    deviceStatus: 'online', battery: 87, signal: 92, updateTime: '2025-04-02 15:23:45',
+  {
+    id: 1, code: 'CH4-YL-001', name: '辰州北路地下管廊1#', address: '沅陵县太常片区辰州北路地下综合管廊南段',
+    lng: 110.3940, lat: 28.4765, methane: 1.42, temperature: 20.1, humidity: 68,
+    pipelineName: '辰州北路高压段', spaceType: '地下综合管廊', alertLevel: 'critical',
+    deviceCode: 'GT-CH4-1001', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-03-15',
+    deviceStatus: 'online', battery: 68, signal: 82, updateTime: '2026-06-13 15:23:45',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 13:20:00', type: 'danger', content: '甲烷浓度1.42%VOL,严重超限,已触发联动排风' },
+      { id: 2, time: '2026-06-13 10:15:00', type: 'warning', content: '甲烷浓度0.88%VOL,超过预警阈值0.5%' }
+    ],
+    trendData: [0.45, 0.48, 0.52, 0.58, 0.62, 0.68, 0.72, 0.78, 0.82, 0.88, 0.95, 1.05, 1.15, 1.25, 1.32, 1.38, 1.42, 1.42, 1.40, 1.38, 1.35, 1.32, 1.28, 1.25]
+  },
+  {
+    id: 2, code: 'CH4-YL-002', name: '古城中路地下空间2#', address: '沅陵县太常片区古城中路太常安置区地下车库B区',
+    lng: 110.3972, lat: 28.4710, methane: 0.68, temperature: 22.3, humidity: 72,
+    pipelineName: '古城中路中压段', spaceType: '地下车库', alertLevel: 'warning',
+    deviceCode: 'GT-CH4-1002', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-04-20',
+    deviceStatus: 'online', battery: 75, signal: 88, updateTime: '2026-06-13 15:22:30',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 14:30:00', type: 'warning', content: '甲烷浓度0.68%VOL,超过预警阈值0.5%,需关注' }
+    ],
+    trendData: [0.42, 0.44, 0.46, 0.48, 0.50, 0.52, 0.55, 0.58, 0.60, 0.62, 0.64, 0.66, 0.68, 0.68, 0.67, 0.66, 0.65, 0.64, 0.62, 0.60, 0.58, 0.55, 0.52, 0.50]
+  },
+  {
+    id: 3, code: 'CH4-YL-003', name: '迎宾东路地下通道3#', address: '沅陵县太常片区迎宾东路商贸城地下人行通道',
+    lng: 110.4055, lat: 28.4690, methane: 2.15, temperature: 23.5, humidity: 75,
+    pipelineName: '迎宾东路中压段', spaceType: '地下人行通道', alertLevel: 'critical',
+    deviceCode: 'GT-CH4-1003', deviceModel: 'GT-CX2000Pro', manufacturer: '汉威科技', installDate: '2025-05-10',
+    deviceStatus: 'online', battery: 52, signal: 71, updateTime: '2026-06-13 15:21:15',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 12:45:00', type: 'danger', content: '甲烷浓度2.15%VOL,严重超标,已通知燃气公司现场处置' },
+      { id: 2, time: '2026-06-13 08:30:00', type: 'danger', content: '甲烷浓度1.85%VOL,持续上升趋势' }
+    ],
+    trendData: [0.85, 0.92, 1.05, 1.18, 1.32, 1.45, 1.58, 1.68, 1.75, 1.82, 1.90, 1.98, 2.05, 2.10, 2.15, 2.15, 2.12, 2.10, 2.08, 2.05, 2.02, 1.98, 1.95, 1.90]
+  },
+  {
+    id: 4, code: 'CH4-YL-004', name: '建设路地下管沟4#', address: '沅陵县太常片区建设路县政府南侧地下管线沟',
+    lng: 110.3940, lat: 28.4656, methane: 0.18, temperature: 19.2, humidity: 62,
+    pipelineName: '建设路中压段', spaceType: '地下管线沟', alertLevel: 'normal',
+    deviceCode: 'GT-CH4-1004', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-06-01',
+    deviceStatus: 'online', battery: 91, signal: 93, updateTime: '2026-06-13 15:24:00',
     alertRecords: [],
-    trendData: [0.28, 0.30, 0.32, 0.31, 0.33, 0.32, 0.30, 0.29, 0.31, 0.32, 0.34, 0.33, 0.32, 0.31, 0.30, 0.32, 0.33, 0.35, 0.34, 0.32, 0.31, 0.30, 0.29, 0.28] },
-  { id: 2, code: 'MP-002', name: '世纪大道监测点', address: '世纪大道福山路口', lng: 121.505, lat: 31.235,
-    methane: 0.68, temperature: 19.2, humidity: 68, pipelineName: '世纪大道中压管线', alertLevel: 'warning',
-    deviceCode: 'GAS-1002', deviceModel: 'GT-2000', manufacturer: '华盾科技', installDate: '2024-04-20',
-    deviceStatus: 'online', battery: 72, signal: 88, updateTime: '2025-04-02 15:22:30',
-    alertRecords: [{ id: 1, time: '2025-04-02 14:30:00', type: 'warning', content: '甲烷浓度0.68%VOL,超过预警阈值0.5%' }],
-    trendData: [0.42, 0.45, 0.48, 0.52, 0.55, 0.58, 0.62, 0.65, 0.68, 0.68, 0.67, 0.65, 0.63, 0.60, 0.58, 0.55, 0.52, 0.50, 0.48, 0.46, 0.45, 0.44, 0.43, 0.42] },
-  { id: 3, code: 'MP-003', name: '张杨路监测点', address: '张杨路松林路口', lng: 121.52, lat: 31.242,
-    methane: 1.42, temperature: 20.1, humidity: 72, pipelineName: '张杨路高压管线', alertLevel: 'critical',
-    deviceCode: 'GAS-1003', deviceModel: 'GT-2000Pro', manufacturer: '华盾科技', installDate: '2024-05-10',
-    deviceStatus: 'online', battery: 55, signal: 76, updateTime: '2025-04-02 15:21:15',
-    alertRecords: [{ id: 1, time: '2025-04-02 13:20:00', type: 'danger', content: '甲烷浓度1.42%VOL,严重超标' }],
-    trendData: [0.85, 0.92, 1.02, 1.12, 1.22, 1.32, 1.38, 1.42, 1.42, 1.40, 1.38, 1.35, 1.32, 1.28, 1.25, 1.22, 1.18, 1.15, 1.12, 1.08, 1.05, 1.00, 0.95, 0.90] },
-  { id: 4, code: 'MP-004', name: '龙阳路监测点', address: '龙阳路白杨路口', lng: 121.51, lat: 31.215,
-    methane: 0.15, temperature: 17.8, humidity: 60, pipelineName: '龙阳路中压管线', alertLevel: 'normal',
-    deviceCode: 'GAS-1004', deviceModel: 'GT-2000', manufacturer: '华盾科技', installDate: '2024-06-01',
-    deviceStatus: 'offline', battery: 0, signal: 0, updateTime: '2025-04-02 08:00:00',
+    trendData: [0.12, 0.13, 0.14, 0.14, 0.15, 0.15, 0.16, 0.16, 0.17, 0.17, 0.18, 0.18, 0.18, 0.17, 0.17, 0.16, 0.16, 0.15, 0.15, 0.14, 0.14, 0.13, 0.13, 0.12]
+  },
+  {
+    id: 6, code: 'CH4-YL-006', name: '天宁东路管沟6#', address: '沅陵县太常片区天宁东路末端综合管沟',
+    lng: 110.4088, lat: 28.4752, methane: 0.72, temperature: 22.0, humidity: 65,
+    pipelineName: '天宁路中压段', spaceType: '综合管沟', alertLevel: 'warning',
+    deviceCode: 'GT-CH4-1006', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-08-20',
+    deviceStatus: 'online', battery: 72, signal: 79, updateTime: '2026-06-13 15:19:45',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 15:00:00', type: 'warning', content: '甲烷浓度0.72%VOL,超过预警阈值' }
+    ],
+    trendData: [0.35, 0.38, 0.42, 0.45, 0.48, 0.52, 0.55, 0.58, 0.62, 0.65, 0.68, 0.70, 0.72, 0.72, 0.71, 0.70, 0.68, 0.66, 0.64, 0.62, 0.60, 0.58, 0.55, 0.52]
+  },
+  {
+    id: 7, code: 'CH4-YL-007', name: '辰州中路管廊入口7#', address: '沅陵县太常片区辰州中路与天宁路交叉口管廊入口',
+    lng: 110.3935, lat: 28.4710, methane: 0.35, temperature: 19.8, humidity: 66,
+    pipelineName: '辰州中路高压段', spaceType: '管廊入口井', alertLevel: 'normal',
+    deviceCode: 'GT-CH4-1007', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-09-10',
+    deviceStatus: 'online', battery: 88, signal: 91, updateTime: '2026-06-13 15:25:10',
     alertRecords: [],
-    trendData: [0.12, 0.13, 0.14, 0.15, 0.15, 0.14, 0.13, 0.12, 0.11, 0.12, 0.13, 0.14, 0.15, 0.14, 0.13, 0.12, 0.11, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.14] },
-  { id: 5, code: 'MP-005', name: '东方路监测点', address: '东方路蓝村路口', lng: 121.53, lat: 31.248,
-    methane: 0.82, temperature: 19.5, humidity: 66, pipelineName: '东方路中压管线', alertLevel: 'warning',
-    deviceCode: 'GAS-1005', deviceModel: 'GT-2000', manufacturer: '华盾科技', installDate: '2024-07-15',
-    deviceStatus: 'online', battery: 68, signal: 85, updateTime: '2025-04-02 15:20:45',
-    alertRecords: [{ id: 1, time: '2025-04-02 15:15:00', type: 'warning', content: '甲烷浓度0.82%VOL,超过预警阈值' }],
-    trendData: [0.55, 0.58, 0.62, 0.65, 0.68, 0.72, 0.75, 0.78, 0.82, 0.82, 0.80, 0.78, 0.75, 0.72, 0.70, 0.68, 0.65, 0.62, 0.60, 0.58, 0.55, 0.52, 0.50, 0.48] },
-  { id: 6, code: 'MP-006', name: '浦电路监测点', address: '浦电路向城路口', lng: 121.49, lat: 31.228,
-    methane: 0.08, temperature: 18.0, humidity: 63, pipelineName: '浦电路低压管线', alertLevel: 'normal',
-    deviceCode: 'GAS-1006', deviceModel: 'GT-2000', manufacturer: '华盾科技', installDate: '2024-08-20',
-    deviceStatus: 'online', battery: 94, signal: 95, updateTime: '2025-04-02 15:19:30',
+    trendData: [0.28, 0.29, 0.30, 0.31, 0.32, 0.33, 0.34, 0.34, 0.35, 0.35, 0.35, 0.34, 0.34, 0.33, 0.32, 0.31, 0.30, 0.29, 0.29, 0.28, 0.28, 0.28, 0.29, 0.30]
+  },
+  {
+    id: 8, code: 'CH4-YL-008', name: '古城北路管廊8#', address: '沅陵县太常片区古城北路与天宁路交叉口管廊检查井',
+    lng: 110.3978, lat: 28.4775, methane: 0.22, temperature: 21.5, humidity: 60,
+    pipelineName: '古城北路中压段', spaceType: '管廊检查井', alertLevel: 'normal',
+    deviceCode: 'GT-CH4-1008', deviceModel: 'GT-CX2000', manufacturer: '汉威科技', installDate: '2025-10-05',
+    deviceStatus: 'online', battery: 82, signal: 87, updateTime: '2026-06-13 15:23:00',
     alertRecords: [],
-    trendData: [0.06, 0.07, 0.08, 0.08, 0.07, 0.06, 0.05, 0.06, 0.07, 0.08, 0.08, 0.07, 0.06, 0.05, 0.06, 0.07, 0.08, 0.09, 0.08, 0.07, 0.06, 0.05, 0.06, 0.07] }
+    trendData: [0.18, 0.19, 0.19, 0.20, 0.20, 0.21, 0.21, 0.22, 0.22, 0.22, 0.23, 0.23, 0.22, 0.22, 0.21, 0.21, 0.20, 0.20, 0.19, 0.19, 0.18, 0.18, 0.19, 0.19]
+  },
+  {
+    id: 9, code: 'CH4-YL-009', name: '建设东路工业园9#', address: '沅陵县太常片区建设东路工业园地下管廊入口',
+    lng: 110.4050, lat: 28.4658, methane: 0.55, temperature: 24.2, humidity: 58,
+    pipelineName: '建设东路高压段', spaceType: '工业园地下管廊', alertLevel: 'warning',
+    deviceCode: 'GT-CH4-1009', deviceModel: 'GT-CX2000Pro', manufacturer: '汉威科技', installDate: '2025-11-20',
+    deviceStatus: 'online', battery: 65, signal: 74, updateTime: '2026-06-13 15:22:00',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 11:40:00', type: 'warning', content: '甲烷浓度0.55%VOL,略超预警阈值,持续观察中' }
+    ],
+    trendData: [0.30, 0.32, 0.35, 0.38, 0.40, 0.42, 0.45, 0.48, 0.50, 0.52, 0.55, 0.55, 0.54, 0.52, 0.50, 0.48, 0.46, 0.44, 0.42, 0.40, 0.38, 0.35, 0.33, 0.32]
+  },
+  {
+    id: 11, code: 'CH4-YL-011', name: '滨江东路商业街11#', address: '沅陵县太常片区滨江东路商业街地下管沟',
+    lng: 110.4066, lat: 28.4630, methane: 0.95, temperature: 22.8, humidity: 70,
+    pipelineName: '滨江东路低压段', spaceType: '商业街地下管沟', alertLevel: 'warning',
+    deviceCode: 'GT-CH4-1011', deviceModel: 'GT-CX2000Pro', manufacturer: '汉威科技', installDate: '2026-01-15',
+    deviceStatus: 'online', battery: 58, signal: 68, updateTime: '2026-06-13 15:20:50',
+    alertRecords: [
+      { id: 1, time: '2026-06-13 16:10:00', type: 'warning', content: '甲烷浓度0.95%VOL,接近严重告警阈值,需加密巡检' }
+    ],
+    trendData: [0.55, 0.58, 0.62, 0.65, 0.68, 0.72, 0.75, 0.78, 0.82, 0.85, 0.88, 0.90, 0.92, 0.95, 0.95, 0.94, 0.92, 0.90, 0.88, 0.85, 0.82, 0.78, 0.75, 0.72]
+  }
 ])
 
-// 统计数据
+// ==================== 统计数据 ====================
 const criticalCount = computed(() => mockPoints.value.filter(p => p.alertLevel === 'critical').length)
 const warningCount = computed(() => mockPoints.value.filter(p => p.alertLevel === 'warning').length)
 const normalCount = computed(() => mockPoints.value.filter(p => p.alertLevel === 'normal').length)
@@ -269,19 +294,22 @@ const totalPoints = computed(() => mockPoints.value.length)
 const totalDevices = computed(() => mockPoints.value.length)
 const onlineCount = computed(() => mockPoints.value.filter(p => p.deviceStatus === 'online').length)
 const onlineRate = computed(() => ((onlineCount.value / totalDevices.value) * 100).toFixed(1))
-const coverageRate = computed(() => 78)
+const coverageRate = computed(() => 82)
+const coveredPipelines = computed(() => 10)
 const maxConcentration = computed(() => Math.max(...mockPoints.value.map(p => p.methane)).toFixed(2))
-const maxConcentrationPoint = computed(() => mockPoints.value.find(p => p.methane === parseFloat(maxConcentration.value))?.name || '—')
+const maxConcentrationPoint = computed(() => mockPoints.value.find(p => p.methane === Math.max(...mockPoints.value.map(x => x.methane)))?.name || '—')
 const avgConcentration = computed(() => (mockPoints.value.reduce((sum, p) => sum + p.methane, 0) / totalPoints.value).toFixed(2))
 
-// 筛选
+// ==================== 筛选与分页 ====================
 const searchKeyword = ref('')
-const dateRange = ref([])
+const alertFilter = ref('')
 const currentPage = ref(1)
 const pageSize = 5
 const selectedPoint = ref(mockPoints.value[0])
 const currentPoint = ref(null)
 const detailDrawerVisible = ref(false)
+const trendChartRef = ref(null)
+let trendChartInstance = null
 
 const filteredPoints = computed(() => {
   let list = mockPoints.value
@@ -289,6 +317,7 @@ const filteredPoints = computed(() => {
     const kw = searchKeyword.value.toLowerCase()
     list = list.filter(p => p.name.toLowerCase().includes(kw) || p.address.toLowerCase().includes(kw))
   }
+  if (alertFilter.value) list = list.filter(p => p.alertLevel === alertFilter.value)
   return list
 })
 const paginatedPoints = computed(() => {
@@ -296,179 +325,180 @@ const paginatedPoints = computed(() => {
   return filteredPoints.value.slice(start, start + pageSize)
 })
 
-// 趋势图配置
-const trendChartOption = computed(() => {
-  if (!currentPoint.value) return {}
-  const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
-  return {
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
-    yAxis: { type: 'value', name: '甲烷浓度 (%VOL)' },
+// ==================== 选择点位 ====================
+function selectPoint(point) {
+  currentPoint.value = point
+  detailDrawerVisible.value = true
+  nextTick(() => renderTrendChart())
+}
+
+// ==================== ECharts趋势图 ====================
+function renderTrendChart() {
+  if (!trendChartRef.value || !currentPoint.value) return
+  if (!trendChartInstance) {
+    trendChartInstance = echarts.init(trendChartRef.value)
+  }
+  const times = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`)
+  const data = currentPoint.value.trendData
+
+  trendChartInstance.setOption({
+    tooltip: {
+      trigger: 'axis',
+      formatter: params => `${params[0].axisValue}<br/>甲烷浓度: <b>${params[0].value} %VOL</b>`
+    },
+    grid: { left: '3%', right: '5%', top: 10, bottom: 20, containLabel: true },
+    xAxis: { type: 'category', data: times, axisLabel: { fontSize: 10, interval: 4 }, axisTick: { show: false } },
+    yAxis: { type: 'value', name: '%VOL', min: 0, axisTick: { show: false }, splitLine: { lineStyle: { color: '#f0f0f0' } } },
     series: [{
-      name: '甲烷浓度',
-      type: 'line',
-      data: currentPoint.value.trendData,
-      smooth: true,
+      name: '甲烷浓度', type: 'line', data,
+      smooth: true, symbol: 'circle', symbolSize: 3,
       lineStyle: { color: '#f56c6c', width: 2 },
-      areaStyle: { opacity: 0.1, color: '#f56c6c' }
+      itemStyle: { color: '#f56c6c' },
+      areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(245,108,108,0.2)' }, { offset: 1, color: 'rgba(245,108,108,0.02)' }]) },
+      markLine: {
+        silent: true, symbol: 'none',
+        label: { fontSize: 10, formatter: '{b}' },
+        data: [
+          { name: '预警0.5%', yAxis: 0.5, lineStyle: { color: '#e6a23c', type: 'dashed', width: 1 } },
+          { name: '严重1.0%', yAxis: 1.0, lineStyle: { color: '#f56c6c', type: 'dashed', width: 1 } }
+        ]
+      }
     }]
-  }
-})
+  })
+}
 
-// GIS地图绘制
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
-const hoverPoint = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
-
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
+// ==================== 百度地图 ====================
+const mapError = ref('')
+let mapInstance = null
+let activeInfoWindow = null
+let mapMarkers = []
+
+function waitForBMapGL() {
+  if (typeof BMapGL !== 'undefined') { initMap(); return }
+  let attempts = 0
+  const timer = setInterval(() => {
+    attempts++
+    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
+    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
+  }, 200)
 }
 
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0)
-    ctx.lineTo(x, mapHeight)
-    ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y)
-    ctx.lineTo(mapWidth, y)
-    ctx.stroke()
+function closeInfoWindow() {
+  if (activeInfoWindow) {
+    try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}
+    activeInfoWindow = null
   }
 }
 
-function drawPoints() {
-  filteredPoints.value.forEach(point => {
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    if (x < -30 || x > mapWidth + 30 || y < -30 || y > mapHeight + 30) return
+function initMap() {
+  const el = document.getElementById('methaneBaiduMap')
+  if (!el) { mapError.value = '地图容器未找到。'; return }
 
-    let color = '#67c23a'
-    let size = 12
-    if (point.alertLevel === 'critical') {
-      color = '#f56c6c'
-      size = 16
-    } else if (point.alertLevel === 'warning') {
-      color = '#e6a23c'
-      size = 14
-    }
-    if (point.deviceStatus === 'offline') {
-      color = '#909399'
-      size = 10
-    }
+  try {
+    mapInstance = new BMapGL.Map('methaneBaiduMap', { enableMapClick: true })
+  } catch (e) {
+    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    return
+  }
 
-    ctx.beginPath()
-    ctx.arc(x, y, size, 0, 2 * Math.PI)
-    ctx.fillStyle = color
-    ctx.fill()
-    ctx.strokeStyle = '#fff'
-    ctx.lineWidth = 2
-    ctx.stroke()
-
-    if (point.alertLevel === 'critical') {
-      ctx.beginPath()
-      ctx.arc(x, y, size + 4, 0, 2 * Math.PI)
-      ctx.fillStyle = 'rgba(245,108,108,0.3)'
-      ctx.fill()
-    }
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  mapInstance.enableScrollWheelZoom(true)
+  mapInstance.setMinZoom(12)
+  mapInstance.setMaxZoom(18)
+
+  mapInstance.addEventListener('click', () => {
+    closeInfoWindow()
   })
-}
 
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawPoints()
+  renderMapMarkers()
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-  filteredPoints.value.forEach(point => {
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = point }
+function renderMapMarkers() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  closeInfoWindow()
+  mapMarkers.forEach(m => { try { mapInstance.removeOverlay(m) } catch (_) {} })
+  mapMarkers = []
+
+  mockPoints.value.forEach(p => {
+    const pt = new BMapGL.Point(p.lng, p.lat)
+    let color = '#67c23a'
+    let size = 12
+    if (p.alertLevel === 'critical') { color = '#f56c6c'; size = 16 }
+    else if (p.alertLevel === 'warning') { color = '#e6a23c'; size = 14 }
+    if (p.deviceStatus === 'offline') { color = '#909399'; size = 10 }
+
+    const html = `<div style="width:${size*2}px;height:${size*2}px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3);cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:bold;">CH4</div>`
+    const label = new BMapGL.Label(html, {
+      position: pt,
+      offset: new BMapGL.Size(-size, -size)
+    })
+    label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
+
+    label.addEventListener('click', (e) => {
+      if (e && e.domEvent) e.domEvent.stopPropagation()
+      openPointInfo(p)
+    })
+
+    mapInstance.addOverlay(label)
+    mapMarkers.push(label)
+
+    // 严重告警脉冲圈
+    if (p.alertLevel === 'critical') {
+      const pulseHtml = `<div style="width:${size*3}px;height:${size*3}px;border-radius:50%;background:rgba(245,108,108,0.25);border:2px solid rgba(245,108,108,0.5);"></div>`
+      const pulseLabel = new BMapGL.Label(pulseHtml, {
+        position: pt,
+        offset: new BMapGL.Size(-size * 1.5, -size * 1.5)
+      })
+      pulseLabel.setStyle({ border: 'none', background: 'transparent', padding: '0' })
+      mapInstance.addOverlay(pulseLabel)
+      mapMarkers.push(pulseLabel)
+    }
   })
-  if (hit) { currentPoint.value = hit; detailDrawerVisible.value = true }
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-  filteredPoints.value.forEach(point => {
-    const { x, y } = worldToCanvas(point.lng, point.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = point }
-  })
-  if (hit) {
-    hoverPoint.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else { hoverPoint.value = null }
+function openPointInfo(point) {
+  selectPoint(point)
+  closeInfoWindow()
+  const pt = new BMapGL.Point(point.lng, point.lat)
+  const levelText = point.alertLevel === 'critical' ? '严重告警' : point.alertLevel === 'warning' ? '预警' : '正常'
+  const html = `<div style="font-size:13px;line-height:1.8;max-width:240px;"><b>${point.name}</b><br/>甲烷: <b style="color:${point.methane > 1 ? '#f56c6c' : point.methane > 0.5 ? '#e6a23c' : '#67c23a'}">${point.methane} %VOL</b><br/>状态: ${levelText}<br/>地址: ${point.address}</div>`
+  activeInfoWindow = new BMapGL.InfoWindow(html, { width: 240, height: 120 })
+  mapInstance.openInfoWindow(activeInfoWindow, pt)
+  mapInstance.panTo(pt)
 }
 
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
-  })
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
-    renderMap()
-  })
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
-  })
+function zoomIn() { mapInstance?.zoomIn() }
+function zoomOut() { mapInstance?.zoomOut() }
+function fitBounds() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  closeInfoWindow()
 }
 
-const selectPoint = (point) => { currentPoint.value = point; detailDrawerVisible.value = true }
-const refreshData = () => { ElMessage.success('数据已刷新') }
-const exportReport = () => { ElMessage.success('导出报表(演示)') }
+function refreshData() {
+  renderMapMarkers()
+  ElMessage.success('数据已刷新')
+}
+function exportReport() { ElMessage.success('导出报表(演示功能)') }
 
-watch(() => filteredPoints.value, () => renderMap())
+// 搜索联动地图
+watch([searchKeyword, alertFilter], () => {
+  currentPage.value = 1
+})
 
 onMounted(() => {
-  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
+  nextTick(() => {
+    waitForBMapGL()
+  })
+  window.addEventListener('resize', () => { try { trendChartInstance?.resize() } catch {} })
+})
+
+onBeforeUnmount(() => {
+  try { mapInstance?.destroy() } catch (_) {}
+  try { trendChartInstance?.dispose() } catch (_) {}
+  mapInstance = null
+  trendChartInstance = null
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
 </script>
 
 <style scoped>
@@ -480,15 +510,16 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   overflow: hidden;
 }
 .page-header {
-  background: white;
+  background: #fff;
   padding: 12px 24px;
   display: flex;
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
 }
 .header-left { display: flex; align-items: center; gap: 20px; }
-.header-left .title { font-size: 1.3rem; font-weight: 600; }
+.header-left .title { font-size: 1.25rem; font-weight: 600; color: #1f2d3d; }
 .stats-badge { display: flex; gap: 20px; }
 .stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
 .stat-item.critical { color: #f56c6c; }
@@ -504,14 +535,15 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   grid-template-columns: repeat(4, 1fr);
   gap: 16px;
   padding: 16px;
-  background: white;
+  background: #fff;
   margin: 0 12px 12px 12px;
-  border-radius: 16px;
+  border-radius: 12px;
+  flex-shrink: 0;
 }
 .kpi-card { text-align: center; }
 .kpi-card.critical .kpi-value { color: #f56c6c; }
-.kpi-value { font-size: 32px; font-weight: bold; }
-.kpi-value .unit { font-size: 14px; font-weight: normal; }
+.kpi-value { font-size: 28px; font-weight: 700; }
+.kpi-value .unit { font-size: 13px; font-weight: 400; }
 .kpi-label { font-size: 13px; color: #909399; margin-top: 4px; }
 .kpi-trend { font-size: 11px; color: #67c23a; margin-top: 4px; }
 
@@ -522,80 +554,93 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   padding: 0 12px 12px 12px;
   gap: 12px;
 }
+
+/* 左侧列表 */
 .point-list-panel {
-  width: 360px;
-  background: white;
+  width: 370px;
+  background: #fff;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
   overflow: hidden;
+  flex-shrink: 0;
 }
 .list-header {
-  padding: 16px;
+  padding: 14px 16px;
   border-bottom: 1px solid #e9ecef;
   display: flex;
   justify-content: space-between;
+  align-items: center;
   font-weight: 600;
 }
-.point-list { flex: 1; overflow-y: auto; padding: 12px; }
+.total-num { font-size: 12px; color: #909399; font-weight: 400; }
+.filter-row { padding: 6px 16px; }
+.point-list { flex: 1; overflow-y: auto; padding: 4px 8px 8px; }
 .point-item {
   padding: 12px;
-  margin-bottom: 8px;
+  margin-bottom: 6px;
   border-radius: 12px;
   background: #fafbfc;
   border: 1px solid #e9ecef;
   cursor: pointer;
+  transition: all 0.2s;
 }
+.point-item:hover { background: #f0f5ff; }
 .point-item.selected { background: #ecf5ff; border-color: #409eff; }
 .point-item.critical { border-left: 3px solid #f56c6c; }
 .point-item.warning { border-left: 3px solid #e6a23c; }
-.point-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
-.point-name { font-weight: 600; }
-.point-address { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
+.point-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
+.point-name { font-weight: 600; font-size: 14px; }
+.point-address { font-size: 11px; color: #909399; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 .point-concentration { font-size: 13px; margin-bottom: 4px; }
-.point-concentration.high { color: #f56c6c; font-weight: bold; }
-.point-device { font-size: 11px; color: #909399; display: flex; align-items: center; gap: 4px; }
+.point-concentration b { color: #1f2d3d; }
+.point-concentration.high { color: #f56c6c; }
+.point-concentration.high b { color: #f56c6c; }
+.point-device { font-size: 11px; color: #c0c4cc; }
+.pagination-area { padding: 10px; text-align: center; border-top: 1px solid #eee; }
 
+/* 右侧地图 */
 .map-panel {
   flex: 1;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
   overflow: hidden;
 }
 .map-toolbar {
-  padding: 12px 16px;
+  padding: 10px 16px;
   border-bottom: 1px solid #e9ecef;
   display: flex;
   justify-content: space-between;
   align-items: center;
+  flex-shrink: 0;
 }
-.legend { display: flex; gap: 16px; font-size: 12px; }
-.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
+.legend { display: flex; gap: 16px; font-size: 12px; flex-wrap: wrap; }
+.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
 .critical-dot { background: #f56c6c; }
 .warning-dot { background: #e6a23c; }
 .normal-dot { background: #67c23a; }
 .offline-dot { background: #909399; }
-.map-wrapper { flex: 1; position: relative; cursor: crosshair; }
-canvas { width: 100%; height: 100%; display: block; }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
-  border-radius: 10px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
+.map-wrapper { flex: 1; position: relative; overflow: hidden; }
+.map-div { width: 100%; height: 100%; }
+.map-error {
+  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
+  color: #f56c6c; font-size: 14px; background: #fff; padding: 16px 24px;
+  border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);
 }
+
+/* 详情抽屉 */
 .point-detail { padding: 8px; }
 .section-title { font-weight: 600; margin: 16px 0 12px 0; font-size: 14px; }
 .data-cards { display: flex; gap: 12px; }
 .data-card { flex: 1; background: #f8f9fc; border-radius: 12px; padding: 12px; text-align: center; }
-.data-card.warning { background: #fff8f0; border: 1px solid #f56c6c; }
+.data-card.danger { background: #fef0f0; border: 1px solid #f56c6c; }
+.data-card.warning { background: #fdf6ec; border: 1px solid #e6a23c; }
 .data-label { font-size: 12px; color: #909399; }
-.data-value { font-size: 20px; font-weight: bold; margin-top: 4px; }
-.trend-chart { height: 180px; }
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
-</style>
+.data-value { font-size: 20px; font-weight: 700; margin-top: 4px; }
+.data-value .unit { font-size: 11px; font-weight: 400; color: #909399; }
+.trend-chart { height: 200px; }
+.device-section { margin-top: 8px; }
+.alert-section { margin-top: 8px; }
+</style>

+ 654 - 310
src/views/subSystem/gas/gasfxpg/gwfxpg.vue

@@ -14,11 +14,11 @@
       </div>
       <div class="header-right">
         <el-input
-            v-model="searchKeyword"
-            placeholder="搜索管段名称/编号/位置"
-            clearable
-            :prefix-icon="Search"
-            style="width: 240px"
+          v-model="searchKeyword"
+          placeholder="搜索管段名称/编号"
+          clearable
+          :prefix-icon="Search"
+          style="width: 220px"
         />
         <el-button type="primary" :icon="Location" @click="locateSearch">定位</el-button>
         <el-button :icon="Refresh" @click="refreshMap">刷新</el-button>
@@ -32,7 +32,7 @@
       <div class="control-panel">
         <div class="panel-section">
           <div class="section-title">风险图层</div>
-          <el-checkbox-group v-model="visibleLayers">
+          <el-checkbox-group v-model="visibleLayers" @change="onLayerChange">
             <div class="layer-item">
               <el-checkbox value="high">高风险管段</el-checkbox>
               <span class="layer-badge high">●</span>
@@ -45,28 +45,29 @@
               <el-checkbox value="low">低风险管段</el-checkbox>
               <span class="layer-badge low">●</span>
             </div>
+            <div class="layer-item">
+              <el-checkbox value="hazard">周边危险源</el-checkbox>
+              <span class="layer-badge hazard">●</span>
+            </div>
+            <div class="layer-item">
+              <el-checkbox value="target">防护目标</el-checkbox>
+              <span class="layer-badge target">●</span>
+            </div>
           </el-checkbox-group>
         </div>
 
         <div class="panel-section">
           <div class="section-title">风险筛选</div>
-          <el-slider v-model="riskScoreRange" range :min="0" :max="100" :marks="riskMarks" />
-          <div class="range-labels">
-            <span>0分</span>
-            <span>25</span>
-            <span>50</span>
-            <span>75</span>
-            <span>100分</span>
-          </div>
+          <el-slider v-model="riskScoreRange" range :min="0" :max="100" :marks="riskMarks" @change="onScoreFilter" />
         </div>
 
         <div class="panel-section">
           <div class="section-title">快速定位</div>
           <el-select v-model="quickLocation" placeholder="选择区域" size="small" style="width: 100%" @change="locateArea">
-            <el-option label="世纪大道沿线" value="area1" />
-            <el-option label="张杨路沿线" value="area2" />
-            <el-option label="龙阳路沿线" value="area3" />
-            <el-option label="人民广场周边" value="area4" />
+            <el-option label="辰州路沿线" value="area1" />
+            <el-option label="古城路沿线" value="area2" />
+            <el-option label="建设路沿线" value="area3" />
+            <el-option label="太常片区全貌" value="area4" />
           </el-select>
         </div>
 
@@ -77,6 +78,8 @@
             <div class="stat-row"><span>中风险管段:</span><strong class="medium-text">{{ mediumRiskCount }}</strong><span>段</span></div>
             <div class="stat-row"><span>低风险管段:</span><strong class="low-text">{{ lowRiskCount }}</strong><span>段</span></div>
             <div class="stat-row"><span>平均风险评分:</span><strong>{{ avgRiskScore }}</strong><span>分</span></div>
+            <div class="stat-row"><span>周边危险源:</span><strong class="high-text">{{ hazardCount }}</strong><span>处</span></div>
+            <div class="stat-row"><span>防护目标:</span><strong class="medium-text">{{ targetCount }}</strong><span>处</span></div>
           </div>
         </div>
 
@@ -84,7 +87,7 @@
           <div class="section-title">图例说明</div>
           <div class="legend-item"><span class="legend-line high-line"></span> 高风险管线 (≥70分)</div>
           <div class="legend-item"><span class="legend-line medium-line"></span> 中风险管线 (40-69分)</div>
-          <div class="legend-item"><span class="legend-line low-line"></span> 低风险管线 (<40分)</div>
+          <div class="legend-item"><span class="legend-line low-line"></span> 低风险管线 (&lt;40分)</div>
           <div class="legend-item"><span class="legend-point hazard-point"></span> 周边危险源</div>
           <div class="legend-item"><span class="legend-point target-point"></span> 防护目标</div>
         </div>
@@ -98,31 +101,29 @@
             <el-button size="small" :icon="ZoomOut" @click="zoomOut">缩小</el-button>
             <el-button size="small" :icon="Location" @click="fitBounds">重置</el-button>
           </el-button-group>
-          <div class="coordinate-info">
-            坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}
-          </div>
+          <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="pipelineRiskMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="updateCoordinates"></canvas>
+        <div class="map-wrapper">
+          <div id="riskAssessmentBaiduMap" class="map-div"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
           <!-- 悬浮信息卡 -->
           <div v-if="hoverPipeline" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
             <div class="hover-title">{{ hoverPipeline.name }}</div>
-            <div>风险等级: {{ hoverPipeline.riskLevel === 'high' ? '高风险' : hoverPipeline.riskLevel === 'medium' ? '中风险' : '低风险' }}</div>
+            <div>风险等级: {{ hoverPipeline.riskLevelName }}</div>
             <div>风险评分: {{ hoverPipeline.riskScore }}分</div>
             <div>管材: {{ hoverPipeline.material }}</div>
-            <div>压力级别: {{ hoverPipeline.pressureLevel }}</div>
           </div>
         </div>
 
         <!-- 详情抽屉 -->
-        <el-drawer v-model="detailDrawerVisible" :title="`管段详情 - ${currentPipeline?.name}`" size="480px" direction="rtl">
+        <el-drawer v-model="detailDrawerVisible" :title="`管段详情 - ${currentPipeline?.name}`" size="520px" direction="rtl" @opened="onDrawerOpened">
           <div v-if="currentPipeline" class="pipeline-detail">
-            <el-descriptions :column="1" border>
+            <el-descriptions :column="1" border size="small">
               <el-descriptions-item label="管段名称">{{ currentPipeline.name }}</el-descriptions-item>
               <el-descriptions-item label="管段编号">{{ currentPipeline.code }}</el-descriptions-item>
               <el-descriptions-item label="风险等级">
-                <el-tag :type="currentPipeline.riskLevel === 'high' ? 'danger' : currentPipeline.riskLevel === 'medium' ? 'warning' : 'success'">
-                  {{ currentPipeline.riskLevel === 'high' ? '高风险' : currentPipeline.riskLevel === 'medium' ? '中风险' : '低风险' }}
+                <el-tag :type="currentPipeline.riskLevel === 'high' ? 'danger' : currentPipeline.riskLevel === 'medium' ? 'warning' : 'success'" size="small">
+                  {{ currentPipeline.riskLevelName }}
                 </el-tag>
               </el-descriptions-item>
               <el-descriptions-item label="风险评分">{{ currentPipeline.riskScore }}分</el-descriptions-item>
@@ -140,6 +141,10 @@
                 <li v-for="(cause, idx) in currentPipeline.riskCauses" :key="idx">{{ cause }}</li>
               </ul>
             </div>
+            <div class="chart-section">
+              <div class="sub-title">近6次风险评估趋势</div>
+              <div ref="chartRef" class="trend-chart"></div>
+            </div>
             <div class="action-buttons">
               <el-button type="primary" size="small" @click="viewDetailReport">查看详细报告</el-button>
               <el-button size="small" @click="locatePipeline">地图定位</el-button>
@@ -152,70 +157,166 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
 
-// 可见图层
-const visibleLayers = ref(['high', 'medium', 'low'])
-const riskScoreRange = ref([0, 100])
-const searchKeyword = ref('')
-const quickLocation = ref('')
+// ==================== 百度地图初始化 ====================
+const MAP_CENTER = { lng: 110.3940, lat: 28.4690 }
+const MAP_DEFAULT_ZOOM = 14
 
-// 风险评分区间标记
-const riskMarks = { 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }
+let mapInstance = null
+const mapError = ref('')
+const currentLng = ref(110.394)
+const currentLat = ref(28.469)
+
+const overlayStore = {
+  high: [], medium: [], low: [], hazard: [], target: []
+}
 
-// 模拟管线数据(含风险等级和坐标路径)
+function waitForBMapGL() {
+  return new Promise((resolve, reject) => {
+    let attempts = 0
+    const timer = setInterval(() => {
+      attempts++
+      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL) }
+      if (attempts > 150) { clearInterval(timer); reject(new Error('百度地图API加载超时')) }
+    }, 200)
+  })
+}
+
+function initMap() {
+  const BMapGL = window.BMapGL
+  const el = document.getElementById('riskAssessmentBaiduMap')
+  if (!el) return
+  mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  mapInstance.enableScrollWheelZoom(true)
+  mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
+  mapInstance.addEventListener('mousemove', (e) => {
+    currentLng.value = e.latlng.lng
+    currentLat.value = e.latlng.lat
+  })
+  renderAllOverlays()
+}
+
+// ==================== 沅陵县静态数据 ====================
+
+function riskLevelName(level) {
+  return level === 'high' ? '高风险' : level === 'medium' ? '中风险' : '低风险'
+}
+
+// 管线风险数据(8段)
 const pipelines = ref([
   {
-    id: 1, code: 'PL-001', name: '世纪大道中压管线', area: '浦东新区世纪大道沿线',
+    id: 1, code: 'PL-YL-001', name: '辰州路高压管线', area: '沅陵县太常片区辰州路',
     riskLevel: 'high', riskScore: 86.5,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 12.5,
-    buryYear: '2018年', owner: '浦东燃气公司', lastInspection: '2025-02-15',
-    coordinates: [[121.48, 31.24], [121.50, 31.245], [121.52, 31.24], [121.54, 31.235]],
-    riskCauses: ['管道服役年限较长', '周边施工活动频繁', '防腐层检测有局部破损', '位于交通密集区域']
+    riskLevelName: '高风险',
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 3.8,
+    buryYear: '2015年', owner: '沅陵燃气有限公司', lastInspection: '2026-03-15',
+    path: [[110.3940, 28.4820], [110.3942, 28.4770], [110.3938, 28.4710], [110.3936, 28.4645], [110.3934, 28.4580]],
+    riskCauses: ['管道服役超过10年', '辰州路主干道交通密集', '防腐层检测发现多处局部破损', '沿线有市政施工活动'],
+    trendData: [82.3, 83.1, 84.2, 85.0, 85.8, 86.5]
+  },
+  {
+    id: 2, code: 'PL-YL-002', name: '建设东路高压管', area: '沅陵县太常片区建设东路',
+    riskLevel: 'high', riskScore: 79.2,
+    riskLevelName: '高风险',
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 400, length: 3.6,
+    buryYear: '2016年', owner: '沅陵燃气有限公司', lastInspection: '2025-11-08',
+    path: [[110.3824, 28.4660], [110.3935, 28.4662], [110.4050, 28.4658]],
+    riskCauses: ['高压运行风险等级高', '周边有化工企业', '检测发现多处腐蚀点', '管道穿越工业园区域'],
+    trendData: [74.8, 75.6, 76.5, 77.3, 78.1, 79.2]
   },
   {
-    id: 2, code: 'PL-002', name: '张杨路高压管线', area: '浦东新区张杨路沿线',
+    id: 3, code: 'PL-YL-003', name: '古城路中压管线', area: '沅陵县太常片区古城路',
     riskLevel: 'medium', riskScore: 64.3,
-    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 18.2,
-    buryYear: '2020年', owner: '浦西燃气公司', lastInspection: '2025-03-10',
-    coordinates: [[121.48, 31.255], [121.50, 31.25], [121.515, 31.248], [121.53, 31.245]],
-    riskCauses: ['高压运行风险', '沿线有在建工程', '部分区域防腐层老化']
+    riskLevelName: '中风险',
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.2,
+    buryYear: '2018年', owner: '沅陵燃气有限公司', lastInspection: '2026-02-10',
+    path: [[110.3970, 28.4820], [110.3973, 28.4760], [110.3970, 28.4700], [110.3963, 28.4635]],
+    riskCauses: ['沿线有在建工程', '部分区域防腐层老化', '古城路商业区人流量大', '管线穿越古城地下管沟区域'],
+    trendData: [60.1, 61.0, 62.1, 63.2, 63.8, 64.3]
   },
   {
-    id: 3, code: 'PL-003', name: '浦东南路低压管线', area: '浦东新区浦东南路',
-    riskLevel: 'low', riskScore: 28.6,
-    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 8.6,
-    buryYear: '2022年', owner: '浦东燃气公司', lastInspection: '2025-01-20',
-    coordinates: [[121.485, 31.258], [121.495, 31.252], [121.50, 31.248]],
-    riskCauses: ['新建管道状况良好', '低压运行风险可控', '周边环境稳定']
+    id: 4, code: 'PL-YL-004', name: '迎宾路中压管线', area: '沅陵县太常片区迎宾路',
+    riskLevel: 'medium', riskScore: 55.8,
+    riskLevelName: '中风险',
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.6,
+    buryYear: '2019年', owner: '沅陵燃气有限公司', lastInspection: '2026-04-28',
+    path: [[110.3824, 28.4688], [110.3930, 28.4692], [110.4054, 28.4685]],
+    riskCauses: ['管道运行正常但有轻微腐蚀', '迎宾路商业区人流较大', '沿线有电力管沟交叉'],
+    trendData: [52.0, 52.8, 53.5, 54.2, 55.0, 55.8]
   },
   {
-    id: 4, code: 'PL-004', name: '龙阳路中压管线', area: '浦东新区龙阳路',
-    riskLevel: 'high', riskScore: 79.2,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 10.3,
-    buryYear: '2016年', owner: '浦西燃气公司', lastInspection: '2024-11-08',
-    coordinates: [[121.47, 31.22], [121.49, 31.218], [121.51, 31.215], [121.53, 31.212]],
-    riskCauses: ['管道服役超过8年', '检测发现多处腐蚀点', '周边有化工企业']
+    id: 5, code: 'PL-YL-005', name: '天宁路中压管线', area: '沅陵县太常片区天宁路',
+    riskLevel: 'medium', riskScore: 48.2,
+    riskLevelName: '中风险',
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 3.8,
+    buryYear: '2020年', owner: '沅陵燃气有限公司', lastInspection: '2026-05-12',
+    path: [[110.3850, 28.4755], [110.3935, 28.4758], [110.4088, 28.4750]],
+    riskCauses: ['PE管接头检测维护', '天宁西路有地下车库施工', '管道穿越安置区'],
+    trendData: [44.5, 45.3, 46.1, 47.0, 47.6, 48.2]
   },
   {
-    id: 5, code: 'PL-005', name: '东方路中压管线', area: '浦东新区东方路',
-    riskLevel: 'medium', riskScore: 55.8,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 7.8,
-    buryYear: '2019年', owner: '浦东燃气公司', lastInspection: '2025-02-28',
-    coordinates: [[121.52, 31.232], [121.53, 31.238], [121.535, 31.245], [121.54, 31.25]],
-    riskCauses: ['位于商业区人流量大', '管道运行正常但有轻微腐蚀']
+    id: 6, code: 'PL-YL-006', name: '辰州北路中压管线', area: '沅陵县太常片区辰州北路',
+    riskLevel: 'medium', riskScore: 42.5,
+    riskLevelName: '中风险',
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 200, length: 1.2,
+    buryYear: '2021年', owner: '沅陵燃气有限公司', lastInspection: '2026-05-28',
+    path: [[110.3940, 28.4820], [110.3942, 28.4855]],
+    riskCauses: ['辰州北路综合管廊相邻区域', '管段较短但周边环境复杂'],
+    trendData: [39.2, 40.0, 40.8, 41.5, 42.0, 42.5]
+  },
+  {
+    id: 7, code: 'PL-YL-007', name: '滨江路低压管线', area: '沅陵县太常片区滨江路',
+    riskLevel: 'low', riskScore: 28.6,
+    riskLevelName: '低风险',
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 3.8,
+    buryYear: '2022年', owner: '沅陵燃气有限公司', lastInspection: '2026-06-03',
+    path: [[110.3820, 28.4635], [110.3930, 28.4638], [110.4066, 28.4630]],
+    riskCauses: ['新建管道状况良好', '低压运行风险可控', '滨江路河堤段需关注水土流失'],
+    trendData: [25.0, 25.8, 26.5, 27.2, 27.9, 28.6]
+  },
+  {
+    id: 8, code: 'PL-YL-008', name: '太常南路低压管线', area: '沅陵县太常片区太常南路',
+    riskLevel: 'low', riskScore: 18.3,
+    riskLevelName: '低风险',
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 160, length: 1.5,
+    buryYear: '2023年', owner: '沅陵燃气有限公司', lastInspection: '2026-06-08',
+    path: [[110.3934, 28.4580], [110.3932, 28.4555]],
+    riskCauses: ['新建管道风险低', '周边环境稳定', '位于城南新开发区域'],
+    trendData: [15.8, 16.3, 16.9, 17.4, 17.9, 18.3]
   }
 ])
 
-// 统计
-const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'high').length)
-const mediumRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'medium').length)
-const lowRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'low').length)
-const avgRiskScore = computed(() => (pipelines.value.reduce((sum, p) => sum + p.riskScore, 0) / pipelines.value.length).toFixed(1))
+// 周边危险源
+const hazardPoints = ref([
+  { id: 'HZ1', name: '太常LNG储配站', type: '危险源', level: 'high', lng: 110.4041, lat: 28.4753, desc: '液化天然气储配站,储气量5000m³', riskRadius: '500m' },
+  { id: 'HZ2', name: '工业园计量站', type: '危险源', level: 'high', lng: 110.4052, lat: 28.4580, desc: '工业园燃气计量调压站', riskRadius: '300m' },
+  { id: 'HZ3', name: '城南加油站', type: '危险源', level: 'medium', lng: 110.3940, lat: 28.4560, desc: '加油站,与燃气管线水平距离约150m', riskRadius: '200m' },
+  { id: 'HZ4', name: '太常化工仓库', type: '危险源', level: 'medium', lng: 110.4070, lat: 28.4670, desc: '化工原料存储仓库', riskRadius: '400m' }
+])
+
+// 防护目标
+const targetPoints = ref([
+  { id: 'TG1', name: '太常中学', type: '防护目标', level: 'high', lng: 110.3930, lat: 28.4720, desc: '在校师生约2000人,距最近管线约200m' },
+  { id: 'TG2', name: '县人民医院太常分院', type: '防护目标', level: 'high', lng: 110.3950, lat: 28.4680, desc: '二级医院,距最近管线约150m' },
+  { id: 'TG3', name: '太常安置小区', type: '防护目标', level: 'medium', lng: 110.3910, lat: 28.4770, desc: '居民安置小区,约800户,距最近管线约100m' },
+  { id: 'TG4', name: '县政府办公区', type: '防护目标', level: 'medium', lng: 110.3880, lat: 28.4740, desc: '政府办公区域,距最近管线约300m' },
+  { id: 'TG5', name: '滨江公园', type: '防护目标', level: 'low', lng: 110.4000, lat: 28.4620, desc: '市民休闲公园,周末人流量较大' }
+])
+
+// 风险评分区间标记
+const riskMarks = { 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }
+
+// ==================== 图层 & 筛选 ====================
+const visibleLayers = ref(['high', 'medium', 'low', 'hazard', 'target'])
+const riskScoreRange = ref([0, 100])
+const searchKeyword = ref('')
+const quickLocation = ref('')
 
-// 过滤后的管线(按风险评分范围)
 const filteredPipelines = computed(() => {
   let list = pipelines.value
   if (riskScoreRange.value) {
@@ -224,219 +325,220 @@ const filteredPipelines = computed(() => {
   return list
 })
 
-// GIS画布相关
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
-const currentLng = ref(121.505)
-const currentLat = ref(31.235)
+// ==================== 统计 ====================
+const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'high').length)
+const mediumRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'medium').length)
+const lowRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'low').length)
+const avgRiskScore = computed(() => (pipelines.value.reduce((sum, p) => sum + p.riskScore, 0) / pipelines.value.length).toFixed(1))
+const hazardCount = computed(() => hazardPoints.value.length)
+const targetCount = computed(() => targetPoints.value.length)
+
+// ==================== 交互状态 ====================
 const hoverPipeline = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
+const hoverX = ref(0)
+const hoverY = ref(0)
 const currentPipeline = ref(null)
 const detailDrawerVisible = ref(false)
 
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
-}
-
-function canvasToWorld(x, y) {
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  const px = (x - cx - offsetX) / mapZoom + cx
-  const py = (y - cy - offsetY) / mapZoom + cy
-  const lng = bounds.minLng + (px / mapWidth) * (bounds.maxLng - bounds.minLng)
-  const lat = bounds.maxLat - (py / mapHeight) * (bounds.maxLat - bounds.minLat)
-  return { lng, lat }
-}
-
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0); ctx.lineTo(x, mapHeight); ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y); ctx.lineTo(mapWidth, y); ctx.stroke()
-  }
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('世纪大道', 350, 120)
-  ctx.fillText('张杨路', 450, 280)
-  ctx.fillText('东方路', 580, 200)
-  ctx.fillText('龙阳路', 200, 370)
-}
-
-function drawPipelines() {
-  filteredPipelines.value.forEach(pipeline => {
-    const layerKey = pipeline.riskLevel
-    if (!visibleLayers.value.includes(layerKey)) return
-
-    let color = '#67c23a'
-    let lineWidth = 4
-    if (pipeline.riskLevel === 'high') { color = '#f56c6c'; lineWidth = 5 }
-    else if (pipeline.riskLevel === 'medium') { color = '#e6a23c'; lineWidth = 4 }
-    else { color = '#67c23a'; lineWidth = 3 }
-
-    const points = pipeline.coordinates.map(coord => worldToCanvas(coord[0], coord[1]))
-    if (points.length < 2) return
-
-    ctx.beginPath()
-    ctx.moveTo(points[0].x, points[0].y)
-    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y)
-    ctx.strokeStyle = color
-    ctx.lineWidth = lineWidth
-    ctx.stroke()
-
-    // 绘制管线标签
-    const midIdx = Math.floor(points.length / 2)
-    if (points[midIdx]) {
-      ctx.fillStyle = '#333'
-      ctx.font = '11px "Microsoft YaHei"'
-      ctx.shadowBlur = 0
-      ctx.fillText(pipeline.name.slice(0, 6), points[midIdx].x - 20, points[midIdx].y - 8)
-      ctx.fillStyle = color
-      ctx.font = 'bold 10px sans-serif'
-      ctx.fillText(pipeline.riskScore + '分', points[midIdx].x - 15, points[midIdx].y + 12)
-    }
+// ==================== 地图覆盖物渲染 ====================
+function clearOverlays() {
+  Object.values(overlayStore).forEach(arr => {
+    arr.forEach(o => { try { mapInstance.removeOverlay(o) } catch {} })
+    arr.length = 0
   })
 }
 
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawPipelines()
-}
-
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-function refreshMap() { renderMap() }
-
-function updateCoordinates(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  const { lng, lat } = canvasToWorld(mouseX, mouseY)
-  currentLng.value = lng
-  currentLat.value = lat
-}
-
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 25, hit = null
-
-  filteredPipelines.value.forEach(pipeline => {
-    let minPointDist = Infinity
-    pipeline.coordinates.forEach(coord => {
-      const { x, y } = worldToCanvas(coord[0], coord[1])
-      const dist = Math.hypot(mouseX - x, mouseY - y)
-      if (dist < minPointDist) minPointDist = dist
-    })
-    if (minPointDist < minDist) {
-      minDist = minPointDist
-      hit = pipeline
-    }
-  })
-  if (hit) { currentPipeline.value = hit; detailDrawerVisible.value = true }
+function addToStore(layer, overlay) {
+  if (overlayStore[layer]) overlayStore[layer].push(overlay)
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 25, hit = null
+function renderPipelines() {
+  const BMapGL = window.BMapGL
+  filteredPipelines.value.forEach(p => {
+    const layerKey = p.riskLevel
+    const points = p.path.map(c => new BMapGL.Point(c[0], c[1]))
+    const color = p.riskLevel === 'high' ? '#f56c6c' : p.riskLevel === 'medium' ? '#e6a23c' : '#67c23a'
+    const weight = p.riskLevel === 'high' ? 6 : p.riskLevel === 'medium' ? 5 : 4
+    const polyline = new BMapGL.Polyline(points, {
+      strokeColor: color, strokeWeight: weight, strokeOpacity: 0.8
+    })
+    mapInstance.addOverlay(polyline)
+    addToStore(layerKey, polyline)
+
+    // 点击事件
+    polyline.addEventListener('click', () => {
+      currentPipeline.value = p
+      detailDrawerVisible.value = true
+    })
+
+    // hover 事件
+    polyline.addEventListener('mouseover', () => {
+      hoverPipeline.value = p
+    })
+    polyline.addEventListener('mouseout', () => {
+      hoverPipeline.value = null
+    })
+
+    // 管线名称标签
+    const mid = Math.floor(points.length / 2)
+    const scoreLabel = new BMapGL.Label(p.riskScore + '分', {
+      position: points[mid],
+      offset: new BMapGL.Size(0, -16)
+    })
+    scoreLabel.setStyle({
+      color: '#fff', fontSize: '11px', fontWeight: 'bold', padding: '2px 8px',
+      backgroundColor: color, borderRadius: '10px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(scoreLabel)
+    addToStore(layerKey, scoreLabel)
 
-  filteredPipelines.value.forEach(pipeline => {
-    let minPointDist = Infinity
-    pipeline.coordinates.forEach(coord => {
-      const { x, y } = worldToCanvas(coord[0], coord[1])
-      const dist = Math.hypot(mouseX - x, mouseY - y)
-      if (dist < minPointDist) minPointDist = dist
+    const nameLabel = new BMapGL.Label(p.name, {
+      position: points[mid],
+      offset: new BMapGL.Size(0, 4)
     })
-    if (minPointDist < minDist) {
-      minDist = minPointDist
-      hit = pipeline
-    }
+    nameLabel.setStyle({
+      color: '#333', fontSize: '10px', padding: '1px 4px',
+      backgroundColor: 'rgba(255,255,255,0.85)', borderRadius: '2px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(nameLabel)
+    addToStore(layerKey, nameLabel)
   })
-  if (hit) {
-    hoverPipeline.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else {
-    hoverPipeline.value = null
-  }
 }
 
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
+function renderHazardPoints() {
+  const BMapGL = window.BMapGL
+  hazardPoints.value.forEach(h => {
+    const pt = new BMapGL.Point(h.lng, h.lat)
+    const label = new BMapGL.Label(h.name, {
+      position: pt,
+      offset: new BMapGL.Size(0, -18)
+    })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: '#9b59b6', borderRadius: '3px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToStore('hazard', label)
+
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToStore('hazard', marker)
+
+    marker.addEventListener('click', () => {
+      currentPipeline.value = {
+        name: h.name, code: h.id, riskLevelName: h.type,
+        riskLevel: h.level, riskScore: '—', material: '—',
+        pressureLevel: '—', diameter: '—', length: '—',
+        buryYear: '—', owner: '—', lastInspection: '—',
+        riskCauses: [h.desc, `影响半径: ${h.riskRadius}`],
+        trendData: null
+      }
+      detailDrawerVisible.value = true
+    })
   })
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
-    renderMap()
+}
+
+function renderTargetPoints() {
+  const BMapGL = window.BMapGL
+  targetPoints.value.forEach(t => {
+    const pt = new BMapGL.Point(t.lng, t.lat)
+    const label = new BMapGL.Label(t.name, {
+      position: pt,
+      offset: new BMapGL.Size(0, -18)
+    })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: '#409eff', borderRadius: '3px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToStore('target', label)
+
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToStore('target', marker)
+
+    marker.addEventListener('click', () => {
+      currentPipeline.value = {
+        name: t.name, code: t.id, riskLevelName: t.type,
+        riskLevel: t.level, riskScore: '—', material: '—',
+        pressureLevel: '—', diameter: '—', length: '—',
+        buryYear: '—', owner: '—', lastInspection: '—',
+        riskCauses: [t.desc],
+        trendData: null
+      }
+      detailDrawerVisible.value = true
+    })
   })
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
+}
+
+function renderAllOverlays() {
+  clearOverlays()
+  renderPipelines()
+  renderHazardPoints()
+  renderTargetPoints()
+  onLayerChange()
+}
+
+// ==================== 图层显隐 ====================
+function onLayerChange() {
+  const set = new Set(visibleLayers.value)
+  Object.entries(overlayStore).forEach(([layer, overlays]) => {
+    overlays.forEach(o => {
+      try {
+        set.has(layer) ? o.show() : o.hide()
+      } catch {}
+    })
   })
 }
 
+function onScoreFilter() {
+  renderAllOverlays()
+}
+
+// ==================== 工具栏操作 ====================
+function zoomIn() {
+  if (mapInstance) mapInstance.zoomIn()
+}
+
+function zoomOut() {
+  if (mapInstance) mapInstance.zoomOut()
+}
+
+function fitBounds() {
+  if (mapInstance) {
+    const BMapGL = window.BMapGL
+    mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  }
+}
+
+function refreshMap() {
+  ElMessage.success('地图已刷新')
+}
+
 function locateArea() {
   const areas = {
-    area1: { lng: 121.505, lat: 31.24, zoom: 1.5 },
-    area2: { lng: 121.51, lat: 31.25, zoom: 1.5 },
-    area3: { lng: 121.50, lat: 31.22, zoom: 1.5 },
-    area4: { lng: 121.48, lat: 31.24, zoom: 1.8 }
+    area1: { lng: 110.3938, lat: 28.4700, zoom: 15 },
+    area2: { lng: 110.3970, lat: 28.4730, zoom: 15 },
+    area3: { lng: 110.3935, lat: 28.4660, zoom: 15 },
+    area4: { lng: 110.3940, lat: 28.469, zoom: 13 }
   }
   const area = areas[quickLocation.value]
-  if (area) {
-    mapZoom = area.zoom
-    const center = worldToCanvas(area.lng, area.lat)
-    offsetX = mapWidth/2 - center.x
-    offsetY = mapHeight/2 - center.y
-    renderMap()
+  if (area && mapInstance) {
+    const BMapGL = window.BMapGL
+    mapInstance.centerAndZoom(new BMapGL.Point(area.lng, area.lat), area.zoom)
+    ElMessage.success('已定位到: ' + quickLocation.value)
   }
 }
 
 function locateSearch() {
   if (!searchKeyword.value) return
-  const found = pipelines.value.find(p => p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value))
-  if (found) {
-    const centerCoord = found.coordinates[Math.floor(found.coordinates.length / 2)]
-    mapZoom = 2
-    const center = worldToCanvas(centerCoord[0], centerCoord[1])
-    offsetX = mapWidth/2 - center.x
-    offsetY = mapHeight/2 - center.y
-    renderMap()
+  const found = pipelines.value.find(p =>
+    p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value)
+  )
+  if (found && mapInstance) {
+    const BMapGL = window.BMapGL
+    const mid = found.path[Math.floor(found.path.length / 2)]
+    mapInstance.centerAndZoom(new BMapGL.Point(mid[0], mid[1]), 16)
     currentPipeline.value = found
     detailDrawerVisible.value = true
     ElMessage.success(`已定位到: ${found.name}`)
@@ -445,57 +547,173 @@ function locateSearch() {
   }
 }
 
-const locatePipeline = () => {
-  if (currentPipeline.value) {
-    const centerCoord = currentPipeline.value.coordinates[Math.floor(currentPipeline.value.coordinates.length / 2)]
-    mapZoom = 2
-    const center = worldToCanvas(centerCoord[0], centerCoord[1])
-    offsetX = mapWidth/2 - center.x
-    offsetY = mapHeight/2 - center.y
-    renderMap()
-    detailDrawerVisible.value = false
+function locatePipeline() {
+  if (!mapInstance || !currentPipeline.value) return
+  const BMapGL = window.BMapGL
+  const item = currentPipeline.value
+  let lng, lat
+  if (item.path) {
+    const mid = item.path[Math.floor(item.path.length / 2)]
+    lng = mid[0]; lat = mid[1]
+  } else if (item.lng) {
+    lng = item.lng; lat = item.lat
+  } else {
+    return
   }
+  mapInstance.centerAndZoom(new BMapGL.Point(lng, lat), 16)
+  detailDrawerVisible.value = false
+}
+
+function viewDetailReport() {
+  ElMessage.info('查看详细风险评估报告(演示)')
+}
+
+// ==================== 趋势图 ====================
+const chartRef = ref(null)
+let chartInstance = null
+
+function renderChart() {
+  if (!chartRef.value || !currentPipeline.value?.trendData) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
+  const data = currentPipeline.value.trendData
+  chartInstance.setOption({
+    tooltip: { trigger: 'axis' },
+    grid: { left: 40, right: 20, top: 20, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      data: ['第1次', '第2次', '第3次', '第4次', '第5次', '第6次'],
+      axisLabel: { fontSize: 10 }
+    },
+    yAxis: {
+      type: 'value',
+      name: '分',
+      min: 0,
+      max: 100,
+      axisLabel: { fontSize: 10 }
+    },
+    series: [{
+      data: data,
+      type: 'line',
+      smooth: true,
+      lineStyle: { color: '#f56c6c', width: 2 },
+      itemStyle: { color: '#f56c6c' },
+      areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+        { offset: 0, color: 'rgba(245,108,108,0.3)' },
+        { offset: 1, color: 'rgba(245,108,108,0.05)' }
+      ])}
+    }]
+  })
+}
+
+function onDrawerOpened() {
+  nextTick(() => renderChart())
 }
 
-const viewDetailReport = () => { ElMessage.info('查看详细风险评估报告(演示)') }
+// ==================== 生命周期 ====================
+watch(detailDrawerVisible, (val) => {
+  if (!val) {
+    currentPipeline.value = null
+    chartInstance = null
+  }
+})
 
-watch([() => visibleLayers.value, () => riskScoreRange.value], () => renderMap(), { deep: true })
+onMounted(async () => {
+  window.addEventListener('mousemove', onWindowMouseMove)
+  try {
+    await waitForBMapGL()
+    nextTick(() => initMap())
+  } catch {
+    mapError.value = '百度地图加载失败,请刷新页面重试'
+  }
+})
 
-onMounted(() => {
-  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
+onBeforeUnmount(() => {
+  window.removeEventListener('mousemove', onWindowMouseMove)
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
+  if (mapInstance) {
+    try { mapInstance.destroy() } catch {}
+    mapInstance = null
+  }
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
+
+// ==================== 全局鼠标移动(用于悬浮卡定位) ====================
+function onWindowMouseMove(e) {
+  if (hoverPipeline.value) {
+    hoverX.value = e.clientX + 12
+    hoverY.value = e.clientY - 40
+  }
+}
 </script>
 
 <style scoped>
 .pipeline-risk-gis {
   background: #f0f2f6;
-  height: 100vh;
+  height: 100%;
   display: flex;
   flex-direction: column;
   overflow: hidden;
 }
+
 .page-header {
-  background: white;
+  background: #fff;
   padding: 12px 24px;
   display: flex;
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+}
+
+.header-left .title {
+  font-size: 1.25rem;
+  font-weight: 600;
+  color: #303133;
+}
+
+.stats-badge {
+  display: flex;
+  gap: 20px;
+}
+
+.stat-item {
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
 }
-.header-left { display: flex; align-items: center; gap: 20px; }
-.header-left .title { font-size: 1.3rem; font-weight: 600; }
-.stats-badge { display: flex; gap: 20px; }
-.stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
+
 .stat-item.high { color: #f56c6c; }
 .stat-item.medium { color: #e6a23c; }
 .stat-item.low { color: #67c23a; }
-.stat-item .dot { width: 8px; height: 8px; border-radius: 50%; }
+
+.stat-item .dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+
 .stat-item.high .dot { background: #f56c6c; }
 .stat-item.medium .dot { background: #e6a23c; }
 .stat-item.low .dot { background: #67c23a; }
-.header-right { display: flex; gap: 12px; }
 
+.header-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+/* 主布局 */
 .main-layout {
   display: flex;
   flex: 1;
@@ -503,71 +721,197 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   padding: 12px;
   gap: 12px;
 }
+
+/* 左侧控制面板 */
 .control-panel {
   width: 260px;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   padding: 16px;
   display: flex;
   flex-direction: column;
-  gap: 20px;
+  gap: 16px;
   overflow-y: auto;
+  flex-shrink: 0;
+}
+
+.panel-section {
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 16px;
 }
-.panel-section { border-bottom: 1px solid #e9ecef; padding-bottom: 16px; }
-.panel-section:last-child { border-bottom: none; }
-.section-title { font-weight: 600; margin-bottom: 12px; font-size: 14px; }
-.layer-item { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
-.layer-badge { font-size: 16px; }
+
+.panel-section:last-child {
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.section-title {
+  font-weight: 600;
+  margin-bottom: 10px;
+  font-size: 14px;
+  color: #303133;
+}
+
+.layer-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 6px;
+}
+
+.layer-badge {
+  font-size: 16px;
+}
+
 .layer-badge.high { color: #f56c6c; }
 .layer-badge.medium { color: #e6a23c; }
 .layer-badge.low { color: #67c23a; }
-.range-labels { display: flex; justify-content: space-between; font-size: 10px; color: #909399; margin-top: 4px; }
-.stats-list { display: flex; flex-direction: column; gap: 8px; }
-.stat-row { display: flex; justify-content: space-between; font-size: 13px; }
+.layer-badge.hazard { color: #9b59b6; }
+.layer-badge.target { color: #409eff; }
+
+.stats-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.stat-row {
+  display: flex;
+  justify-content: space-between;
+  font-size: 13px;
+}
+
 .high-text { color: #f56c6c; }
 .medium-text { color: #e6a23c; }
 .low-text { color: #67c23a; }
-.legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; }
-.legend-line { width: 30px; height: 3px; display: inline-block; }
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 6px;
+  font-size: 12px;
+}
+
+.legend-line {
+  width: 30px;
+  height: 3px;
+  display: inline-block;
+  border-radius: 1px;
+}
+
 .legend-line.high-line { background: #f56c6c; }
 .legend-line.medium-line { background: #e6a23c; }
 .legend-line.low-line { background: #67c23a; }
-.legend-point { width: 10px; height: 10px; display: inline-block; border-radius: 50%; }
+
+.legend-point {
+  width: 10px;
+  height: 10px;
+  display: inline-block;
+  border-radius: 50%;
+}
+
 .legend-point.hazard-point { background: #9b59b6; }
 .legend-point.target-point { background: #409eff; }
 
+/* 右侧GIS地图 */
 .gis-container {
   flex: 1;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
   overflow: hidden;
 }
+
 .map-toolbar {
   padding: 10px 16px;
-  border-bottom: 1px solid #e9ecef;
+  border-bottom: 1px solid #ebeef5;
   display: flex;
   justify-content: space-between;
   align-items: center;
+  flex-shrink: 0;
+}
+
+.coordinate-info {
+  font-size: 12px;
+  color: #909399;
+}
+
+.map-wrapper {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+}
+
+.map-div {
+  width: 100%;
+  height: 100%;
 }
-.coordinate-info { font-size: 12px; color: #909399; }
-.map-wrapper { flex: 1; position: relative; cursor: crosshair; }
-canvas { width: 100%; height: 100%; display: block; }
+
+.map-error {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #f56c6c;
+  font-size: 14px;
+}
+
+/* 悬浮信息卡 */
 .hover-card {
   position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
+  background: rgba(0, 0, 0, 0.82);
+  color: #fff;
+  padding: 10px 14px;
   border-radius: 10px;
   font-size: 12px;
   pointer-events: none;
-  z-index: 200;
-}
-.hover-title { font-weight: 600; margin-bottom: 4px; }
-.pipeline-detail { padding: 8px; }
-.sub-title { font-weight: 600; margin: 16px 0 8px 0; }
-.risk-causes ul { margin: 0; padding-left: 20px; }
-.risk-causes li { margin: 6px 0; font-size: 13px; }
-.action-buttons { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
-</style>
+  z-index: 2000;
+  line-height: 1.6;
+}
+
+.hover-title {
+  font-weight: 600;
+  margin-bottom: 4px;
+  font-size: 13px;
+}
+
+/* 详情抽屉 */
+.pipeline-detail {
+  padding: 4px;
+}
+
+.sub-title {
+  font-weight: 600;
+  margin: 16px 0 8px 0;
+  font-size: 14px;
+}
+
+.risk-causes ul {
+  margin: 0;
+  padding-left: 20px;
+}
+
+.risk-causes li {
+  margin: 6px 0;
+  font-size: 13px;
+  color: #606266;
+}
+
+.chart-section {
+  margin-top: 8px;
+}
+
+.trend-chart {
+  width: 100%;
+  height: 200px;
+}
+
+.action-buttons {
+  margin-top: 16px;
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+</style>

+ 93 - 41
src/views/subSystem/gas/gasfxpg/gwfxpgqd.vue

@@ -232,68 +232,120 @@ import { ref, computed } from 'vue'
 import { DataAnalysis, Search, Refresh, Download, Location, Document, Upload } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 
-// 模拟管网风险评估数据
+// 沅陵县管网风险评估数据(8段)
 const pipelines = ref([
   {
-    id: 1, code: 'PL-001', name: '世纪大道中压管线', area: '浦东新区', material: '钢管', diameter: 300, length: 12.5,
-    buildYear: '2018', pressure: 0.4, owner: '浦东燃气公司', lastInspection: '2025-02-15',
+    id: 1, code: 'PL-YL-001', name: '辰州路高压管线', area: '沅陵县太常片区', material: '钢管', diameter: 500, length: 3.8,
+    buildYear: '2015', pressure: 1.6, owner: '沅陵燃气有限公司', lastInspection: '2026-03-15',
     riskLevel: 'high', riskScore: 86.5,
-    dimensions: { leakProbability: 85, consequenceSeverity: 92, corrosionRisk: 78, thirdPartyRisk: 88, agingLevel: 65 },
-    riskFactors: ['管道建设年限较长', '周边施工活动频繁', '防腐层检测有局部破损', '位于交通密集区域'],
-    mitigationMeasures: ['立即安排专业检测', '加密巡检频次至每周2次', '设置施工警示标识', '制定应急预案'],
+    dimensions: { leakProbability: 85, consequenceSeverity: 92, corrosionRisk: 78, thirdPartyRisk: 88, agingLevel: 82 },
+    riskFactors: ['管道服役超过10年,老化程度较高', '辰州路主干道交通密集,第三方破坏风险大', '防腐层检测发现多处局部破损', '沿线有市政雨污分流施工活动'],
+    mitigationMeasures: ['立即安排管道内检测及完整性评价', '加密巡检频次至每周2次', '在施工区域设置警示标识并派专人监护', '制定辰州路段专项应急预案'],
     historyRecords: [
-      { id: 1, time: '2025-01-15', riskLevel: 'high', score: 84.2, type: 'danger', description: '年度风险评估,高风险' },
-      { id: 2, time: '2024-07-20', riskLevel: 'medium', score: 68.5, type: 'warning', description: '中期评估,风险上升' },
-      { id: 3, time: '2024-01-10', riskLevel: 'medium', score: 62.3, type: 'primary', description: '首次全面评估' }
+      { id: 1, time: '2026-03-15', riskLevel: 'high', score: 86.5, type: 'danger', description: '2026年度春季风险评估,高风险,腐蚀加剧' },
+      { id: 2, time: '2025-09-20', riskLevel: 'high', score: 84.2, type: 'danger', description: '秋季评估,因施工活动风险上调' },
+      { id: 3, time: '2025-03-10', riskLevel: 'medium', score: 68.5, type: 'warning', description: '春季评估,首次发现防腐层破损' },
+      { id: 4, time: '2024-09-05', riskLevel: 'medium', score: 62.3, type: 'primary', description: '首次全面风险评估' }
     ],
-    attachments: [{ name: '检测报告_20250215.pdf' }, { name: '管线图纸.dwg' }, { name: '维护记录.xlsx' }]
+    attachments: [{ name: '辰州路高压管线检测报告_20260315.pdf' }, { name: '管线竣工图纸.dwg' }, { name: '防腐层检测记录.xlsx' }]
   },
   {
-    id: 2, code: 'PL-002', name: '张杨路高压管线', area: '浦东新区', material: '钢管', diameter: 500, length: 18.2,
-    buildYear: '2020', pressure: 1.6, owner: '浦西燃气公司', lastInspection: '2025-03-10',
+    id: 2, code: 'PL-YL-002', name: '建设东路高压管', area: '沅陵县太常片区', material: '钢管', diameter: 400, length: 3.6,
+    buildYear: '2016', pressure: 1.6, owner: '沅陵燃气有限公司', lastInspection: '2025-11-08',
+    riskLevel: 'high', riskScore: 79.2,
+    dimensions: { leakProbability: 82, consequenceSeverity: 76, corrosionRisk: 85, thirdPartyRisk: 72, agingLevel: 88 },
+    riskFactors: ['高压运行风险等级高', '管道穿越工业园区域,周边有化工企业', '检测发现多处腐蚀点,局部壁厚减薄', '建设路沿线重型车辆通行频繁'],
+    mitigationMeasures: ['紧急申请维修资金进行局部换管', '加强对化工企业周边管段的气体监测', '在工业园段增设阴极保护', '每季度进行一次壁厚检测'],
+    historyRecords: [
+      { id: 1, time: '2025-11-08', riskLevel: 'high', score: 79.2, type: 'danger', description: '检测发现严重腐蚀,壁厚减薄超限' },
+      { id: 2, time: '2025-06-15', riskLevel: 'high', score: 76.8, type: 'danger', description: '中期评估,腐蚀风险升高' },
+      { id: 3, time: '2025-01-20', riskLevel: 'medium', score: 61.5, type: 'warning', description: '年度风险评估,发现局部异常' }
+    ],
+    attachments: [{ name: '建设东路高压管腐蚀检测报告.pdf' }, { name: '壁厚检测数据.xlsx' }, { name: '维修方案_v2.docx' }]
+  },
+  {
+    id: 3, code: 'PL-YL-003', name: '古城路中压管线', area: '沅陵县太常片区', material: '钢管', diameter: 300, length: 3.2,
+    buildYear: '2018', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-02-10',
     riskLevel: 'medium', riskScore: 64.3,
     dimensions: { leakProbability: 58, consequenceSeverity: 72, corrosionRisk: 55, thirdPartyRisk: 68, agingLevel: 42 },
-    riskFactors: ['高压运行风险', '沿线有在建工程', '部分区域防腐层老化'],
-    mitigationMeasures: ['加强压力监控', '对施工区域重点巡查', '计划2025年Q3进行内检测'],
+    riskFactors: ['沿线有在建房地产开发工程', '部分区域防腐层老化脱落', '古城路商业区人流量大,后果严重性较高', '管线穿越古城地下管沟区域,交叉风险'],
+    mitigationMeasures: ['对施工区域加密巡查至每周3次', '计划2026年Q3进行内检测', '与开发商签订管线保护协议', '更新防腐层老化段'],
     historyRecords: [
-      { id: 1, time: '2025-03-10', riskLevel: 'medium', score: 64.3, type: 'warning', description: '季度评估,中风险' },
-      { id: 2, time: '2024-12-05', riskLevel: 'medium', score: 61.8, type: 'primary', description: '年度评估' }
+      { id: 1, time: '2026-02-10', riskLevel: 'medium', score: 64.3, type: 'warning', description: '季度评估,施工活动导致风险上升' },
+      { id: 2, time: '2025-11-15', riskLevel: 'medium', score: 61.8, type: 'primary', description: '秋季风险评估,防腐层检测' },
+      { id: 3, time: '2025-08-20', riskLevel: 'medium', score: 58.5, type: 'primary', description: '夏季常规评估' },
+      { id: 4, time: '2025-05-10', riskLevel: 'low', score: 38.2, type: 'success', description: '春季评估,风险较低' }
     ],
-    attachments: [{ name: '检测报告_20250310.pdf' }, { name: '运行记录.pdf' }]
+    attachments: [{ name: '古城路中压管线检测报告_20260210.pdf' }, { name: '管沟交叉段详图.dwg' }, { name: '巡检记录_2026Q1.xlsx' }]
   },
   {
-    id: 3, code: 'PL-003', name: '浦东南路低压管线', area: '浦东新区', material: 'PE管', diameter: 200, length: 8.6,
-    buildYear: '2022', pressure: 0.1, owner: '浦东燃气公司', lastInspection: '2025-01-20',
-    riskLevel: 'low', riskScore: 28.6,
-    dimensions: { leakProbability: 25, consequenceSeverity: 35, corrosionRisk: 18, thirdPartyRisk: 32, agingLevel: 15 },
-    riskFactors: ['新建管道状况良好', '周边环境稳定', '压力等级较低'],
-    mitigationMeasures: ['常规巡检(每月1次)', '建立运行档案'],
+    id: 4, code: 'PL-YL-004', name: '迎宾路中压管线', area: '沅陵县太常片区', material: '钢管', diameter: 300, length: 3.6,
+    buildYear: '2019', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-04-28',
+    riskLevel: 'medium', riskScore: 55.8,
+    dimensions: { leakProbability: 48, consequenceSeverity: 62, corrosionRisk: 52, thirdPartyRisk: 58, agingLevel: 38 },
+    riskFactors: ['管道运行正常但有轻微腐蚀迹象', '迎宾路商业区人流较大', '沿线有电力管沟交叉,存在杂散电流干扰'],
+    mitigationMeasures: ['加强日常巡检,每月至少2次', '排查电力管沟交叉段杂散电流影响', '计划2026年Q4进行防腐层检测'],
     historyRecords: [
-      { id: 1, time: '2025-01-20', riskLevel: 'low', score: 28.6, type: 'success', description: '风险评估,低风险' }
+      { id: 1, time: '2026-04-28', riskLevel: 'medium', score: 55.8, type: 'warning', description: '春季评估,轻微腐蚀需关注' },
+      { id: 2, time: '2025-10-15', riskLevel: 'medium', score: 52.3, type: 'primary', description: '秋季常规评估' },
+      { id: 3, time: '2025-04-20', riskLevel: 'medium', score: 50.1, type: 'primary', description: '年度风险评估' }
     ],
-    attachments: [{ name: '竣工图纸.pdf' }, { name: '检测报告_20250120.pdf' }]
+    attachments: [{ name: '迎宾路中压管线运行记录.pdf' }, { name: '杂散电流检测报告.docx' }]
   },
   {
-    id: 4, code: 'PL-004', name: '龙阳路中压管线', area: '浦东新区', material: '钢管', diameter: 250, length: 10.3,
-    buildYear: '2016', pressure: 0.4, owner: '浦西燃气公司', lastInspection: '2024-11-08',
-    riskLevel: 'high', riskScore: 79.2,
-    dimensions: { leakProbability: 82, consequenceSeverity: 76, corrosionRisk: 85, thirdPartyRisk: 72, agingLevel: 88 },
-    riskFactors: ['管道服役超过8年', '检测发现多处腐蚀点', '沿线有学校等敏感目标'],
-    mitigationMeasures: ['紧急申请维修资金', '局部更换计划', '加密检测频次'],
+    id: 5, code: 'PL-YL-005', name: '天宁路中压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 250, length: 3.8,
+    buildYear: '2020', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-05-12',
+    riskLevel: 'medium', riskScore: 48.2,
+    dimensions: { leakProbability: 42, consequenceSeverity: 55, corrosionRisk: 28, thirdPartyRisk: 65, agingLevel: 25 },
+    riskFactors: ['PE管接头处需定期检测维护', '天宁西路有地下车库临近施工', '管道穿越太常安置区,人口密集', '地质沉降风险需关注'],
+    mitigationMeasures: ['每年对PE管接头进行超声波检测', '临近施工期间加密巡查', '建立安置区居民燃气安全宣传机制'],
     historyRecords: [
-      { id: 1, time: '2024-11-08', riskLevel: 'high', score: 79.2, type: 'danger', description: '检测发现严重腐蚀' }
+      { id: 1, time: '2026-05-12', riskLevel: 'medium', score: 48.2, type: 'warning', description: '夏季评估,地下车库施工影响' },
+      { id: 2, time: '2025-11-20', riskLevel: 'low', score: 35.6, type: 'success', description: '秋季评估,风险可控' },
+      { id: 3, time: '2025-05-10', riskLevel: 'low', score: 32.8, type: 'success', description: '春季常规评估' }
     ],
-    attachments: [{ name: '腐蚀检测报告.pdf' }, { name: '维修方案.docx' }]
+    attachments: [{ name: '天宁路中压PE管检测报告.pdf' }, { name: '接头检测记录_2026.xlsx' }]
   },
   {
-    id: 5, code: 'PL-005', name: '东方路中压管线', area: '浦东新区', material: '钢管', diameter: 300, length: 7.8,
-    buildYear: '2019', pressure: 0.4, owner: '浦东燃气公司', lastInspection: '2025-02-28',
-    riskLevel: 'medium', riskScore: 55.8,
-    dimensions: { leakProbability: 48, consequenceSeverity: 62, corrosionRisk: 52, thirdPartyRisk: 58, agingLevel: 38 },
-    riskFactors: ['位于商业区人流量大', '管道运行正常但有轻微腐蚀'],
-    mitigationMeasures: ['加强日常巡检', '计划2025年Q4检测'],
-    historyRecords: [{ id: 1, time: '2025-02-28', riskLevel: 'medium', score: 55.8, type: 'warning', description: '中期评估' }],
-    attachments: [{ name: '运行数据.xlsx' }]
+    id: 6, code: 'PL-YL-006', name: '辰州北路中压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 200, length: 1.2,
+    buildYear: '2021', pressure: 0.4, owner: '沅陵燃气有限公司', lastInspection: '2026-05-28',
+    riskLevel: 'medium', riskScore: 42.5,
+    dimensions: { leakProbability: 38, consequenceSeverity: 48, corrosionRisk: 20, thirdPartyRisk: 55, agingLevel: 18 },
+    riskFactors: ['辰州北路综合管廊相邻区域,交叉风险', '管段较短但周边地下管线复杂', '电力、通信管线共用管廊通道'],
+    mitigationMeasures: ['加强管廊交叉段气体监测', '与管廊管理单位建立联动机制', '定期检查PE管与管廊的隔离情况'],
+    historyRecords: [
+      { id: 1, time: '2026-05-28', riskLevel: 'medium', score: 42.5, type: 'warning', description: '管廊交叉段专项评估' },
+      { id: 2, time: '2025-11-10', riskLevel: 'low', score: 35.2, type: 'success', description: '秋季评估,低风险' },
+      { id: 3, time: '2025-05-05', riskLevel: 'low', score: 30.8, type: 'success', description: '春季常规评估' }
+    ],
+    attachments: [{ name: '辰州北路管廊交叉段评估报告.pdf' }, { name: '管廊通道图纸.dwg' }]
+  },
+  {
+    id: 7, code: 'PL-YL-007', name: '滨江路低压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 200, length: 3.8,
+    buildYear: '2022', pressure: 0.1, owner: '沅陵燃气有限公司', lastInspection: '2026-06-03',
+    riskLevel: 'low', riskScore: 28.6,
+    dimensions: { leakProbability: 25, consequenceSeverity: 35, corrosionRisk: 18, thirdPartyRisk: 32, agingLevel: 15 },
+    riskFactors: ['新建管道状况良好,整体风险低', '低压运行,泄漏后果可控', '滨江路河堤段需关注汛期水土流失影响'],
+    mitigationMeasures: ['常规巡检(每月1次)', '汛期前后加强河堤段管线上方土体检查', '建立运行档案,记录运行参数'],
+    historyRecords: [
+      { id: 1, time: '2026-06-03', riskLevel: 'low', score: 28.6, type: 'success', description: '夏季汛期前风险评估,低风险' },
+      { id: 2, time: '2025-12-10', riskLevel: 'low', score: 26.2, type: 'success', description: '冬季评估,运行正常' },
+      { id: 3, time: '2025-06-05', riskLevel: 'low', score: 24.5, type: 'success', description: '首次年度风险评估' }
+    ],
+    attachments: [{ name: '滨江路低压管线竣工图纸.pdf' }, { name: '运行记录_2026.xlsx' }]
+  },
+  {
+    id: 8, code: 'PL-YL-008', name: '太常南路低压管线', area: '沅陵县太常片区', material: 'PE管', diameter: 160, length: 1.5,
+    buildYear: '2023', pressure: 0.1, owner: '沅陵燃气有限公司', lastInspection: '2026-06-08',
+    riskLevel: 'low', riskScore: 18.3,
+    dimensions: { leakProbability: 18, consequenceSeverity: 22, corrosionRisk: 10, thirdPartyRisk: 25, agingLevel: 8 },
+    riskFactors: ['新建管道风险极低', '位于城南新开发区域,周边环境稳定', '管径小、压力低,风险可控'],
+    mitigationMeasures: ['常规巡检(每2月1次)', '关注区域开发建设动态', '定期更新电子档案'],
+    historyRecords: [
+      { id: 1, time: '2026-06-08', riskLevel: 'low', score: 18.3, type: 'success', description: '年度风险评估,新建管道运行良好' },
+      { id: 2, time: '2025-12-15', riskLevel: 'low', score: 16.8, type: 'success', description: '首次运行评估,各项指标正常' }
+    ],
+    attachments: [{ name: '太常南路竣工图纸.pdf' }, { name: '管道材质证明.pdf' }, { name: '验收报告.pdf' }]
   }
 ])
 

+ 198 - 80
src/views/subSystem/gas/gasfxpg/gwfxxq.vue

@@ -79,9 +79,7 @@
         <!-- 风险成因雷达图 -->
         <div class="radar-section">
           <div class="section-title">风险构成要素雷达图</div>
-          <div class="radar-chart">
-            <v-chart :option="radarChartOption" autoresize />
-          </div>
+          <div ref="radarRef" class="radar-chart"></div>
           <div class="risk-summary">
             综合风险评分: <strong :class="getScoreClass(selectedPipeline.riskScore)">{{ selectedPipeline.riskScore }}分</strong>
             <span class="risk-level">属于{{ selectedPipeline.riskLevel === 'high' ? '高风险' : selectedPipeline.riskLevel === 'medium' ? '中风险' : '低风险' }}管段</span>
@@ -183,111 +181,209 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { DataAnalysis, Search, Refresh, Download, Location, Monitor, Document, Warning, OfficeBuilding, CircleClose } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { RadarChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
+import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 
-use([CanvasRenderer, RadarChart, TitleComponent, TooltipComponent, LegendComponent])
-
-// 模拟管网数据
+// 沅陵县管网风险构成分析数据(8段)
 const pipelines = ref([
   {
-    id: 1, code: 'PL-001', name: '世纪大道中压管线', area: '浦东新区世纪大道沿线',
+    id: 1, code: 'PL-YL-001', name: '辰州路高压管线', area: '沅陵县太常片区辰州路',
     riskLevel: 'high', riskScore: 86.5,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 12.5,
-    buryYear: '2018年', designLife: '30年', owner: '浦东燃气公司',
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 3.8,
+    buryYear: '2015年', designLife: '30年', owner: '沅陵燃气有限公司',
+    maintenanceRecords: [
+      { id: 1, time: '2025-09-15', description: '防腐层检测发现3处破损,补口修复', type: '紧急' },
+      { id: 2, time: '2025-03-20', description: '辰州路雨污分流施工段增设管线保护盖板', type: '常规' },
+      { id: 3, time: '2024-08-10', description: '定期巡检,发现1处标识桩损坏,已更换', type: '常规' }
+    ],
+    hazards: [
+      { id: 1, name: '城南加油站', distance: 85, level: '高' },
+      { id: 2, name: '辰州路在建施工工地', distance: 50, level: '高' },
+      { id: 3, name: '太常LNG储配站', distance: 180, level: '中' }
+    ],
+    protectionTargets: [
+      { id: 1, name: '县人民医院太常分院', distance: 120, type: '医院' },
+      { id: 2, name: '太常中学', distance: 280, type: '学校' },
+      { id: 3, name: '辰州路商业街', distance: 60, type: '商业区' }
+    ],
+    riskCauses: [
+      '管道服役超过10年,防腐层出现老化剥落现象',
+      '辰州路主干道交通密集,重型车辆震动荷载大',
+      '雨污分流施工距管线仅50m,第三方破坏风险高',
+      '周边有加油站及LNG储配站等重大危险源',
+      '历史维修记录显示防腐层已多次修复'
+    ],
+    radarData: [82, 88, 78, 85, 92]
+  },
+  {
+    id: 2, code: 'PL-YL-002', name: '建设东路高压管', area: '沅陵县太常片区建设东路',
+    riskLevel: 'high', riskScore: 79.2,
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 400, length: 3.6,
+    buryYear: '2016年', designLife: '30年', owner: '沅陵燃气有限公司',
     maintenanceRecords: [
-      { id: 1, time: '2024-11-15', description: '发现防腐层破损,进行补口修复', type: '常规' },
-      { id: 2, time: '2025-02-20', description: '泄漏检测,更换密封垫片', type: '紧急' }
+      { id: 1, time: '2025-11-08', description: '壁厚检测发现局部减薄超限,紧急停气修复', type: '紧急' },
+      { id: 2, time: '2025-06-20', description: '工业园段增设阴极保护装置', type: '常规' },
+      { id: 3, time: '2025-01-10', description: '检测发现多处腐蚀点,标记重点监控', type: '常规' }
     ],
     hazards: [
-      { id: 1, name: '加油站', distance: 85, level: '高' },
-      { id: 2, name: '在建施工工地', distance: 120, level: '中' },
-      { id: 3, name: '危险品仓库', distance: 200, level: '中' }
+      { id: 1, name: '太常化工仓库', distance: 130, level: '高' },
+      { id: 2, name: '工业园计量站', distance: 90, level: '中' },
+      { id: 3, name: '建设路加油站', distance: 200, level: '中' }
     ],
     protectionTargets: [
-      { id: 1, name: 'XX小学', distance: 150, type: '学校' },
-      { id: 2, name: '世纪公园', distance: 300, type: '公园' },
-      { id: 3, name: '社区医院', distance: 280, type: '医院' }
+      { id: 1, name: '太常工业园', distance: 150, type: '工业区' },
+      { id: 2, name: '建设东路居民区', distance: 100, type: '住宅区' }
     ],
     riskCauses: [
-      '管道服役年限较长(已达7年),防腐层出现老化现象',
-      '周边存在加油站等重大危险源,泄漏后果严重',
-      '毗邻在建施工工地,第三方破坏风险较高',
-      '位于交通密集区域,震动荷载大',
-      '历史维修记录显示存在多次泄漏修复'
+      '高压运行,泄漏后果严重性高',
+      '管道穿越工业园区域,周边化工企业密集',
+      '检测发现多处腐蚀点,局部壁厚减薄',
+      '建设路重型车辆通行频繁,地面荷载大',
+      '防腐层老化加速,需加密监测'
     ],
-    radarData: [85, 78, 88, 82, 75]
+    radarData: [88, 72, 85, 76, 65]
   },
   {
-    id: 2, code: 'PL-002', name: '张杨路高压管线', area: '浦东新区张杨路沿线',
+    id: 3, code: 'PL-YL-003', name: '古城路中压管线', area: '沅陵县太常片区古城路',
     riskLevel: 'medium', riskScore: 64.3,
-    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 18.2,
-    buryYear: '2020年', designLife: '30年', owner: '浦西燃气公司',
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.2,
+    buryYear: '2018年', designLife: '30年', owner: '沅陵燃气有限公司',
     maintenanceRecords: [
-      { id: 1, time: '2024-08-10', description: '定期检测,防腐层状况良好', type: '常规' }
+      { id: 1, time: '2026-01-12', description: '古城路管沟交叉段防腐层修复', type: '常规' },
+      { id: 2, time: '2025-08-05', description: '房地产开发区域增设警示标识及隔离围栏', type: '常规' }
     ],
     hazards: [
-      { id: 1, name: '地铁施工区', distance: 95, level: '中' },
-      { id: 2, name: '大型商场', distance: 180, level: '低' }
+      { id: 1, name: '古城路在建房地产项目', distance: 65, level: '中' },
+      { id: 2, name: '古城中路电力管沟', distance: 30, level: '中' }
     ],
     protectionTargets: [
-      { id: 1, name: 'XX中学', distance: 200, type: '学校' },
-      { id: 2, name: '商业综合体', distance: 250, type: '商业区' }
+      { id: 1, name: '古城路商业步行街', distance: 80, type: '商业区' },
+      { id: 2, name: '太常安置小区', distance: 150, type: '住宅区' },
+      { id: 3, name: '沅陵古城游客中心', distance: 200, type: '公共场所' }
     ],
     riskCauses: [
-      '高压运行,泄漏后果严重',
-      '沿线有地铁施工,第三方破坏风险',
-      '周边有学校等敏感目标',
-      '历史维修记录较少,管道状况良好'
+      '沿线房地产开发施工,第三方破坏风险中等',
+      '与电力管沟交叉段存在杂散电流干扰',
+      '古城路商业区人流量大,后果严重性较高',
+      '部分区域防腐层出现老化脱落',
+      '管沟交叉区域检测难度大'
     ],
-    radarData: [65, 72, 58, 68, 55]
+    radarData: [42, 68, 55, 58, 72]
   },
   {
-    id: 3, code: 'PL-003', name: '浦东南路低压管线', area: '浦东新区浦东南路',
-    riskLevel: 'low', riskScore: 28.6,
-    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 8.6,
-    buryYear: '2022年', designLife: '50年', owner: '浦东燃气公司',
-    maintenanceRecords: [],
-    hazards: [],
+    id: 4, code: 'PL-YL-004', name: '迎宾路中压管线', area: '沅陵县太常片区迎宾路',
+    riskLevel: 'medium', riskScore: 55.8,
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.6,
+    buryYear: '2019年', designLife: '30年', owner: '沅陵燃气有限公司',
+    maintenanceRecords: [
+      { id: 1, time: '2025-11-10', description: '电力管沟交叉段杂散电流排流治理', type: '常规' },
+      { id: 2, time: '2025-05-18', description: '定期巡检,防腐层状况整体良好', type: '常规' }
+    ],
+    hazards: [
+      { id: 1, name: '迎宾东路地下通道', distance: 120, type: '中' },
+      { id: 2, name: '迎宾路电力管沟', distance: 25, level: '中' }
+    ],
     protectionTargets: [
-      { id: 1, name: '居民小区', distance: 120, type: '住宅区' }
+      { id: 1, name: '迎宾路商业广场', distance: 100, type: '商业区' },
+      { id: 2, name: '太常街道办事处', distance: 200, type: '办公区' }
     ],
     riskCauses: [
-      '新建管道,状况良好',
-      '低压运行,风险可控',
-      '周边环境稳定,无重大危险源',
-      'PE管材耐腐蚀性能好'
+      '电力管沟交叉段存在杂散电流干扰风险',
+      '迎宾路商业区人流较大,防护目标敏感',
+      '钢管轻微腐蚀,需定期检测维护',
+      '管线运行正常,整体风险可控'
     ],
-    radarData: [25, 32, 28, 35, 22]
+    radarData: [38, 58, 52, 48, 62]
   },
   {
-    id: 4, code: 'PL-004', name: '龙阳路中压管线', area: '浦东新区龙阳路',
-    riskLevel: 'high', riskScore: 79.2,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 10.3,
-    buryYear: '2016年', designLife: '30年', owner: '浦西燃气公司',
+    id: 5, code: 'PL-YL-005', name: '天宁路中压管线', area: '沅陵县太常片区天宁路',
+    riskLevel: 'medium', riskScore: 48.2,
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 3.8,
+    buryYear: '2020年', designLife: '50年', owner: '沅陵燃气有限公司',
+    maintenanceRecords: [
+      { id: 1, time: '2025-12-05', description: '天宁西路地下车库施工段PE管接头超声波检测', type: '常规' }
+    ],
+    hazards: [
+      { id: 1, name: '天宁西路地下车库施工区', distance: 40, level: '中' }
+    ],
+    protectionTargets: [
+      { id: 1, name: '太常安置小区', distance: 60, type: '住宅区' },
+      { id: 2, name: '天宁路小学', distance: 180, type: '学校' },
+      { id: 3, name: '县政府办公区', distance: 250, type: '办公区' }
+    ],
+    riskCauses: [
+      '地下车库施工距管线仅40m,需重点监控',
+      'PE管接头处需定期检测确保密封',
+      '管道穿越安置区,人口密集防护要求高',
+      'PE管材耐腐蚀但抗外力破坏能力较弱'
+    ],
+    radarData: [25, 55, 28, 42, 48]
+  },
+  {
+    id: 6, code: 'PL-YL-006', name: '辰州北路中压管线', area: '沅陵县太常片区辰州北路',
+    riskLevel: 'medium', riskScore: 42.5,
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 200, length: 1.2,
+    buryYear: '2021年', designLife: '50年', owner: '沅陵燃气有限公司',
     maintenanceRecords: [
-      { id: 1, time: '2024-10-08', description: '检测发现多处腐蚀点', type: '紧急' },
-      { id: 2, time: '2025-01-15', description: '局部防腐层修复', type: '常规' }
+      { id: 1, time: '2025-10-15', description: '管廊交叉段巡检,隔离设施完好', type: '常规' }
     ],
     hazards: [
-      { id: 1, name: '化工厂', distance: 150, level: '高' },
-      { id: 2, name: '加油站', distance: 220, level: '中' }
+      { id: 1, name: '辰州北路综合管廊', distance: 15, level: '高' }
     ],
     protectionTargets: [
-      { id: 1, name: '幼儿园', distance: 180, type: '学校' }
+      { id: 1, name: '辰州北路商业区', distance: 80, type: '商业区' },
+      { id: 2, name: '古城北路居民区', distance: 120, type: '住宅区' }
     ],
     riskCauses: [
-      '管道服役超过8年,检测发现多处腐蚀',
-      '周边有化工厂等重大危险源',
-      '腐蚀风险较高,需加强监测',
-      '历史维修记录频次较高'
+      '与综合管廊共通道,电力通信管线交叉密集',
+      '管廊内可燃气体可能积聚,交叉风险高',
+      '管段较短但周边地下环境复杂',
+      'PE管与管廊结构隔离需持续维护'
     ],
-    radarData: [82, 85, 78, 76, 80]
+    radarData: [18, 55, 20, 65, 48]
+  },
+  {
+    id: 7, code: 'PL-YL-007', name: '滨江路低压管线', area: '沅陵县太常片区滨江路',
+    riskLevel: 'low', riskScore: 28.6,
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 3.8,
+    buryYear: '2022年', designLife: '50年', owner: '沅陵燃气有限公司',
+    maintenanceRecords: [
+      { id: 1, time: '2025-08-20', description: '汛期后河堤段土体稳定性检查,正常', type: '常规' }
+    ],
+    hazards: [
+      { id: 1, name: '滨江东路河堤管廊', distance: 100, level: '低' }
+    ],
+    protectionTargets: [
+      { id: 1, name: '滨江公园', distance: 80, type: '公园' },
+      { id: 2, name: '滨江路居民区', distance: 150, type: '住宅区' }
+    ],
+    riskCauses: [
+      '新建管道,整体状况良好',
+      '低压运行,泄漏后果可控',
+      '河堤段需关注汛期水土流失影响',
+      'PE管耐腐蚀,维护需求低'
+    ],
+    radarData: [15, 25, 18, 22, 35]
+  },
+  {
+    id: 8, code: 'PL-YL-008', name: '太常南路低压管线', area: '沅陵县太常片区太常南路',
+    riskLevel: 'low', riskScore: 18.3,
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 160, length: 1.5,
+    buryYear: '2023年', designLife: '50年', owner: '沅陵燃气有限公司',
+    maintenanceRecords: [],
+    hazards: [],
+    protectionTargets: [
+      { id: 1, name: '城南新开发小区', distance: 100, type: '住宅区' }
+    ],
+    riskCauses: [
+      '新建管道,风险极低',
+      '管径小、压力低,泄漏影响范围有限',
+      '位于城南新开发区域,周边环境稳定',
+      'PE管材耐腐蚀性能好,寿命长'
+    ],
+    radarData: [8, 18, 10, 12, 22]
   }
 ])
 
@@ -300,7 +396,7 @@ const highRiskCount = computed(() => pipelines.value.filter(p => p.riskLevel ===
 const searchKeyword = ref('')
 const riskFilter = ref('')
 const currentPage = ref(1)
-const pageSize = 4
+const pageSize = 5
 const selectedPipeline = ref(pipelines.value[0])
 
 const filteredPipelines = computed(() => {
@@ -320,34 +416,41 @@ const paginatedPipelines = computed(() => {
   return filteredPipelines.value.slice(start, start + pageSize)
 })
 
-// 雷达图配置
-const radarChartOption = computed(() => {
-  if (!selectedPipeline.value) return {}
-  return {
+// 雷达图
+const radarRef = ref(null)
+let radarInstance = null
+
+function renderRadarChart() {
+  if (!radarRef.value || !selectedPipeline.value) return
+  if (!radarInstance) {
+    radarInstance = echarts.init(radarRef.value)
+  }
+  const data = selectedPipeline.value.radarData
+  radarInstance.setOption({
     tooltip: {},
-    legend: { data: ['本管段'] },
+    legend: { data: ['风险评分'], bottom: 0 },
     radar: {
       indicator: [
         { name: '管道老化风险', max: 100 },
-        { name: '第三方破坏风险', max: 100 },
+        { name: '第三方破坏', max: 100 },
         { name: '腐蚀泄漏风险', max: 100 },
-        { name: '周边危险源影响', max: 100 },
+        { name: '危险源影响', max: 100 },
         { name: '防护目标敏感性', max: 100 }
       ],
       shape: 'circle',
       center: ['50%', '50%'],
-      radius: '65%',
+      radius: '62%',
       name: { textStyle: { fontSize: 10, color: '#606266' } }
     },
     series: [{
       type: 'radar',
-      data: [{ value: selectedPipeline.value.radarData, name: '风险评分' }],
+      data: [{ value: data, name: '风险评分' }],
       areaStyle: { color: 'rgba(245, 108, 108, 0.2)' },
       lineStyle: { color: '#f56c6c', width: 2 },
       itemStyle: { color: '#f56c6c' }
     }]
-  }
-})
+  })
+}
 
 const getScoreClass = (score) => {
   if (score >= 70) return 'high-score'
@@ -355,9 +458,24 @@ const getScoreClass = (score) => {
   return 'low-score'
 }
 
-const selectPipeline = (pipeline) => { selectedPipeline.value = pipeline }
+const selectPipeline = (pipeline) => {
+  selectedPipeline.value = pipeline
+  nextTick(() => renderRadarChart())
+}
+
 const refreshData = () => { ElMessage.success('数据已刷新') }
 const exportReport = () => { ElMessage.success('导出风险分析报告(演示)') }
+
+onMounted(() => {
+  nextTick(() => renderRadarChart())
+})
+
+onBeforeUnmount(() => {
+  if (radarInstance) {
+    radarInstance.dispose()
+    radarInstance = null
+  }
+})
 </script>
 
 <style scoped>

+ 283 - 242
src/views/subSystem/gas/gasfxpg/gxfxpgsst.vue

@@ -15,11 +15,11 @@
       </div>
       <div class="header-right">
         <el-input
-            v-model="searchKeyword"
-            placeholder="搜索管段/位置"
-            clearable
-            :prefix-icon="Search"
-            style="width: 200px"
+          v-model="searchKeyword"
+          placeholder="搜索管段/位置"
+          clearable
+          :prefix-icon="Search"
+          style="width: 200px"
         />
         <el-button type="primary" :icon="Location" @click="locateSearch">定位</el-button>
         <el-button :icon="Refresh" @click="refreshMap">刷新</el-button>
@@ -93,7 +93,7 @@
 
         <div class="filter-section">
           <div class="filter-title">风险筛选</div>
-          <el-checkbox-group v-model="visibleRisks" class="risk-checkbox">
+          <el-checkbox-group v-model="visibleRisks" class="risk-checkbox" @change="onLayerChange">
             <el-checkbox value="major">重大风险(红)</el-checkbox>
             <el-checkbox value="larger">较大风险(橙)</el-checkbox>
             <el-checkbox value="general">一般风险(黄)</el-checkbox>
@@ -119,8 +119,10 @@
             综合风险指数: <span :class="riskIndexClass">{{ riskIndex }}</span>
           </div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="riskMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
+        <div class="map-wrapper">
+          <div id="riskFourColorBaiduMap" class="map-div"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
+          <!-- 悬浮信息卡 -->
           <div v-if="hoverPipeline" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
             <div class="hover-title">{{ hoverPipeline.name }}</div>
             <div>风险等级: {{ hoverPipeline.riskLevelName }}</div>
@@ -138,7 +140,7 @@
               <span class="tag-name">{{ currentPipeline.riskLevelName }}</span>
               <span class="tag-score">风险评分: {{ currentPipeline.riskScore }}分</span>
             </div>
-            <el-descriptions :column="1" border>
+            <el-descriptions :column="1" border size="small">
               <el-descriptions-item label="管段编号">{{ currentPipeline.code }}</el-descriptions-item>
               <el-descriptions-item label="管材材质">{{ currentPipeline.material }}</el-descriptions-item>
               <el-descriptions-item label="压力级别">{{ currentPipeline.pressureLevel }}</el-descriptions-item>
@@ -166,82 +168,135 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, Location, ZoomIn, ZoomOut, InfoFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 
-// 可见风险等级
-const visibleRisks = ref(['major', 'larger', 'general', 'low'])
-const searchKeyword = ref('')
+// ==================== 百度地图初始化 ====================
+const MAP_CENTER = { lng: 110.3940, lat: 28.4690 }
+const MAP_DEFAULT_ZOOM = 14
+
+let mapInstance = null
+const mapError = ref('')
+const currentLng = ref(110.394)
+const currentLat = ref(28.469)
+
+const overlayStore = {
+  major: [], larger: [], general: [], low: []
+}
+
+function waitForBMapGL() {
+  return new Promise((resolve, reject) => {
+    let attempts = 0
+    const timer = setInterval(() => {
+      attempts++
+      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL) }
+      if (attempts > 150) { clearInterval(timer); reject(new Error('百度地图API加载超时')) }
+    }, 200)
+  })
+}
+
+function initMap() {
+  const BMapGL = window.BMapGL
+  const el = document.getElementById('riskFourColorBaiduMap')
+  if (!el) return
+  mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+  mapInstance.enableScrollWheelZoom(true)
+  mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
+  mapInstance.addEventListener('mousemove', (e) => {
+    currentLng.value = e.latlng.lng
+    currentLat.value = e.latlng.lat
+  })
+  renderAllOverlays()
+}
+
+// ==================== 沅陵县燃气管网四色风险数据 ====================
+function getRiskLevel(score) {
+  if (score >= 75) return 'major'
+  if (score >= 55) return 'larger'
+  if (score >= 35) return 'general'
+  return 'low'
+}
+
+function getRiskLevelName(level) {
+  const map = { major: '重大风险', larger: '较大风险', general: '一般风险', low: '低风险' }
+  return map[level]
+}
+
+function getRiskColor(level) {
+  const map = { major: '#f56c6c', larger: '#e6a23c', general: '#f4d03f', low: '#409eff' }
+  return map[level]
+}
 
-// 模拟管线数据(四色风险等级)
 const pipelines = ref([
   {
-    id: 1, code: 'PL-001', name: '世纪大道中压管线', area: '浦东新区世纪大道沿线',
-    riskLevel: 'major', riskLevelName: '重大风险', riskScore: 92,
-    color: '#f56c6c', lineWidth: 6,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 12.5,
-    buryYear: '2018年', owner: '浦东燃气公司', lastInspection: '2025-02-15',
-    coordinates: [[121.48, 31.24], [121.50, 31.245], [121.52, 31.24], [121.54, 31.235]],
-    riskFactors: ['管道服役年限较长(7年)', '周边施工活动频繁', '防腐层检测有局部破损', '位于交通密集区域', '邻近加油站等危险源']
+    id: 1, code: 'PL-YL-001', name: '辰州路高压管线',
+    riskScore: 86.5, riskLevel: 'major', riskLevelName: '重大风险', color: '#f56c6c', lineWidth: 6,
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 3.8,
+    buryYear: '2015年', owner: '沅陵燃气有限公司', lastInspection: '2026-03-15',
+    path: [[110.3940, 28.4820], [110.3942, 28.4770], [110.3938, 28.4710], [110.3936, 28.4645], [110.3934, 28.4580]],
+    riskFactors: ['管道服役超过10年,老化程度较高', '辰州路主干道交通密集,第三方破坏风险大', '防腐层检测发现多处局部破损', '沿线有市政雨污分流施工活动', '邻近城南加油站等危险源']
   },
   {
-    id: 2, code: 'PL-002', name: '张杨路高压管线', area: '浦东新区张杨路沿线',
-    riskLevel: 'larger', riskLevelName: '较大风险', riskScore: 76,
-    color: '#e6a23c', lineWidth: 5,
-    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 500, length: 18.2,
-    buryYear: '2020年', owner: '浦西燃气公司', lastInspection: '2025-03-10',
-    coordinates: [[121.48, 31.255], [121.50, 31.25], [121.515, 31.248], [121.53, 31.245]],
-    riskFactors: ['高压运行风险较高', '沿线有在建地铁工程', '部分区域防腐层老化', '周边有大型商业综合体']
+    id: 2, code: 'PL-YL-002', name: '建设东路高压管',
+    riskScore: 79.2, riskLevel: 'major', riskLevelName: '重大风险', color: '#f56c6c', lineWidth: 6,
+    material: '钢管', pressureLevel: '高压 (1.6MPa)', diameter: 400, length: 3.6,
+    buryYear: '2016年', owner: '沅陵燃气有限公司', lastInspection: '2025-11-08',
+    path: [[110.3824, 28.4660], [110.3935, 28.4662], [110.4050, 28.4658]],
+    riskFactors: ['高压运行风险等级高', '管道穿越工业园区域,周边有化工企业', '检测发现多处腐蚀点,局部壁厚减薄', '建设路沿线重型车辆通行频繁', '历史维修频次较高']
   },
   {
-    id: 3, code: 'PL-003', name: '浦东南路低压管线', area: '浦东新区浦东南路',
-    riskLevel: 'general', riskLevelName: '一般风险', riskScore: 58,
-    color: '#f4d03f', lineWidth: 4,
-    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 8.6,
-    buryYear: '2022年', owner: '浦东燃气公司', lastInspection: '2025-01-20',
-    coordinates: [[121.485, 31.258], [121.495, 31.252], [121.50, 31.248]],
-    riskFactors: ['位于商业区人流量较大', '管道运行正常但有轻微腐蚀迹象', '周边有学校等敏感目标']
+    id: 3, code: 'PL-YL-003', name: '古城路中压管线',
+    riskScore: 64.3, riskLevel: 'larger', riskLevelName: '较大风险', color: '#e6a23c', lineWidth: 5,
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.2,
+    buryYear: '2018年', owner: '沅陵燃气有限公司', lastInspection: '2026-02-10',
+    path: [[110.3970, 28.4820], [110.3973, 28.4760], [110.3970, 28.4700], [110.3963, 28.4635]],
+    riskFactors: ['沿线有在建房地产开发工程', '与电力管沟交叉段存在杂散电流干扰', '古城路商业区人流量大,后果严重性较高', '部分区域防腐层出现老化脱落']
   },
   {
-    id: 4, code: 'PL-004', name: '龙阳路中压管线', area: '浦东新区龙阳路',
-    riskLevel: 'major', riskLevelName: '重大风险', riskScore: 88,
-    color: '#f56c6c', lineWidth: 6,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 10.3,
-    buryYear: '2016年', owner: '浦西燃气公司', lastInspection: '2024-11-08',
-    coordinates: [[121.47, 31.22], [121.49, 31.218], [121.51, 31.215], [121.53, 31.212]],
-    riskFactors: ['管道服役超过8年', '检测发现多处腐蚀点', '周边有化工企业', '历史维修频次高']
+    id: 4, code: 'PL-YL-004', name: '迎宾路中压管线',
+    riskScore: 55.8, riskLevel: 'larger', riskLevelName: '较大风险', color: '#e6a23c', lineWidth: 5,
+    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 3.6,
+    buryYear: '2019年', owner: '沅陵燃气有限公司', lastInspection: '2026-04-28',
+    path: [[110.3824, 28.4688], [110.3930, 28.4692], [110.4054, 28.4685]],
+    riskFactors: ['电力管沟交叉段存在杂散电流干扰风险', '迎宾路商业区人流较大', '钢管轻微腐蚀,需定期检测维护', '管线运行正常,整体风险可控']
   },
   {
-    id: 5, code: 'PL-005', name: '东方路中压管线', area: '浦东新区东方路',
-    riskLevel: 'larger', riskLevelName: '较大风险', riskScore: 68,
-    color: '#e6a23c', lineWidth: 5,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 300, length: 7.8,
-    buryYear: '2019年', owner: '浦东燃气公司', lastInspection: '2025-02-28',
-    coordinates: [[121.52, 31.232], [121.53, 31.238], [121.535, 31.245], [121.54, 31.25]],
-    riskFactors: ['位于商业区人流量大', '管道运行正常但有轻微腐蚀', '邻近多条市政管线']
+    id: 5, code: 'PL-YL-005', name: '天宁路中压管线',
+    riskScore: 48.2, riskLevel: 'general', riskLevelName: '一般风险', color: '#f4d03f', lineWidth: 4,
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 3.8,
+    buryYear: '2020年', owner: '沅陵燃气有限公司', lastInspection: '2026-05-12',
+    path: [[110.3850, 28.4755], [110.3935, 28.4758], [110.4088, 28.4750]],
+    riskFactors: ['地下车库施工距管线较近,需重点监控', 'PE管接头处需定期检测确保密封', '管道穿越太常安置区,人口密集', 'PE管材耐腐蚀但抗外力破坏能力较弱']
   },
   {
-    id: 6, code: 'PL-006', name: '临港新片区燃气管线', area: '浦东新区临港',
-    riskLevel: 'low', riskLevelName: '低风险', riskScore: 28,
-    color: '#409eff', lineWidth: 3,
-    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 15.6,
-    buryYear: '2023年', owner: '浦东燃气公司', lastInspection: '2025-03-05',
-    coordinates: [[121.50, 31.20], [121.52, 31.205], [121.54, 31.21]],
-    riskFactors: ['新建管道状况良好', '周边环境稳定', '无重大危险源', '低压运行风险可控']
+    id: 6, code: 'PL-YL-006', name: '辰州北路中压管线',
+    riskScore: 42.5, riskLevel: 'general', riskLevelName: '一般风险', color: '#f4d03f', lineWidth: 4,
+    material: 'PE管', pressureLevel: '中压 (0.4MPa)', diameter: 200, length: 1.2,
+    buryYear: '2021年', owner: '沅陵燃气有限公司', lastInspection: '2026-05-28',
+    path: [[110.3940, 28.4820], [110.3942, 28.4855]],
+    riskFactors: ['与综合管廊共通道,电力通信管线交叉密集', '管廊内可燃气体可能积聚,交叉风险高', '管段较短但周边地下环境复杂', 'PE管与管廊结构隔离需持续维护']
   },
   {
-    id: 7, code: 'PL-007', name: '金桥出口加工区管线', area: '浦东新区金桥',
-    riskLevel: 'general', riskLevelName: '一般风险', riskScore: 48,
-    color: '#f4d03f', lineWidth: 4,
-    material: '钢管', pressureLevel: '中压 (0.4MPa)', diameter: 250, length: 9.2,
-    buryYear: '2017年', owner: '浦西燃气公司', lastInspection: '2025-01-15',
-    coordinates: [[121.53, 31.24], [121.545, 31.245], [121.555, 31.25]],
-    riskFactors: ['管道服役年限较长', '工业区车辆通行频繁', '防腐层状况一般']
+    id: 7, code: 'PL-YL-007', name: '滨江路低压管线',
+    riskScore: 28.6, riskLevel: 'low', riskLevelName: '低风险', color: '#409eff', lineWidth: 3,
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 200, length: 3.8,
+    buryYear: '2022年', owner: '沅陵燃气有限公司', lastInspection: '2026-06-03',
+    path: [[110.3820, 28.4635], [110.3930, 28.4638], [110.4066, 28.4630]],
+    riskFactors: ['新建管道,整体状况良好', '低压运行,泄漏后果可控', '河堤段需关注汛期水土流失影响', 'PE管耐腐蚀,维护需求低']
+  },
+  {
+    id: 8, code: 'PL-YL-008', name: '太常南路低压管线',
+    riskScore: 18.3, riskLevel: 'low', riskLevelName: '低风险', color: '#409eff', lineWidth: 3,
+    material: 'PE管', pressureLevel: '低压 (0.1MPa)', diameter: 160, length: 1.5,
+    buryYear: '2023年', owner: '沅陵燃气有限公司', lastInspection: '2026-06-08',
+    path: [[110.3934, 28.4580], [110.3932, 28.4555]],
+    riskFactors: ['新建管道风险极低', '管径小、压力低,泄漏影响范围有限', '位于城南新开发区域,周边环境稳定', 'PE管材耐腐蚀性能好,寿命长']
   }
 ])
 
-// 统计数据
+// ==================== 统计数据 ====================
 const majorCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'major').length)
 const largerCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'larger').length)
 const generalCount = computed(() => pipelines.value.filter(p => p.riskLevel === 'general').length)
@@ -262,7 +317,10 @@ const riskIndexClass = computed(() => {
   return 'low'
 })
 
-// 过滤后的管线
+// ==================== 筛选 ====================
+const visibleRisks = ref(['major', 'larger', 'general', 'low'])
+const searchKeyword = ref('')
+
 const filteredPipelines = computed(() => {
   let list = pipelines.value
   if (searchKeyword.value) {
@@ -272,173 +330,116 @@ const filteredPipelines = computed(() => {
   return list.filter(p => visibleRisks.value.includes(p.riskLevel))
 })
 
-// GIS画布相关
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
+// ==================== 交互状态 ====================
 const hoverPipeline = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
+const hoverX = ref(0)
+const hoverY = ref(0)
 const currentPipeline = ref(null)
 const detailDrawerVisible = ref(false)
 
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
+// ==================== 地图覆盖物渲染 ====================
+function clearOverlays() {
+  Object.values(overlayStore).forEach(arr => {
+    arr.forEach(o => { try { mapInstance.removeOverlay(o) } catch {} })
+    arr.length = 0
+  })
 }
 
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#f0f2f0'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#d4dcd4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0); ctx.lineTo(x, mapHeight); ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y); ctx.lineTo(mapWidth, y); ctx.stroke()
-  }
-  ctx.fillStyle = '#b8d4b8'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('世纪大道', 350, 120)
-  ctx.fillText('张杨路', 450, 280)
-  ctx.fillText('东方路', 580, 200)
-  ctx.fillText('龙阳路', 200, 370)
+function addToStore(layer, overlay) {
+  if (overlayStore[layer]) overlayStore[layer].push(overlay)
 }
 
-function drawPipelines() {
-  filteredPipelines.value.forEach(pipeline => {
-    const points = pipeline.coordinates.map(coord => worldToCanvas(coord[0], coord[1]))
-    if (points.length < 2) return
-
-    ctx.beginPath()
-    ctx.moveTo(points[0].x, points[0].y)
-    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y)
-    ctx.strokeStyle = pipeline.color
-    ctx.lineWidth = pipeline.lineWidth * Math.max(0.8, Math.min(1.5, mapZoom))
-    ctx.stroke()
-
-    // 绘制管线标签
-    const midIdx = Math.floor(points.length / 2)
-    if (points[midIdx] && mapZoom > 0.8) {
-      ctx.fillStyle = '#333'
-      ctx.font = '10px "Microsoft YaHei"'
-      ctx.shadowBlur = 0
-      ctx.fillText(pipeline.name.slice(0, 6), points[midIdx].x - 18, points[midIdx].y - 8)
-      ctx.fillStyle = pipeline.color
-      ctx.font = 'bold 10px sans-serif'
-      ctx.fillText(pipeline.riskScore + '分', points[midIdx].x - 12, points[midIdx].y + 12)
-    }
+function renderPipelines() {
+  const BMapGL = window.BMapGL
+  pipelines.value.forEach(p => {
+    if (!visibleRisks.value.includes(p.riskLevel)) return
+    const points = p.path.map(c => new BMapGL.Point(c[0], c[1]))
+    const polyline = new BMapGL.Polyline(points, {
+      strokeColor: p.color, strokeWeight: p.lineWidth, strokeOpacity: 0.82
+    })
+    mapInstance.addOverlay(polyline)
+    addToStore(p.riskLevel, polyline)
+
+    polyline.addEventListener('click', () => {
+      currentPipeline.value = p
+      detailDrawerVisible.value = true
+    })
+    polyline.addEventListener('mouseover', () => { hoverPipeline.value = p })
+    polyline.addEventListener('mouseout', () => { hoverPipeline.value = null })
+
+    const mid = Math.floor(points.length / 2)
+    // 风险评分标签
+    const scoreLabel = new BMapGL.Label(p.riskScore + '分', {
+      position: points[mid],
+      offset: new BMapGL.Size(0, -16)
+    })
+    scoreLabel.setStyle({
+      color: '#fff', fontSize: '11px', fontWeight: 'bold', padding: '2px 8px',
+      backgroundColor: p.color, borderRadius: '10px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(scoreLabel)
+    addToStore(p.riskLevel, scoreLabel)
+
+    // 管线名称标签
+    const nameLabel = new BMapGL.Label(p.name, {
+      position: points[mid],
+      offset: new BMapGL.Size(0, 4)
+    })
+    nameLabel.setStyle({
+      color: '#333', fontSize: '10px', padding: '1px 4px',
+      backgroundColor: 'rgba(255,255,255,0.85)', borderRadius: '2px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(nameLabel)
+    addToStore(p.riskLevel, nameLabel)
   })
 }
 
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawPipelines()
+function renderAllOverlays() {
+  clearOverlays()
+  renderPipelines()
+  onLayerChange()
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-function refreshMap() { renderMap() }
-
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 25, hit = null
-
-  filteredPipelines.value.forEach(pipeline => {
-    let minPointDist = Infinity
-    pipeline.coordinates.forEach(coord => {
-      const { x, y } = worldToCanvas(coord[0], coord[1])
-      const dist = Math.hypot(mouseX - x, mouseY - y)
-      if (dist < minPointDist) minPointDist = dist
+// ==================== 图层显隐 ====================
+function onLayerChange() {
+  const set = new Set(visibleRisks.value)
+  Object.entries(overlayStore).forEach(([layer, overlays]) => {
+    overlays.forEach(o => {
+      try { set.has(layer) ? o.show() : o.hide() } catch {}
     })
-    if (minPointDist < minDist) {
-      minDist = minPointDist
-      hit = pipeline
-    }
   })
-  if (hit) { currentPipeline.value = hit; detailDrawerVisible.value = true }
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 25, hit = null
-
-  filteredPipelines.value.forEach(pipeline => {
-    let minPointDist = Infinity
-    pipeline.coordinates.forEach(coord => {
-      const { x, y } = worldToCanvas(coord[0], coord[1])
-      const dist = Math.hypot(mouseX - x, mouseY - y)
-      if (dist < minPointDist) minPointDist = dist
-    })
-    if (minPointDist < minDist) {
-      minDist = minPointDist
-      hit = pipeline
-    }
-  })
-  if (hit) {
-    hoverPipeline.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else {
-    hoverPipeline.value = null
+// ==================== 工具栏操作 ====================
+function zoomIn() {
+  if (mapInstance) mapInstance.zoomIn()
+}
+
+function zoomOut() {
+  if (mapInstance) mapInstance.zoomOut()
+}
+
+function fitBounds() {
+  if (mapInstance) {
+    const BMapGL = window.BMapGL
+    mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
   }
 }
 
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
-  })
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
-    renderMap()
-  })
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
-  })
+function refreshMap() {
+  renderAllOverlays()
+  ElMessage.success('地图已刷新')
 }
 
 function locateSearch() {
   if (!searchKeyword.value) return
-  const found = pipelines.value.find(p => p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value))
-  if (found) {
-    const centerCoord = found.coordinates[Math.floor(found.coordinates.length / 2)]
-    mapZoom = 2
-    const center = worldToCanvas(centerCoord[0], centerCoord[1])
-    offsetX = mapWidth/2 - center.x
-    offsetY = mapHeight/2 - center.y
-    renderMap()
+  const found = pipelines.value.find(p =>
+    p.name.includes(searchKeyword.value) || p.code.includes(searchKeyword.value)
+  )
+  if (found && mapInstance) {
+    const BMapGL = window.BMapGL
+    const mid = found.path[Math.floor(found.path.length / 2)]
+    mapInstance.centerAndZoom(new BMapGL.Point(mid[0], mid[1]), 16)
     currentPipeline.value = found
     detailDrawerVisible.value = true
     ElMessage.success(`已定位到: ${found.name}`)
@@ -447,59 +448,81 @@ function locateSearch() {
   }
 }
 
-const locatePipeline = () => {
-  if (currentPipeline.value) {
-    const centerCoord = currentPipeline.value.coordinates[Math.floor(currentPipeline.value.coordinates.length / 2)]
-    mapZoom = 2
-    const center = worldToCanvas(centerCoord[0], centerCoord[1])
-    offsetX = mapWidth/2 - center.x
-    offsetY = mapHeight/2 - center.y
-    renderMap()
-    detailDrawerVisible.value = false
-  }
+function locatePipeline() {
+  if (!currentPipeline.value || !mapInstance) return
+  const BMapGL = window.BMapGL
+  const mid = currentPipeline.value.path[Math.floor(currentPipeline.value.path.length / 2)]
+  mapInstance.centerAndZoom(new BMapGL.Point(mid[0], mid[1]), 16)
+  detailDrawerVisible.value = false
 }
 
-const viewDetail = () => { ElMessage.info('查看详细风险评估报告(演示)') }
+function viewDetail() {
+  ElMessage.info('查看详细风险评估报告(演示)')
+}
 
-watch(() => visibleRisks.value, () => renderMap(), { deep: true })
-watch(() => searchKeyword.value, () => renderMap())
+// ==================== 悬浮卡定位 ====================
+function onWindowMouseMove(e) {
+  if (hoverPipeline.value) {
+    hoverX.value = e.clientX + 12
+    hoverY.value = e.clientY - 40
+  }
+}
 
-onMounted(() => {
-  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  window.addEventListener('mousemove', onWindowMouseMove)
+  try {
+    await waitForBMapGL()
+    nextTick(() => initMap())
+  } catch {
+    mapError.value = '百度地图加载失败,请刷新页面重试'
+  }
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('mousemove', onWindowMouseMove)
+  if (mapInstance) {
+    try { mapInstance.destroy() } catch {}
+    mapInstance = null
+  }
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
 </script>
 
 <style scoped>
 .risk-four-color-map {
   background: #f0f2f6;
-  height: 100vh;
+  height: 100%;
   display: flex;
   flex-direction: column;
   overflow: hidden;
 }
+
 .page-header {
-  background: white;
+  background: #fff;
   padding: 12px 24px;
   display: flex;
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
 }
+
 .header-left { display: flex; align-items: center; gap: 20px; }
 .header-left .title { font-size: 1.3rem; font-weight: 600; }
+
 .stats-badge { display: flex; gap: 16px; }
 .stat-item { font-size: 12px; display: flex; align-items: center; gap: 6px; }
 .stat-item.major { color: #f56c6c; }
 .stat-item.larger { color: #e6a23c; }
-.stat-item.general { color: #f4d03f; }
+.stat-item.general { color: #d4a017; }
 .stat-item.low { color: #409eff; }
 .stat-item .dot { width: 8px; height: 8px; border-radius: 50%; }
 .stat-item.major .dot { background: #f56c6c; }
 .stat-item.larger .dot { background: #e6a23c; }
 .stat-item.general .dot { background: #f4d03f; }
 .stat-item.low .dot { background: #409eff; }
-.header-right { display: flex; gap: 12px; }
+
+.header-right { display: flex; gap: 12px; align-items: center; }
 
 .main-layout {
   display: flex;
@@ -508,19 +531,23 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   padding: 12px;
   gap: 12px;
 }
+
 .legend-panel {
   width: 260px;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   padding: 16px;
   display: flex;
   flex-direction: column;
   gap: 16px;
   overflow-y: auto;
+  flex-shrink: 0;
 }
+
 .panel-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
-.legend-item { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
-.legend-color { width: 30px; height: 20px; border-radius: 4px; }
+
+.legend-item { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
+.legend-color { width: 30px; height: 20px; border-radius: 4px; flex-shrink: 0; }
 .legend-item.major .legend-color { background: #f56c6c; }
 .legend-item.larger .legend-color { background: #e6a23c; }
 .legend-item.general .legend-color { background: #f4d03f; }
@@ -551,38 +578,49 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
 
 .info-note { background: #f5f7fa; border-radius: 8px; padding: 10px; font-size: 11px; color: #606266; display: flex; align-items: flex-start; gap: 8px; }
 
+/* GIS地图 */
 .gis-container {
   flex: 1;
-  background: white;
+  background: #fff;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
   overflow: hidden;
 }
+
 .map-toolbar {
   padding: 10px 16px;
-  border-bottom: 1px solid #e9ecef;
+  border-bottom: 1px solid #ebeef5;
   display: flex;
   justify-content: space-between;
   align-items: center;
+  flex-shrink: 0;
 }
+
 .risk-summary { font-size: 13px; }
 .risk-summary .high { color: #f56c6c; font-weight: bold; }
 .risk-summary .medium { color: #e6a23c; font-weight: bold; }
 .risk-summary .low { color: #67c23a; font-weight: bold; }
-.map-wrapper { flex: 1; position: relative; cursor: crosshair; }
-canvas { width: 100%; height: 100%; display: block; }
+
+.map-wrapper { flex: 1; position: relative; overflow: hidden; }
+.map-div { width: 100%; height: 100%; }
+.map-error { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f56c6c; font-size: 14px; }
+
+/* 悬浮信息卡 */
 .hover-card {
   position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
+  background: rgba(0, 0, 0, 0.82);
+  color: #fff;
+  padding: 10px 14px;
   border-radius: 10px;
   font-size: 12px;
   pointer-events: none;
-  z-index: 200;
+  z-index: 2000;
+  line-height: 1.6;
 }
-.hover-title { font-weight: 600; margin-bottom: 4px; }
+.hover-title { font-weight: 600; margin-bottom: 4px; font-size: 13px; }
+
+/* 详情抽屉 */
 .pipeline-detail { padding: 8px; }
 .risk-tag { text-align: center; padding: 12px; border-radius: 8px; margin-bottom: 16px; }
 .risk-tag.major { background: #f56c6c20; border: 1px solid #f56c6c; color: #f56c6c; }
@@ -591,8 +629,11 @@ canvas { width: 100%; height: 100%; display: block; }
 .risk-tag.low { background: #409eff20; border: 1px solid #409eff; color: #409eff; }
 .tag-name { font-size: 18px; font-weight: bold; display: block; }
 .tag-score { font-size: 12px; }
-.sub-title { font-weight: 600; margin: 16px 0 8px 0; }
+
+.risk-factors { margin-top: 16px; }
+.risk-factors .sub-title { font-weight: 600; margin-bottom: 8px; }
 .risk-factors ul { margin: 0; padding-left: 20px; }
 .risk-factors li { margin: 6px 0; font-size: 13px; }
+
 .action-buttons { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
-</style>
+</style>

+ 75 - 55
src/views/subSystem/gas/gasjcyj/gxllyctx.vue

@@ -74,9 +74,8 @@
                 <el-icon><Location /></el-icon> {{ alert.address }}
               </div>
               <div class="alert-data">
-                <span class="data-item">流量: {{ alert.pressure }} kPa</span>
-                <span class="data-item">流量: {{ alert.flow }} m³/h</span>
-                <span class="data-item">阈值: {{ alert.threshold }} kPa</span>
+                <span class="data-item">瞬时流量: {{ alert.flow }} Nm³/h</span>
+                <span class="data-item">上限: {{ alert.threshold }} Nm³/h</span>
               </div>
               <div class="alert-time">{{ alert.alertTime }}</div>
             </div>
@@ -110,13 +109,12 @@
           <el-table :data="paginatedHistory" stripe border style="width: 100%" @row-click="viewAlertDetail">
             <el-table-column prop="pointName" label="监测点位" min-width="140" show-overflow-tooltip />
             <el-table-column prop="address" label="位置" min-width="160" show-overflow-tooltip />
-            <el-table-column prop="pressure" label="异常流量(kPa)" width="110" sortable>
+            <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="130" sortable>
               <template #default="{ row }">
-                <span class="alert-value">{{ row.pressure }}</span>
+                <span class="alert-value">{{ row.flow }}</span>
               </template>
             </el-table-column>
-            <el-table-column prop="flow" label="实时流量(m³/h)" width="110" />
-            <el-table-column prop="threshold" label="阈值(kPa)" width="90" />
+            <el-table-column prop="threshold" label="上限(Nm³/h)" width="110" />
             <el-table-column prop="alertTime" label="异常时间" width="150" sortable />
             <el-table-column prop="status" label="状态" width="80" align="center">
               <template #default="{ row }">
@@ -145,16 +143,15 @@
     </div>
 
     <!-- 异常详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`异常详情 - ${currentAlert?.pointName}`" size="480px" direction="rtl">
+    <el-drawer v-model="detailDrawerVisible" :title="`异常详情 - ${currentAlert?.pointName}`" size="480px" direction="rtl" @opened="onDrawerOpened">
       <div v-if="currentAlert" class="alert-detail">
         <el-descriptions :column="1" border>
           <el-descriptions-item label="监测点位">{{ currentAlert.pointName }}</el-descriptions-item>
           <el-descriptions-item label="点位编号">{{ currentAlert.pointCode }}</el-descriptions-item>
           <el-descriptions-item label="详细地址">{{ currentAlert.address }}</el-descriptions-item>
           <el-descriptions-item label="所属管线">{{ currentAlert.pipelineName }}</el-descriptions-item>
-          <el-descriptions-item label="异常流量">{{ currentAlert.pressure }} kPa</el-descriptions-item>
-          <el-descriptions-item label="实时流量">{{ currentAlert.flow }} m³/h</el-descriptions-item>
-          <el-descriptions-item label="报警阈值">{{ currentAlert.threshold }} kPa</el-descriptions-item>
+          <el-descriptions-item label="异常流量">{{ currentAlert.flow }} Nm³/h</el-descriptions-item>
+          <el-descriptions-item label="报警阈值">{{ currentAlert.threshold }} Nm³/h</el-descriptions-item>
           <el-descriptions-item label="异常时间">{{ currentAlert.alertTime }}</el-descriptions-item>
           <el-descriptions-item label="处理状态">
             <el-tag :type="currentAlert.status === 'resolved' ? 'success' : 'danger'">
@@ -169,9 +166,7 @@
         <!-- 流量趋势图 -->
         <div class="trend-section">
           <div class="section-title">流量趋势(最近1小时)</div>
-          <div class="trend-chart">
-            <v-chart :option="trendChartOption" autoresize />
-          </div>
+          <div ref="chartRef" class="trend-chart"></div>
         </div>
 
         <!-- 处理操作 -->
@@ -185,50 +180,60 @@
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟SCADA实时数据 - 实时异常
+// ==================== 沅陵县管线流量异常报警数据 ====================
+// 实时异常(当前未处理)
 const realtimeAlerts = ref([
-  { id: 1, pointCode: 'PR-001', pointName: '人民路流量监测点', address: '人民路与世纪大道路口', pipelineName: '世纪大道中压管线',
-    pressure: 412, flow: 1250, threshold: 400, alertTime: '2025-04-02 15:23:45', status: 'pending',
-    trendData: [385, 388, 392, 395, 398, 402, 405, 408, 410, 412, 412, 411, 409, 407, 405, 403, 400, 398, 396, 394, 392, 390, 388, 386] },
-  { id: 2, pointCode: 'PR-003', pointName: '东方路流量监测点', address: '东方路蓝村路口', pipelineName: '东方路中压管线',
-    pressure: 365, flow: 980, threshold: 350, alertTime: '2025-04-02 15:18:22', status: 'pending',
-    trendData: [340, 345, 350, 352, 355, 358, 360, 362, 365, 365, 364, 362, 360, 358, 356, 354, 352, 350, 348, 346, 344, 342, 340, 338] },
-  { id: 3, pointCode: 'PR-005', pointName: '龙阳路流量监测点', address: '龙阳路白杨路口', pipelineName: '龙阳路中压管线',
-    pressure: 385, flow: 1100, threshold: 370, alertTime: '2025-04-02 15:05:10', status: 'pending',
-    trendData: [360, 365, 368, 370, 372, 375, 378, 380, 383, 385, 385, 384, 382, 380, 378, 376, 374, 372, 370, 368, 366, 364, 362, 360] }
+  { id: 1, pointCode: 'FL-YL-002', pointName: '辰州中路流量计2#', address: '沅陵县太常片区辰州中路调压站进口侧', pipelineName: '辰州中路高压段',
+    flow: 2620, threshold: 2500, alertTime: '2026-06-13 15:31:45', status: 'pending',
+    trendData: [2100, 2150, 2200, 2250, 2300, 2350, 2380, 2420, 2480, 2520, 2550, 2580, 2600, 2620, 2620, 2610, 2600, 2580, 2560, 2540, 2520, 2500, 2480, 2450] },
+  { id: 2, pointCode: 'FL-YL-003', pointName: '古城中路流量计2#', address: '沅陵县太常片区古城中路太常安置区段', pipelineName: '古城中路中压段',
+    flow: 820, threshold: 800, alertTime: '2026-06-13 15:29:58', status: 'pending',
+    trendData: [680, 700, 720, 740, 750, 760, 770, 780, 790, 800, 810, 820, 820, 815, 810, 805, 800, 795, 790, 785, 780, 775, 770, 765] },
+  { id: 3, pointCode: 'FL-YL-006', pointName: '滨江东路流量计2#', address: '沅陵县太常片区滨江东路商业街', pipelineName: '滨江东路低压段',
+    flow: 210, threshold: 200, alertTime: '2026-06-13 15:30:40', status: 'pending',
+    trendData: [155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 210, 208, 206, 204, 200, 198, 195, 192, 190, 185, 180, 175] },
+  { id: 4, pointCode: 'FL-YL-015', pointName: '建设东路流量计3#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
+    flow: 2480, threshold: 2500, alertTime: '2026-06-13 14:55:20', status: 'pending',
+    trendData: [2200, 2250, 2280, 2300, 2320, 2350, 2380, 2400, 2420, 2440, 2460, 2480, 2480, 2470, 2460, 2440, 2420, 2400, 2380, 2360, 2340, 2320, 2300, 2280] }
 ])
 
-// 模拟历史异常数据
+// 历史异常(已处理/待处理)
 const historyAlerts = ref([
-  { id: 101, pointCode: 'PR-002', pointName: '张杨路流量监测点', address: '张杨路松林路口', pipelineName: '张杨路高压管线',
-    pressure: 435, flow: 1500, threshold: 420, alertTime: '2025-04-01 14:30:00', status: 'resolved', resolveTime: '2025-04-01 15:45:00', handler: '张建国', remark: '调压阀调整后恢复正常' },
-  { id: 102, pointCode: 'PR-004', pointName: '浦东南路流量监测点', address: '浦东南路400号', pipelineName: '浦东南路中压管线',
-    pressure: 398, flow: 1050, threshold: 380, alertTime: '2025-04-01 09:15:00', status: 'resolved', resolveTime: '2025-04-01 10:30:00', handler: '李志强', remark: '管网流量波动,已稳定' },
-  { id: 103, pointCode: 'PR-006', pointName: '金桥流量监测点', address: '金桥路666号', pipelineName: '金桥路低压管线',
-    pressure: 275, flow: 820, threshold: 260, alertTime: '2025-03-31 16:20:00', status: 'pending', resolveTime: null, handler: null, remark: null },
-  { id: 104, pointCode: 'PR-007', pointName: '临港流量监测点', address: '临港新城环湖西二路', pipelineName: '临港燃气管道',
-    pressure: 355, flow: 920, threshold: 340, alertTime: '2025-03-31 10:45:00', status: 'resolved', resolveTime: '2025-03-31 12:00:00', handler: '王大明', remark: '正常波动' },
-  { id: 105, pointCode: 'PR-008', pointName: '外高桥流量监测点', address: '外高桥保税区', pipelineName: '外高桥高压管线',
-    pressure: 505, flow: 1800, threshold: 490, alertTime: '2025-03-30 22:30:00', status: 'resolved', resolveTime: '2025-03-31 01:00:00', handler: '赵志远', remark: '夜间用气高峰导致' }
+  { id: 101, pointCode: 'FL-YL-001', pointName: '辰州北路流量计1#', address: '沅陵县太常片区辰州北路门站出口计量间', pipelineName: '辰州北路高压段',
+    flow: 2550, threshold: 2500, alertTime: '2026-06-12 14:30:00', status: 'resolved', resolveTime: '2026-06-12 15:45:00', handler: '张建国', remark: '下游用气高峰,调压后恢复正常',
+    trendData: [2100, 2150, 2200, 2280, 2350, 2420, 2500, 2550, 2550, 2520, 2460, 2360, 2250, 2150, 2100, 2050, 2000, 1980, 1950, 1920, 1900, 1880, 1850, 1830] },
+  { id: 102, pointCode: 'FL-YL-004', pointName: '古城北路流量计1#', address: '沅陵县太常片区古城北路与天宁路交叉口', pipelineName: '古城北路中压段',
+    flow: 850, threshold: 800, alertTime: '2026-06-12 09:15:00', status: 'resolved', resolveTime: '2026-06-12 10:30:00', handler: '李志强', remark: '管网流量波动,阀门微调后稳定',
+    trendData: [680, 700, 720, 740, 760, 780, 810, 840, 850, 850, 840, 810, 780, 750, 730, 710, 690, 680, 670, 660, 650, 640, 630, 620] },
+  { id: 103, pointCode: 'FL-YL-007', pointName: '迎宾西路流量计1#', address: '沅陵县太常片区迎宾西路县政府对面', pipelineName: '迎宾西路中压段',
+    flow: 520, threshold: 500, alertTime: '2026-06-11 16:20:00', status: 'pending', resolveTime: null, handler: null, remark: '持续超限,待现场巡检确认',
+    trendData: [430, 440, 450, 460, 470, 480, 490, 500, 510, 520, 520, 520, 525, 530, 530, 525, 520, 520, 515, 510, 510, 505, 500, 495] },
+  { id: 104, pointCode: 'FL-YL-012', pointName: '滨江东路流量计2#', address: '沅陵县太常片区滨江东路商业街', pipelineName: '滨江东路低压段',
+    flow: 215, threshold: 200, alertTime: '2026-06-11 10:45:00', status: 'resolved', resolveTime: '2026-06-11 12:00:00', handler: '王大明', remark: '商业街午间用气高峰,属正常波动',
+    trendData: [160, 165, 170, 178, 188, 195, 205, 212, 215, 215, 210, 200, 190, 180, 172, 165, 160, 156, 152, 150, 148, 145, 143, 140] },
+  { id: 105, pointCode: 'FL-YL-009', pointName: '建设路流量计1#', address: '沅陵县太常片区建设路县政府南侧计量站', pipelineName: '建设路中压段',
+    flow: 0, threshold: 800, alertTime: '2026-06-13 08:15:00', status: 'pending', resolveTime: null, handler: null, remark: '设备离线,数据中断,需现场检查',
+    trendData: [820, 810, 800, 780, 750, 700, 620, 500, 350, 180, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
+  { id: 106, pointCode: 'FL-YL-015', pointName: '建设东路流量计3#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
+    flow: 2580, threshold: 2500, alertTime: '2026-06-10 22:30:00', status: 'resolved', resolveTime: '2026-06-10 23:15:00', handler: '赵志远', remark: '工业园区夜间加班用气高峰导致',
+    trendData: [2200, 2250, 2300, 2350, 2400, 2450, 2500, 2550, 2580, 2580, 2550, 2480, 2380, 2280, 2200, 2150, 2100, 2080, 2050, 2020, 2000, 1980, 1950, 1930] },
+  { id: 107, pointCode: 'FL-YL-002', pointName: '辰州中路流量计2#', address: '沅陵县太常片区辰州中路调压站进口侧', pipelineName: '辰州中路高压段',
+    flow: 2510, threshold: 2500, alertTime: '2026-06-10 18:20:00', status: 'resolved', resolveTime: '2026-06-10 19:30:00', handler: '陈志明', remark: '晚高峰居民用气激增,调压后恢复',
+    trendData: [2180, 2220, 2280, 2350, 2420, 2480, 2510, 2510, 2480, 2400, 2300, 2220, 2150, 2100, 2060, 2020, 1980, 1950, 1920, 1900, 1880, 1850, 1830, 1800] }
 ])
 
 // 统计数据
-const totalPoints = computed(() => 28)
-const realtimeTrend = computed(() => 2)
-const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2025-04-02')).length)
+const totalPoints = computed(() => 15)
+const realtimeTrend = computed(() => 1)
+const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
 const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
-const abnormalRate = computed(() => 6.5)
-const avgResponseTime = computed(() => 68)
+const abnormalRate = computed(() => (realtimeAlerts.value.length / totalPoints.value * 100).toFixed(1))
+const avgResponseTime = computed(() => 62)
 
 // 筛选
 const dateRange = ref([])
@@ -268,20 +273,30 @@ const paginatedHistory = computed(() => {
 // 当前选中告警
 const currentAlert = ref(null)
 const detailDrawerVisible = ref(false)
-const trendChartOption = computed(() => {
-  if (!currentAlert.value?.trendData) return {}
+const chartRef = ref(null)
+let chartInstance = null
+
+function renderChart() {
+  if (!chartRef.value || !currentAlert.value?.trendData) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
   const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
   const threshold = currentAlert.value.threshold
-  return {
+  chartInstance.setOption({
     tooltip: { trigger: 'axis' },
     xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
-    yAxis: { type: 'value', name: '流量 (kPa)' },
+    yAxis: { type: 'value', name: 'Nm³/h', min: 0 },
     series: [
-      { name: '实时流量', type: 'line', data: currentAlert.value.trendData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.1 } },
-      { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none' }
+      { name: '瞬时流量', type: 'line', data: currentAlert.value.trendData, smooth: true, symbol: 'circle', symbolSize: 3, lineStyle: { color: '#f56c6c', width: 2 }, itemStyle: { color: '#f56c6c' }, areaStyle: { color: 'rgba(245,108,108,0.1)' } },
+      { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none', itemStyle: { color: '#e6a23c' } }
     ]
-  }
-})
+  })
+}
+
+function onDrawerOpened() {
+  nextTick(() => renderChart())
+}
 
 // 方法
 const viewAlertDetail = (alert) => {
@@ -331,6 +346,11 @@ const exportReport = () => {
 
 // 监听日期范围变化重置页码
 watch([dateRange, historySearch], () => { historyPage.value = 1 })
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
 </script>
 
 <style scoped>

+ 73 - 42
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -145,7 +145,7 @@
     </div>
 
     <!-- 异常详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`异常详情 - ${currentAlert?.pointName}`" size="480px" direction="rtl">
+    <el-drawer v-model="detailDrawerVisible" :title="`异常详情 - ${currentAlert?.pointName}`" size="480px" direction="rtl" @opened="onDrawerOpened">
       <div v-if="currentAlert" class="alert-detail">
         <el-descriptions :column="1" border>
           <el-descriptions-item label="监测点位">{{ currentAlert.pointName }}</el-descriptions-item>
@@ -169,9 +169,7 @@
         <!-- 压力趋势图 -->
         <div class="trend-section">
           <div class="section-title">压力趋势(最近1小时)</div>
-          <div class="trend-chart">
-            <v-chart :option="trendChartOption" autoresize />
-          </div>
+          <div ref="chartRef" class="trend-chart"></div>
         </div>
 
         <!-- 处理操作 -->
@@ -185,50 +183,68 @@
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟SCADA实时数据 - 实时异常
+// 沅陵县压力实时异常数据
 const realtimeAlerts = ref([
-  { id: 1, pointCode: 'PR-001', pointName: '人民路压力监测点', address: '人民路与世纪大道路口', pipelineName: '世纪大道中压管线',
-    pressure: 412, flow: 1250, threshold: 400, alertTime: '2025-04-02 15:23:45', status: 'pending',
-    trendData: [385, 388, 392, 395, 398, 402, 405, 408, 410, 412, 412, 411, 409, 407, 405, 403, 400, 398, 396, 394, 392, 390, 388, 386] },
-  { id: 2, pointCode: 'PR-003', pointName: '东方路压力监测点', address: '东方路蓝村路口', pipelineName: '东方路中压管线',
-    pressure: 365, flow: 980, threshold: 350, alertTime: '2025-04-02 15:18:22', status: 'pending',
-    trendData: [340, 345, 350, 352, 355, 358, 360, 362, 365, 365, 364, 362, 360, 358, 356, 354, 352, 350, 348, 346, 344, 342, 340, 338] },
-  { id: 3, pointCode: 'PR-005', pointName: '龙阳路压力监测点', address: '龙阳路白杨路口', pipelineName: '龙阳路中压管线',
-    pressure: 385, flow: 1100, threshold: 370, alertTime: '2025-04-02 15:05:10', status: 'pending',
-    trendData: [360, 365, 368, 370, 372, 375, 378, 380, 383, 385, 385, 384, 382, 380, 378, 376, 374, 372, 370, 368, 366, 364, 362, 360] }
+  { id: 1, pointCode: 'PR-YL-002', pointName: '辰州中路高压测点2#', address: '沅陵县太常片区辰州中路与天宁路交叉口南侧', pipelineName: '辰州中路高压段',
+    pressure: 432, flow: 2180, threshold: 420, alertTime: '2026-06-13 15:31:45', status: 'pending',
+    trendData: [390, 395, 398, 402, 405, 408, 410, 413, 416, 418, 420, 422, 424, 426, 428, 430, 431, 432, 432, 431, 430, 429, 428, 432] },
+  { id: 2, pointCode: 'PR-YL-005', pointName: '古城中路中压测点2#', address: '沅陵县太常片区古城中路太常安置区段', pipelineName: '古城中路中压段',
+    pressure: 142, flow: 860, threshold: 150, alertTime: '2026-06-13 15:29:58', status: 'pending',
+    trendData: [160, 158, 156, 155, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 142, 143, 144, 145, 146, 147, 148, 142] },
+  { id: 3, pointCode: 'PR-YL-015', pointName: '建设东路高压测点4#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
+    pressure: 410, flow: 1950, threshold: 420, alertTime: '2026-06-13 15:31:25', status: 'pending',
+    trendData: [395, 398, 400, 402, 405, 408, 410, 412, 414, 415, 416, 417, 418, 419, 410, 409, 408, 407, 406, 405, 404, 403, 402, 410] }
 ])
 
-// 模拟历史异常数据
+// 沅陵县压力历史异常数据
 const historyAlerts = ref([
-  { id: 101, pointCode: 'PR-002', pointName: '张杨路压力监测点', address: '张杨路松林路口', pipelineName: '张杨路高压管线',
-    pressure: 435, flow: 1500, threshold: 420, alertTime: '2025-04-01 14:30:00', status: 'resolved', resolveTime: '2025-04-01 15:45:00', handler: '张建国', remark: '调压阀调整后恢复正常' },
-  { id: 102, pointCode: 'PR-004', pointName: '浦东南路压力监测点', address: '浦东南路400号', pipelineName: '浦东南路中压管线',
-    pressure: 398, flow: 1050, threshold: 380, alertTime: '2025-04-01 09:15:00', status: 'resolved', resolveTime: '2025-04-01 10:30:00', handler: '李志强', remark: '管网压力波动,已稳定' },
-  { id: 103, pointCode: 'PR-006', pointName: '金桥压力监测点', address: '金桥路666号', pipelineName: '金桥路低压管线',
-    pressure: 275, flow: 820, threshold: 260, alertTime: '2025-03-31 16:20:00', status: 'pending', resolveTime: null, handler: null, remark: null },
-  { id: 104, pointCode: 'PR-007', pointName: '临港压力监测点', address: '临港新城环湖西二路', pipelineName: '临港燃气管道',
-    pressure: 355, flow: 920, threshold: 340, alertTime: '2025-03-31 10:45:00', status: 'resolved', resolveTime: '2025-03-31 12:00:00', handler: '王大明', remark: '正常波动' },
-  { id: 105, pointCode: 'PR-008', pointName: '外高桥压力监测点', address: '外高桥保税区', pipelineName: '外高桥高压管线',
-    pressure: 505, flow: 1800, threshold: 490, alertTime: '2025-03-30 22:30:00', status: 'resolved', resolveTime: '2025-03-31 01:00:00', handler: '赵志远', remark: '夜间用气高峰导致' }
+  { id: 101, pointCode: 'PR-YL-001', pointName: '辰州北路高压测点1#', address: '沅陵县太常片区辰州北路与建设路交叉口北侧', pipelineName: '辰州北路高压段',
+    pressure: 438, flow: 2320, threshold: 420, alertTime: '2026-06-13 09:15:00', status: 'resolved', resolveTime: '2026-06-13 10:30:00', handler: '张建国', remark: '早高峰用气波动,调压后恢复正常',
+    trendData: [380, 385, 392, 400, 410, 420, 430, 438, 438, 435, 428, 418, 405, 390, 378, 370, 365, 360, 358, 355, 352, 350, 348, 346] },
+  { id: 102, pointCode: 'PR-YL-012', pointName: '滨江东路低压测点2#', address: '沅陵县太常片区滨江东路与天宁东路交叉口', pipelineName: '滨江东路低压段',
+    pressure: 78, flow: 520, threshold: 80, alertTime: '2026-06-13 08:45:00', status: 'pending', resolveTime: null, handler: null, remark: null,
+    trendData: [95, 93, 90, 88, 85, 83, 81, 80, 78, 78, 77, 77, 76, 76, 75, 75, 74, 74, 73, 73, 72, 72, 71, 71] },
+  { id: 103, pointCode: 'PR-YL-003', pointName: '辰州南路高压测点3#', address: '沅陵县太常片区辰州南路近滨江路口', pipelineName: '辰州南路高压段',
+    pressure: 445, flow: 2480, threshold: 420, alertTime: '2026-06-12 16:20:00', status: 'resolved', resolveTime: '2026-06-12 17:45:00', handler: '李志强', remark: '管网压力波动,已稳定',
+    trendData: [390, 395, 400, 408, 418, 428, 440, 445, 445, 440, 430, 418, 405, 392, 382, 375, 370, 366, 362, 358, 355, 352, 350, 348] },
+  { id: 104, pointCode: 'PR-YL-009', pointName: '建设路中压测点1#', address: '沅陵县太常片区建设路县政府南侧', pipelineName: '建设路中压段',
+    pressure: 0, flow: 0, threshold: 150, alertTime: '2026-06-12 12:05:00', status: 'resolved', resolveTime: '2026-06-12 14:30:00', handler: '王大明', remark: '设备离线,现场重启后恢复',
+    trendData: [175, 172, 168, 165, 160, 155, 148, 135, 110, 80, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 160, 170, 175] },
+  { id: 105, pointCode: 'PR-YL-008', pointName: '迎宾东路中压测点2#', address: '沅陵县太常片区迎宾东路城郊公交站东侧', pipelineName: '迎宾东路中压段',
+    pressure: 268, flow: 1350, threshold: 250, alertTime: '2026-06-11 22:30:00', status: 'resolved', resolveTime: '2026-06-12 01:00:00', handler: '赵志远', remark: '夜间用气高峰导致瞬时超压',
+    trendData: [230, 235, 240, 245, 250, 255, 260, 265, 268, 268, 265, 258, 248, 238, 230, 225, 222, 220, 218, 215, 212, 210, 208, 206] },
+  { id: 106, pointCode: 'PR-YL-013', pointName: '天宁西路低压测点1#', address: '沅陵县太常片区天宁西路太常卫生院外墙', pipelineName: '天宁西路低压段',
+    pressure: 72, flow: 480, threshold: 80, alertTime: '2026-06-11 14:10:00', status: 'pending', resolveTime: null, handler: null, remark: null,
+    trendData: [88, 86, 84, 82, 80, 78, 76, 74, 73, 72, 72, 71, 71, 70, 70, 69, 69, 68, 68, 67, 67, 66, 66, 65] },
+  { id: 107, pointCode: 'PR-YL-006', pointName: '古城南路中压测点3#', address: '沅陵县太常片区古城南路近建设路口', pipelineName: '古城南路中压段',
+    pressure: 268, flow: 1420, threshold: 250, alertTime: '2026-06-10 10:45:00', status: 'resolved', resolveTime: '2026-06-10 12:00:00', handler: '陈文斌', remark: '午间用气小高峰,已回落正常',
+    trendData: [225, 230, 238, 245, 252, 258, 265, 268, 268, 265, 258, 248, 238, 230, 225, 220, 216, 212, 210, 208, 206, 204, 202, 200] }
 ])
 
 // 统计数据
-const totalPoints = computed(() => 28)
-const realtimeTrend = computed(() => 2)
-const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2025-04-02')).length)
+const totalPoints = 15
+const realtimeTrend = 1
+const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
 const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
-const abnormalRate = computed(() => 6.5)
-const avgResponseTime = computed(() => 68)
+const abnormalRate = computed(() => {
+  const totalAbnormal = realtimeAlerts.value.length + historyAlerts.value.filter(a => a.status === 'pending').length
+  return ((totalAbnormal / totalPoints) * 100).toFixed(1)
+})
+const avgResponseTime = computed(() => {
+  const resolved = historyAlerts.value.filter(a => a.status === 'resolved' && a.resolveTime)
+  if (!resolved.length) return '--'
+  const times = resolved.map(a => {
+    const start = new Date(a.alertTime)
+    const end = new Date(a.resolveTime)
+    return (end - start) / 60000
+  })
+  return Math.round(times.reduce((a, b) => a + b, 0) / times.length)
+})
 
 // 筛选
 const dateRange = ref([])
@@ -268,11 +284,17 @@ const paginatedHistory = computed(() => {
 // 当前选中告警
 const currentAlert = ref(null)
 const detailDrawerVisible = ref(false)
-const trendChartOption = computed(() => {
-  if (!currentAlert.value?.trendData) return {}
+const chartRef = ref(null)
+let chartInstance = null
+
+function renderChart() {
+  if (!chartRef.value || !currentAlert.value?.trendData) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
   const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
   const threshold = currentAlert.value.threshold
-  return {
+  chartInstance.setOption({
     tooltip: { trigger: 'axis' },
     xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
     yAxis: { type: 'value', name: '压力 (kPa)' },
@@ -280,8 +302,12 @@ const trendChartOption = computed(() => {
       { name: '实时压力', type: 'line', data: currentAlert.value.trendData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.1 } },
       { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none' }
     ]
-  }
-})
+  })
+}
+
+function onDrawerOpened() {
+  nextTick(() => renderChart())
+}
 
 // 方法
 const viewAlertDetail = (alert) => {
@@ -331,6 +357,11 @@ const exportReport = () => {
 
 // 监听日期范围变化重置页码
 watch([dateRange, historySearch], () => { historyPage.value = 1 })
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
 </script>
 
 <style scoped>

+ 97 - 50
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -5,7 +5,7 @@
     <div class="page-header">
       <div class="header-left">
         <el-icon><Warning /></el-icon>
-        <span class="title">燃气场站甲烷报警中心</span>
+        <span class="title">可燃气体中心</span>
         <div class="stats-badge">
           <span class="stat-item critical"><i class="dot"></i>实时报警 {{ realtimeAlerts.length }}</span>
           <span class="stat-item warning"><i class="dot"></i>历史报警 {{ historyAlerts.length }}</span>
@@ -23,9 +23,7 @@
             size="default"
         />
         <el-select v-model="companyFilter" placeholder="燃气公司" clearable style="width: 140px" size="default">
-          <el-option label="浦东燃气" value="pudong" />
-          <el-option label="浦西燃气" value="puxi" />
-          <el-option label="南汇燃气" value="nanhui" />
+          <el-option label="沅陵燃气" value="yuanling" />
         </el-select>
         <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="Download" @click="exportReport">导出报表</el-button>
@@ -162,7 +160,7 @@
     </div>
 
     <!-- 报警详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`报警详情 - ${currentAlert?.stationName}`" size="480px" direction="rtl">
+    <el-drawer v-model="detailDrawerVisible" :title="`报警详情 - ${currentAlert?.stationName}`" size="480px" direction="rtl" @opened="onDrawerOpened">
       <div v-if="currentAlert" class="alert-detail">
         <el-descriptions :column="1" border>
           <el-descriptions-item label="场站名称">{{ currentAlert.stationName }}</el-descriptions-item>
@@ -192,9 +190,7 @@
         <!-- 浓度趋势图 -->
         <div class="trend-section">
           <div class="section-title">甲烷浓度趋势(最近1小时)</div>
-          <div class="trend-chart">
-            <v-chart :option="trendChartOption" autoresize />
-          </div>
+          <div ref="chartRef" class="trend-chart"></div>
         </div>
 
         <!-- 处理操作 -->
@@ -208,55 +204,82 @@
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search, OfficeBuilding } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟SCADA接入数据 - 实时报警
+// 沅陵县场站可燃气体实时报警数据
 const realtimeAlerts = ref([
-  { id: 1, stationCode: 'PD-01', stationName: '浦东天然气门站', companyName: '浦东燃气', companyId: 'pudong',
-    address: '浦东新区世纪大道888号', concentration: 48, threshold: 20, level: 'critical',
-    alertTime: '2025-04-02 15:23:45', status: 'pending',
+  { id: 1, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 48, threshold: 20, level: 'critical',
+    alertTime: '2026-06-13 15:23:45', status: 'pending',
     trendData: [12, 15, 18, 22, 28, 32, 38, 42, 45, 48, 48, 47, 45, 42, 38, 35, 32, 28, 25, 22, 20, 18, 16, 14] },
-  { id: 2, stationCode: 'PX-02', stationName: '浦西调压站', companyName: '浦西燃气', companyId: 'puxi',
-    address: '徐汇区漕溪北路200号', concentration: 28, threshold: 20, level: 'warning',
-    alertTime: '2025-04-02 14:58:12', status: 'pending',
+  { id: 2, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 28, threshold: 20, level: 'warning',
+    alertTime: '2026-06-13 14:58:12', status: 'pending',
     trendData: [15, 17, 19, 22, 24, 26, 28, 28, 27, 26, 25, 24, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11] },
-  { id: 3, stationCode: 'NH-03', stationName: '南汇燃气门站', companyName: '南汇燃气', companyId: 'nanhui',
-    address: '南汇区沪南公路168号', concentration: 35, threshold: 20, level: 'critical',
-    alertTime: '2025-04-02 14:32:05', status: 'pending',
+  { id: 3, stationCode: 'YL-GY-01', stationName: '工业园计量站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区建设东路工业园入口东侧', concentration: 35, threshold: 20, level: 'critical',
+    alertTime: '2026-06-13 14:32:05', status: 'pending',
     trendData: [18, 20, 22, 25, 28, 30, 32, 35, 35, 34, 33, 32, 30, 28, 26, 24, 22, 20, 19, 18, 17, 16, 15, 14] }
 ])
 
-// 模拟历史报警数据
+// 沅陵县场站可燃气体历史报警数据
 const historyAlerts = ref([
-  { id: 101, stationCode: 'BS-04', stationName: '宝山LNG储配站', companyName: '浦西燃气', companyId: 'puxi',
-    address: '宝山区蕴川路500号', concentration: 42, threshold: 20, level: 'critical',
-    alertTime: '2025-04-01 16:30:00', status: 'resolved', resolveTime: '2025-04-01 17:45:00', handler: '张建国', remark: '阀门轻微泄漏已修复' },
-  { id: 102, stationCode: 'LG-05', stationName: '临港燃气门站', companyName: '浦东燃气', companyId: 'pudong',
-    address: '临港新城环湖西二路88号', concentration: 25, threshold: 20, level: 'warning',
-    alertTime: '2025-04-01 09:15:00', status: 'resolved', resolveTime: '2025-04-01 10:20:00', handler: '李志强', remark: '通风后浓度下降' },
-  { id: 103, stationCode: 'JQ-06', stationName: '金桥调压站', companyName: '浦东燃气', companyId: 'pudong',
-    address: '浦东新区金桥路666号', concentration: 52, threshold: 20, level: 'critical',
-    alertTime: '2025-03-31 22:45:00', status: 'pending', resolveTime: null, handler: null, remark: null },
-  { id: 104, stationCode: 'MH-07', stationName: '闵行燃气门站', companyName: '浦西燃气', companyId: 'puxi',
-    address: '闵行区沪闵路5000号', concentration: 22, threshold: 20, level: 'warning',
-    alertTime: '2025-03-31 14:20:00', status: 'resolved', resolveTime: '2025-03-31 15:10:00', handler: '王大明', remark: '正常波动' },
-  { id: 105, stationCode: 'JD-08', stationName: '嘉定调压站', companyName: '浦西燃气', companyId: 'puxi',
-    address: '嘉定区嘉松北路88号', concentration: 38, threshold: 20, level: 'critical',
-    alertTime: '2025-03-30 10:00:00', status: 'resolved', resolveTime: '2025-03-30 11:30:00', handler: '赵志远', remark: '设备校准后正常' }
+  { id: 101, stationCode: 'YL-MZ-01', stationName: '太常天然气门站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区辰州北路末段天然气门站', concentration: 42, threshold: 20, level: 'critical',
+    alertTime: '2026-06-13 08:30:00', status: 'resolved', resolveTime: '2026-06-13 09:45:00', handler: '张建国', remark: '法兰连接处微漏,紧固后恢复正常',
+    trendData: [10, 12, 14, 18, 22, 28, 32, 36, 40, 42, 42, 41, 38, 34, 28, 22, 16, 12, 10, 9, 8, 8, 7, 7] },
+  { id: 102, stationCode: 'YL-CBTY-01', stationName: '城北调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区天宁西路与古城北路交叉口', concentration: 25, threshold: 20, level: 'warning',
+    alertTime: '2026-06-13 07:15:00', status: 'resolved', resolveTime: '2026-06-13 08:20:00', handler: '李志强', remark: '通风后浓度下降至正常范围',
+    trendData: [12, 13, 15, 17, 19, 21, 23, 25, 25, 24, 23, 21, 18, 15, 13, 11, 10, 9, 8, 8, 7, 7, 6, 6] },
+  { id: 103, stationCode: 'YL-CNTY-01', stationName: '城南调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区建设路与古城南路交叉口南侧', concentration: 52, threshold: 20, level: 'critical',
+    alertTime: '2026-06-12 22:45:00', status: 'pending', resolveTime: null, handler: null, remark: null,
+    trendData: [18, 20, 22, 26, 30, 34, 38, 42, 46, 50, 52, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40] },
+  { id: 104, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 22, threshold: 20, level: 'warning',
+    alertTime: '2026-06-12 14:20:00', status: 'resolved', resolveTime: '2026-06-12 15:10:00', handler: '王大明', remark: '巡检期间短暂波动,已自动恢复',
+    trendData: [10, 11, 13, 15, 18, 20, 22, 22, 21, 20, 18, 16, 14, 12, 10, 9, 8, 8, 7, 7, 6, 6, 5, 5] },
+  { id: 105, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 38, threshold: 20, level: 'critical',
+    alertTime: '2026-06-11 10:00:00', status: 'resolved', resolveTime: '2026-06-11 11:30:00', handler: '赵志远', remark: '储罐区检测器校准后读数正常',
+    trendData: [8, 10, 14, 18, 24, 30, 35, 38, 38, 37, 35, 30, 24, 18, 14, 10, 8, 7, 6, 6, 5, 5, 4, 4] },
+  { id: 106, stationCode: 'YL-GY-01', stationName: '工业园计量站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区建设东路工业园入口东侧', concentration: 26, threshold: 20, level: 'warning',
+    alertTime: '2026-06-11 16:10:00', status: 'pending', resolveTime: null, handler: null, remark: null,
+    trendData: [14, 15, 16, 18, 20, 22, 24, 26, 26, 27, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22] },
+  { id: 107, stationCode: 'YL-CBTY-01', stationName: '城北调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区天宁西路与古城北路交叉口', concentration: 24, threshold: 20, level: 'warning',
+    alertTime: '2026-06-10 09:30:00', status: 'resolved', resolveTime: '2026-06-10 10:15:00', handler: '陈文斌', remark: '调压器维护后短暂波动,已稳定',
+    trendData: [11, 13, 15, 18, 21, 23, 24, 24, 23, 21, 18, 15, 12, 10, 9, 8, 7, 7, 6, 6, 5, 5, 5, 4] },
+  { id: 108, stationCode: 'YL-CNTY-01', stationName: '城南调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区建设路与古城南路交叉口南侧', concentration: 31, threshold: 20, level: 'critical',
+    alertTime: '2026-06-12 09:50:00', status: 'resolved', resolveTime: '2026-06-12 11:15:00', handler: '李志强', remark: '阀门密封圈老化,更换后恢复正常',
+    trendData: [12, 14, 16, 19, 22, 26, 29, 31, 31, 30, 28, 25, 21, 16, 12, 9, 7, 6, 5, 5, 4, 4, 3, 3] },
+  { id: 109, stationCode: 'YL-MZ-01', stationName: '太常天然气门站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区辰州北路末段天然气门站', concentration: 23, threshold: 20, level: 'warning',
+    alertTime: '2026-06-11 18:30:00', status: 'resolved', resolveTime: '2026-06-11 19:10:00', handler: '张建国', remark: '交接班巡检发现,通风处置后恢复',
+    trendData: [13, 14, 16, 18, 20, 22, 23, 23, 22, 20, 17, 14, 12, 10, 9, 8, 7, 7, 6, 6, 5, 5, 5, 4] },
+  { id: 110, stationCode: 'YL-TY-01', stationName: '太常调压站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区古城中路与天宁路交叉口东侧', concentration: 33, threshold: 20, level: 'critical',
+    alertTime: '2026-06-10 15:40:00', status: 'pending', resolveTime: null, handler: null, remark: null,
+    trendData: [15, 17, 19, 22, 25, 28, 30, 32, 33, 33, 33, 32, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26] },
+  { id: 111, stationCode: 'YL-LNG-01', stationName: '太常LNG储配站', companyName: '沅陵燃气', companyId: 'yuanling',
+    address: '沅陵县太常片区辰州东路末段LNG储配站', concentration: 21, threshold: 20, level: 'warning',
+    alertTime: '2026-06-10 11:20:00', status: 'resolved', resolveTime: '2026-06-10 12:05:00', handler: '赵志远', remark: '槽车卸液作业期间瞬时波动',
+    trendData: [8, 9, 11, 14, 17, 19, 21, 21, 20, 18, 15, 12, 10, 8, 7, 6, 5, 5, 4, 4, 3, 3, 3, 2] }
 ])
 
 // 统计数据
-const totalStations = computed(() => 12)
-const realtimeTrend = computed(() => 1)
-const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2025-04-02')).length)
+const totalStations = computed(() => {
+  const all = [...realtimeAlerts.value, ...historyAlerts.value]
+  return new Set(all.map(a => a.stationCode)).size
+})
+const realtimeTrend = 1
+const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
 const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
 const maxConcentration = computed(() => {
   const all = [...realtimeAlerts.value, ...historyAlerts.value]
@@ -267,7 +290,16 @@ const maxConcentrationPoint = computed(() => {
   const max = Math.max(...all.map(a => a.concentration))
   return all.find(a => a.concentration === max)?.stationName || '—'
 })
-const avgResponseTime = computed(() => 45)
+const avgResponseTime = computed(() => {
+  const resolved = historyAlerts.value.filter(a => a.status === 'resolved' && a.resolveTime)
+  if (!resolved.length) return '--'
+  const times = resolved.map(a => {
+    const start = new Date(a.alertTime)
+    const end = new Date(a.resolveTime)
+    return (end - start) / 60000
+  })
+  return Math.round(times.reduce((a, b) => a + b, 0) / times.length)
+})
 
 // 筛选
 const dateRange = ref([])
@@ -319,11 +351,17 @@ const paginatedHistory = computed(() => {
 // 当前选中报警
 const currentAlert = ref(null)
 const detailDrawerVisible = ref(false)
-const trendChartOption = computed(() => {
-  if (!currentAlert.value?.trendData) return {}
+const chartRef = ref(null)
+let chartInstance = null
+
+function renderChart() {
+  if (!chartRef.value || !currentAlert.value?.trendData) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
   const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
   const threshold = currentAlert.value.threshold
-  return {
+  chartInstance.setOption({
     tooltip: { trigger: 'axis' },
     xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
     yAxis: { type: 'value', name: '甲烷浓度 (ppm)' },
@@ -331,8 +369,12 @@ const trendChartOption = computed(() => {
       { name: '甲烷浓度', type: 'line', data: currentAlert.value.trendData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.1 } },
       { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none' }
     ]
-  }
-})
+  })
+}
+
+function onDrawerOpened() {
+  nextTick(() => renderChart())
+}
 
 // 方法
 const viewAlertDetail = (alert) => {
@@ -383,6 +425,11 @@ const exportReport = () => {
 // 监听筛选变化重置页码
 watch([dateRange, companyFilter, historySearch], () => { historyPage.value = 1 })
 watch(companyFilter, () => { realtimePage.value = 1 })
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
 </script>
 
 <style scoped>

+ 306 - 245
src/views/subSystem/gas/gasjcyj/rqjcbjyzt.vue

@@ -5,7 +5,7 @@
     <div class="page-header">
       <div class="header-left">
         <el-icon><MapLocation /></el-icon>
-        <span class="title">燃气安全监测报警一张图</span>
+        <span class="title">燃气安全监测报警图</span>
         <div class="stats-badge">
           <span class="stat-item critical"><i class="dot"></i>严重报警 {{ criticalCount }}</span>
           <span class="stat-item warning"><i class="dot"></i>预警 {{ warningCount }}</span>
@@ -13,13 +13,7 @@
         </div>
       </div>
       <div class="header-right">
-        <el-input
-            v-model="searchKeyword"
-            placeholder="搜索设施/地址"
-            clearable
-            :prefix-icon="Search"
-            style="width: 200px"
-        />
+        <el-input v-model="searchKeyword" placeholder="搜索设施/地址" clearable :prefix-icon="Search" style="width:200px" @input="onSearch" />
         <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
         <el-button :icon="FullScreen" @click="fitBounds">全图</el-button>
       </div>
@@ -29,11 +23,9 @@
     <div class="main-layout">
       <!-- 左侧图层控制面板 -->
       <div class="layer-panel">
-        <div class="layer-header">
-          <el-icon></el-icon> 报警态势图层
-        </div>
+        <div class="layer-header">报警态势图层</div>
         <div class="layer-tree">
-          <el-checkbox-group v-model="visibleLayers">
+          <el-checkbox-group v-model="visibleLayers" @change="onLayerChange">
             <div class="layer-group">
               <div class="group-title">燃气管网</div>
               <el-checkbox value="highPressure">高压管线</el-checkbox>
@@ -42,9 +34,9 @@
             </div>
             <div class="layer-group">
               <div class="group-title">地下空间气体报警</div>
-              <el-checkbox value="gasCritical">严重报警(>10%LEL)</el-checkbox>
+              <el-checkbox value="gasCritical">严重报警(&gt;10%LEL)</el-checkbox>
               <el-checkbox value="gasWarning">预警(5-10%LEL)</el-checkbox>
-              <el-checkbox value="gasNormal">正常(<5%LEL)</el-checkbox>
+              <el-checkbox value="gasNormal">正常(&lt;5%LEL)</el-checkbox>
             </div>
             <div class="layer-group">
               <div class="group-title">管线压力/流量报警</div>
@@ -91,16 +83,9 @@
           </el-button-group>
           <div class="coordinate-info">坐标: {{ currentLng.toFixed(4) }}, {{ currentLat.toFixed(4) }}</div>
         </div>
-        <div class="map-wrapper" ref="mapContainer">
-          <canvas id="gasAlertMapCanvas" ref="canvasRef" @click="onMapClick" @mousemove="onCanvasMouseMove"></canvas>
-          <div v-if="hoverFeature" class="hover-card" :style="{ top: hoverY + 'px', left: hoverX + 'px' }">
-            <div class="hover-title">{{ hoverFeature.name }}</div>
-            <div>类型: {{ hoverFeature.typeName }}</div>
-            <div v-if="hoverFeature.concentration">浓度: {{ hoverFeature.concentration }} %LEL</div>
-            <div v-if="hoverFeature.pressure">压力: {{ hoverFeature.pressure }} kPa</div>
-            <div v-if="hoverFeature.flow">流量: {{ hoverFeature.flow }} m³/h</div>
-            <div>报警级别: {{ hoverFeature.level === 'critical' ? '严重' : hoverFeature.level === 'warning' ? '预警' : '正常' }}</div>
-          </div>
+        <div class="map-wrapper">
+          <div id="gasAlertBaiduMap" class="map-div"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
         </div>
 
         <!-- 详情抽屉 -->
@@ -116,16 +101,17 @@
                   {{ currentFeature.level === 'critical' ? '严重报警' : currentFeature.level === 'warning' ? '预警' : '正常' }}
                 </el-tag>
               </el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.concentration" label="甲烷浓度">
+              <el-descriptions-item v-if="currentFeature.concentration !== undefined" label="甲烷浓度">
                 <span :class="{ 'critical-value': currentFeature.concentration > 10 }">{{ currentFeature.concentration }} %LEL</span>
               </el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.pressure" label="管线压力">{{ currentFeature.pressure }} kPa</el-descriptions-item>
-              <el-descriptions-item v-if="currentFeature.flow" label="瞬时流量">{{ currentFeature.flow }} m³/h</el-descriptions-item>
+              <el-descriptions-item v-if="currentFeature.pressure !== undefined" label="管线压力">{{ currentFeature.pressure }} kPa</el-descriptions-item>
+              <el-descriptions-item v-if="currentFeature.flow !== undefined" label="瞬时流量">{{ currentFeature.flow }} Nm³/h</el-descriptions-item>
+              <el-descriptions-item v-if="currentFeature.threshold !== undefined" label="报警阈值">{{ currentFeature.threshold }} {{ currentFeature.thresholdUnit }}</el-descriptions-item>
               <el-descriptions-item label="报警时间">{{ currentFeature.alertTime }}</el-descriptions-item>
             </el-descriptions>
             <div class="action-section" v-if="currentFeature.level !== 'normal'">
               <el-button type="primary" size="small" @click="handleAlert">处理报警</el-button>
-              <el-button type="info" size="small" @click="dispatchOrder">派发工单</el-button>
+              <el-button type="warning" size="small" @click="dispatchOrder">派发工单</el-button>
             </div>
           </div>
         </el-drawer>
@@ -135,255 +121,337 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import { ref, computed, reactive, onMounted, onBeforeUnmount } from 'vue'
 import { MapLocation, Search, Refresh, FullScreen, ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 
-// 可见图层
+// ==================== 图层状态 ====================
 const visibleLayers = ref(['highPressure', 'mediumPressure', 'gasCritical', 'gasWarning', 'pressureAlert', 'stationCritical'])
 
-// --- 管线数据 ---
-const pipelines = ref([
-  { id: 'P1', name: '张杨路高压管线', type: 'highPressure', color: '#f56c6c', lineWidth: 4,
-    coordinates: [[121.48, 31.24], [121.50, 31.245], [121.52, 31.24], [121.54, 31.235]] },
-  { id: 'P2', name: '世纪大道中压管线', type: 'mediumPressure', color: '#e6a23c', lineWidth: 3,
-    coordinates: [[121.49, 31.23], [121.51, 31.228], [121.53, 31.225], [121.55, 31.22]] },
-  { id: 'P3', name: '浦东南路低压管线', type: 'lowPressure', color: '#67c23a', lineWidth: 2,
-    coordinates: [[121.485, 31.255], [121.50, 31.25], [121.515, 31.248], [121.53, 31.245]] }
-])
+const overlayStore = {
+  highPressure: [], mediumPressure: [], lowPressure: [],
+  gasCritical: [], gasWarning: [], gasNormal: [],
+  pressureAlert: [], flowAlert: [],
+  stationCritical: [], stationWarning: []
+}
+
+// ==================== 百度地图相关 ====================
+let mapInstance = null
+let activeInfoWindow = null
+const mapError = ref('')
+const currentLng = ref(110.395)
+const currentLat = ref(28.470)
+const MAP_CENTER = { lng: 110.395, lat: 28.470 }
+const MAP_DEFAULT_ZOOM = 14
+
+function waitForBMapGL() {
+  return new Promise((resolve, reject) => {
+    if (window.BMapGL) { resolve(window.BMapGL); return }
+    let attempts = 0
+    const timer = setInterval(() => {
+      attempts++
+      if (window.BMapGL) { clearInterval(timer); resolve(window.BMapGL) }
+      if (attempts > 150) { clearInterval(timer); reject(new Error('百度地图API加载超时')) }
+    }, 200)
+  })
+}
+
+function initMap() {
+  const BMapGL = window.BMapGL
+  const el = document.getElementById('gasAlertBaiduMap')
+  if (!el) return
+  mapInstance = new BMapGL.Map(el, { enableMapClick: true })
+  const point = new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat)
+  mapInstance.centerAndZoom(point, MAP_DEFAULT_ZOOM)
+  mapInstance.enableScrollWheelZoom(true)
+  mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
+  mapInstance.addEventListener('mousemove', (e) => {
+    currentLng.value = e.latlng.lng
+    currentLat.value = e.latlng.lat
+  })
+  renderAllOverlays()
+}
 
-// --- 地下空间气体监测点 ---
+// ==================== 沅陵县静态数据 ====================
+
+// --- 管线数据 ---
+const pipelines = [
+  { id: 'P1', name: '辰州路高压管线', type: 'highPressure', color: '#f56c6c', width: 5,
+    path: [[110.3940, 28.4820], [110.3942, 28.4770], [110.3938, 28.4710], [110.3936, 28.4645]] },
+  { id: 'P2', name: '建设东路高压管', type: 'highPressure', color: '#f56c6c', width: 5,
+    path: [[110.3940, 28.4660], [110.3980, 28.4662], [110.4050, 28.4658]] },
+  { id: 'P3', name: '古城路中压管线', type: 'mediumPressure', color: '#e6a23c', width: 4,
+    path: [[110.3970, 28.4820], [110.3973, 28.4760], [110.3970, 28.4700], [110.3963, 28.4635]] },
+  { id: 'P4', name: '迎宾路中压管线', type: 'mediumPressure', color: '#e6a23c', width: 4,
+    path: [[110.3824, 28.4688], [110.3930, 28.4692], [110.4054, 28.4685]] },
+  { id: 'P5', name: '建设路中压管线', type: 'mediumPressure', color: '#e6a23c', width: 4,
+    path: [[110.3840, 28.4658], [110.3935, 28.4660], [110.4050, 28.4655]] },
+  { id: 'P6', name: '天宁路中压管线', type: 'mediumPressure', color: '#e6a23c', width: 4,
+    path: [[110.3850, 28.4755], [110.3935, 28.4758], [110.4088, 28.4750]] },
+  { id: 'P7', name: '滨江路低压管线', type: 'lowPressure', color: '#67c23a', width: 3,
+    path: [[110.3820, 28.4635], [110.3930, 28.4638], [110.4066, 28.4630]] }
+]
+
+// --- 地下空间气体报警点 ---
 const gasPoints = ref([
-  { id: 'G1', name: '人民广场地下管廊', type: 'gasMonitor', category: 'gasCritical', level: 'critical', concentration: 15.8, threshold: 8,
-    lng: 121.487, lat: 31.239, address: '人民大道200号地下管廊', alertTime: '2025-04-02 15:23:45', code: 'CH4-001', typeName: '气体监测点' },
-  { id: 'G2', name: '世纪大道电力隧道', type: 'gasMonitor', category: 'gasCritical', level: 'critical', concentration: 12.3, threshold: 8,
-    lng: 121.505, lat: 31.235, address: '世纪大道福山路口', alertTime: '2025-04-02 14:58:12', code: 'CH4-002', typeName: '气体监测点' },
-  { id: 'G3', name: '张杨路雨水管', type: 'gasMonitor', category: 'gasWarning', level: 'warning', concentration: 6.8, threshold: 8,
-    lng: 121.52, lat: 31.242, address: '张杨路松林路口', alertTime: '2025-04-02 14:32:05', code: 'CH4-003', typeName: '气体监测点' },
-  { id: 'G4', name: '龙阳路污水管', type: 'gasMonitor', category: 'gasNormal', level: 'normal', concentration: 2.5, threshold: 8,
-    lng: 121.51, lat: 31.215, address: '龙阳路白杨路口', alertTime: '2025-04-02 14:15:30', code: 'CH4-004', typeName: '气体监测点' }
+  { id: 'G1', name: '辰州北路综合管廊', type: 'gasMonitor', category: 'gasCritical', level: 'critical', concentration: 15.8, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.3940, lat: 28.4780, address: '沅陵县太常片区辰州北路综合管廊入口', alertTime: '2026-06-13 15:23:45', code: 'CH4-YL-001', typeName: '地下空间气体监测点' },
+  { id: 'G2', name: '古城中路地下管沟', type: 'gasMonitor', category: 'gasCritical', level: 'critical', concentration: 12.3, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.3972, lat: 28.4720, address: '沅陵县太常片区古城中路电力管沟', alertTime: '2026-06-13 14:58:12', code: 'CH4-YL-002', typeName: '地下空间气体监测点' },
+  { id: 'G3', name: '建设路雨水管廊', type: 'gasMonitor', category: 'gasWarning', level: 'warning', concentration: 6.8, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.3935, lat: 28.4662, address: '沅陵县太常片区建设路雨水管廊检查井', alertTime: '2026-06-13 14:32:05', code: 'CH4-YL-003', typeName: '地下空间气体监测点' },
+  { id: 'G4', name: '天宁西路地下车库', type: 'gasMonitor', category: 'gasWarning', level: 'warning', concentration: 7.5, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.3855, lat: 28.4755, address: '沅陵县太常片区天宁西路地下停车场', alertTime: '2026-06-13 14:15:30', code: 'CH4-YL-004', typeName: '地下空间气体监测点' },
+  { id: 'G5', name: '滨江东路河堤管廊', type: 'gasMonitor', category: 'gasNormal', level: 'normal', concentration: 2.5, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.4066, lat: 28.4625, address: '沅陵县太常片区滨江东路河堤管廊', alertTime: '2026-06-13 13:50:00', code: 'CH4-YL-005', typeName: '地下空间气体监测点' },
+  { id: 'G6', name: '迎宾东路地下通道', type: 'gasMonitor', category: 'gasNormal', level: 'normal', concentration: 3.2, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.4050, lat: 28.4640, address: '沅陵县太常片区迎宾东路地下人行通道', alertTime: '2026-06-13 13:30:00', code: 'CH4-YL-006', typeName: '地下空间气体监测点' },
+  { id: 'G7', name: '古城北路管沟', type: 'gasMonitor', category: 'gasNormal', level: 'normal', concentration: 1.8, threshold: 10, thresholdUnit: '%LEL',
+    lng: 110.3975, lat: 28.4785, address: '沅陵县太常片区古城北路综合管沟', alertTime: '2026-06-13 12:10:00', code: 'CH4-YL-007', typeName: '地下空间气体监测点' }
 ])
 
-// --- 管线压力监测点 ---
+// --- 管线压力报警点 ---
 const pressurePoints = ref([
-  { id: 'P1', name: '人民路压力监测点', type: 'pressureMonitor', category: 'pressureAlert', level: 'critical', pressure: 412, threshold: 400,
-    lng: 121.49, lat: 31.245, address: '人民路与世纪大道路口', alertTime: '2025-04-02 15:23:45', code: 'PR-001', typeName: '压力监测点' },
-  { id: 'P2', name: '东方路压力监测点', type: 'pressureMonitor', category: 'pressureAlert', level: 'warning', pressure: 365, threshold: 350,
-    lng: 121.53, lat: 31.248, address: '东方路蓝村路口', alertTime: '2025-04-02 15:18:22', code: 'PR-003', typeName: '压力监测点' }
+  { id: 'PR1', name: '辰州中路高压测点2#', type: 'pressureMonitor', category: 'pressureAlert', level: 'critical', pressure: 432, threshold: 420, thresholdUnit: 'kPa',
+    lng: 110.3935, lat: 28.4658, address: '沅陵县太常片区辰州中路与天宁路交叉口南侧', alertTime: '2026-06-13 15:31:45', code: 'PR-YL-002', typeName: '压力监测点' },
+  { id: 'PR2', name: '建设东路高压测点4#', type: 'pressureMonitor', category: 'pressureAlert', level: 'warning', pressure: 410, threshold: 420, thresholdUnit: 'kPa',
+    lng: 110.4050, lat: 28.4577, address: '沅陵县太常片区建设东路工业园入口处', alertTime: '2026-06-13 15:31:25', code: 'PR-YL-015', typeName: '压力监测点' },
+  { id: 'PR3', name: '古城中路中压测点2#', type: 'pressureMonitor', category: 'pressureAlert', level: 'warning', pressure: 142, threshold: 150, thresholdUnit: 'kPa',
+    lng: 110.3970, lat: 28.4669, address: '沅陵县太常片区古城中路太常安置区段', alertTime: '2026-06-13 15:29:58', code: 'PR-YL-005', typeName: '压力监测点' }
 ])
 
-// --- 管线流量监测点 ---
+// --- 管线流量报警点 ---
 const flowPoints = ref([
-  { id: 'F1', name: '世纪大道流量计', type: 'flowMonitor', category: 'flowAlert', level: 'warning', flow: 1250, threshold: 1200,
-    lng: 121.50, lat: 31.233, address: '世纪大道浦电路口', alertTime: '2025-04-02 15:22:00', code: 'FL-001', typeName: '流量监测点' }
+  { id: 'FL1', name: '辰州中路流量计2#', type: 'flowMonitor', category: 'flowAlert', level: 'critical', flow: 2620, threshold: 2500, thresholdUnit: 'Nm³/h',
+    lng: 110.3936, lat: 28.4690, address: '沅陵县太常片区辰州中路调压站进口侧', alertTime: '2026-06-13 15:31:45', code: 'FL-YL-002', typeName: '流量监测点' },
+  { id: 'FL2', name: '古城中路流量计2#', type: 'flowMonitor', category: 'flowAlert', level: 'warning', flow: 820, threshold: 800, thresholdUnit: 'Nm³/h',
+    lng: 110.3972, lat: 28.4710, address: '沅陵县太常片区古城中路太常安置区段', alertTime: '2026-06-13 15:29:58', code: 'FL-YL-003', typeName: '流量监测点' },
+  { id: 'FL3', name: '建设东路流量计3#', type: 'flowMonitor', category: 'flowAlert', level: 'warning', flow: 2480, threshold: 2500, thresholdUnit: 'Nm³/h',
+    lng: 110.4048, lat: 28.4660, address: '沅陵县太常片区建设东路工业园入口处', alertTime: '2026-06-13 14:55:20', code: 'FL-YL-015', typeName: '流量监测点' },
+  { id: 'FL4', name: '滨江东路流量计2#', type: 'flowMonitor', category: 'flowAlert', level: 'warning', flow: 210, threshold: 200, thresholdUnit: 'Nm³/h',
+    lng: 110.4063, lat: 28.4635, address: '沅陵县太常片区滨江东路商业街', alertTime: '2026-06-13 15:30:40', code: 'FL-YL-012', typeName: '流量监测点' }
 ])
 
-// --- 场站燃气监测点 ---
+// --- 场站燃气报警点 ---
 const stationPoints = ref([
-  { id: 'S1', name: '浦东天然气门站', type: 'stationMonitor', category: 'stationCritical', level: 'critical', concentration: 48, threshold: 20,
-    lng: 121.50, lat: 31.238, address: '浦东新区世纪大道888号', alertTime: '2025-04-02 15:23:45', code: 'PD-01', typeName: '燃气门站' },
-  { id: 'S2', name: '浦西调压站', type: 'stationMonitor', category: 'stationWarning', level: 'warning', concentration: 28, threshold: 20,
-    lng: 121.52, lat: 31.23, address: '徐汇区漕溪北路200号', alertTime: '2025-04-02 14:58:12', code: 'PX-02', typeName: '调压站' }
+  { id: 'S1', name: '太常LNG储配站', type: 'stationMonitor', category: 'stationCritical', level: 'critical', concentration: 48, threshold: 20, thresholdUnit: 'ppm',
+    lng: 110.4041, lat: 28.4753, address: '沅陵县太常片区辰州东路末段LNG储配站', alertTime: '2026-06-13 15:23:45', code: 'YL-LNG-01', typeName: '燃气场站' },
+  { id: 'S2', name: '工业园计量站', type: 'stationMonitor', category: 'stationCritical', level: 'critical', concentration: 35, threshold: 20, thresholdUnit: 'ppm',
+    lng: 110.4052, lat: 28.4580, address: '沅陵县太常片区建设东路工业园入口东侧', alertTime: '2026-06-13 14:32:05', code: 'YL-GY-01', typeName: '燃气场站' },
+  { id: 'S3', name: '太常调压站', type: 'stationMonitor', category: 'stationWarning', level: 'warning', concentration: 28, threshold: 20, thresholdUnit: 'ppm',
+    lng: 110.3965, lat: 28.4747, address: '沅陵县太常片区古城中路与天宁路交叉口东侧', alertTime: '2026-06-13 14:58:12', code: 'YL-TY-01', typeName: '燃气场站' },
+  { id: 'S4', name: '城南调压站', type: 'stationMonitor', category: 'stationWarning', level: 'warning', concentration: 52, threshold: 20, thresholdUnit: 'ppm',
+    lng: 110.3965, lat: 28.4585, address: '沅陵县太常片区建设路与古城南路交叉口南侧', alertTime: '2026-06-12 22:45:00', code: 'YL-CNTY-01', typeName: '燃气场站' }
 ])
 
-// 统计数据
-const totalPoints = computed(() => gasPoints.value.length + pressurePoints.value.length + flowPoints.value.length + stationPoints.value.length)
-const criticalCount = computed(() => {
-  return [...gasPoints.value, ...pressurePoints.value, ...stationPoints.value].filter(f => f.level === 'critical').length
-})
-const warningCount = computed(() => {
-  return [...gasPoints.value, ...pressurePoints.value, ...stationPoints.value].filter(f => f.level === 'warning').length
-})
+// ==================== 统计数据 ====================
+const allAlertFeatures = computed(() => [...gasPoints.value, ...pressurePoints.value, ...flowPoints.value, ...stationPoints.value])
+const totalPoints = computed(() => allAlertFeatures.value.length)
+const criticalCount = computed(() => allAlertFeatures.value.filter(f => f.level === 'critical').length)
+const warningCount = computed(() => allAlertFeatures.value.filter(f => f.level === 'warning').length)
 const gasAlertCount = computed(() => gasPoints.value.filter(f => f.level !== 'normal').length)
 const pressureAlertCount = computed(() => pressurePoints.value.length)
 const flowAlertCount = computed(() => flowPoints.value.length)
 const stationAlertCount = computed(() => stationPoints.value.length)
 
-// GIS画布相关
-const canvasRef = ref(null)
-const mapContainer = ref(null)
-let ctx = null
-let mapWidth = 0, mapHeight = 0
-let mapZoom = 1
-let offsetX = 0, offsetY = 0
-let dragStart = null
-let isDragging = false
-const bounds = { minLng: 121.45, maxLng: 121.56, minLat: 31.20, maxLat: 31.27 }
-
-// 交互状态
-const searchKeyword = ref('')
-const currentLng = ref(121.505)
-const currentLat = ref(31.235)
-const hoverFeature = ref(null)
-const hoverX = ref(0), hoverY = ref(0)
-const currentFeature = ref(null)
-const detailDrawerVisible = ref(false)
-
-function worldToCanvas(lng, lat) {
-  const px = (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * mapWidth
-  const py = (1 - (lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * mapHeight
-  const cx = mapWidth / 2, cy = mapHeight / 2
-  return { x: (px - cx) * mapZoom + cx + offsetX, y: (py - cy) * mapZoom + cy + offsetY }
+// ==================== 地图覆盖物渲染 ====================
+function clearOverlays() {
+  Object.values(overlayStore).forEach(arr => {
+    arr.forEach(o => { try { mapInstance.removeOverlay(o) } catch {} })
+    arr.length = 0
+  })
 }
 
-function drawBaseMap() {
-  if (!ctx) return
-  ctx.fillStyle = '#eaf5e9'
-  ctx.fillRect(0, 0, mapWidth, mapHeight)
-  ctx.strokeStyle = '#c8ddc4'
-  ctx.lineWidth = 1
-  for (let i = 0; i <= 10; i++) {
-    const x = (i / 10) * mapWidth
-    ctx.beginPath()
-    ctx.moveTo(x, 0); ctx.lineTo(x, mapHeight); ctx.stroke()
-    const y = (i / 10) * mapHeight
-    ctx.beginPath()
-    ctx.moveTo(0, y); ctx.lineTo(mapWidth, y); ctx.stroke()
-  }
-  ctx.fillStyle = '#a8d0a6'
-  ctx.font = '12px "Microsoft YaHei"'
-  ctx.fillText('世纪大道', 350, 120)
-  ctx.fillText('张杨路', 450, 280)
-  ctx.fillText('人民广场', 200, 300)
+function addToOverlayStore(layer, overlay) {
+  if (overlayStore[layer]) overlayStore[layer].push(overlay)
 }
 
-function drawPipelines() {
-  pipelines.value.forEach(pipeline => {
-    if (!visibleLayers.value.includes(pipeline.type)) return
-    const points = pipeline.coordinates.map(coord => worldToCanvas(coord[0], coord[1]))
-    if (points.length < 2) return
-    ctx.beginPath()
-    ctx.moveTo(points[0].x, points[0].y)
-    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y)
-    ctx.strokeStyle = pipeline.color
-    ctx.lineWidth = pipeline.lineWidth * mapZoom
-    ctx.stroke()
+function renderPipelines() {
+  const BMapGL = window.BMapGL
+  pipelines.forEach(p => {
+    const points = p.path.map(c => new BMapGL.Point(c[0], c[1]))
+    const polyline = new BMapGL.Polyline(points, {
+      strokeColor: p.color, strokeWeight: p.width, strokeOpacity: 0.75
+    })
+    mapInstance.addOverlay(polyline)
+    addToOverlayStore(p.type, polyline)
+    polyline.addEventListener('click', () => {
+      currentFeature.value = { name: p.name, code: p.id, typeName: '燃气管线', address: '沅陵县太常片区', level: 'normal' }
+      detailDrawerVisible.value = true
+    })
   })
 }
 
-function drawPoint(feature, color, size, shape = 'circle') {
-  if (!visibleLayers.value.includes(feature.category)) return
-  const { x, y } = worldToCanvas(feature.lng, feature.lat)
-  if (x < -30 || x > mapWidth + 30 || y < -30 || y > mapHeight + 30) return
-
-  ctx.beginPath()
-  if (shape === 'circle') ctx.arc(x, y, size, 0, 2 * Math.PI)
-  else if (shape === 'rect') ctx.fillRect(x - size/2, y - size/2, size, size)
-  else if (shape === 'triangle') {
-    ctx.moveTo(x, y - size)
-    ctx.lineTo(x + size, y + size/2)
-    ctx.lineTo(x - size, y + size/2)
-    ctx.closePath()
-  }
-  ctx.fillStyle = color
-  ctx.fill()
-  ctx.strokeStyle = '#fff'
-  ctx.lineWidth = 2
-  ctx.stroke()
-
-  // 严重报警添加闪烁外圈
-  if (feature.level === 'critical') {
-    ctx.beginPath()
-    ctx.arc(x, y, size + 5, 0, 2 * Math.PI)
-    ctx.fillStyle = 'rgba(245,108,108,0.3)'
-    ctx.fill()
-  }
+function renderPointMarkers(features, layer, shape, size, bgColor) {
+  const BMapGL = window.BMapGL
+  features.forEach(f => {
+    if (f.category !== layer) return
+    const pt = new BMapGL.Point(f.lng, f.lat)
+    const label = new BMapGL.Label(f.name, {
+      position: pt,
+      offset: new BMapGL.Size(0, -18)
+    })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: bgColor || (f.level === 'critical' ? '#f56c6c' : f.level === 'warning' ? '#e6a23c' : '#67c23a'),
+      borderRadius: '3px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToOverlayStore(layer, label)
+
+    // 点标记(圆点)
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToOverlayStore(layer, marker)
+
+    marker.addEventListener('click', () => {
+      currentFeature.value = f
+      detailDrawerVisible.value = true
+    })
+  })
 }
 
-function drawAllPoints() {
-}
+function renderAllMarkers() {
+  const BMapGL = window.BMapGL
+
+  // 地下空间气体
+  gasPoints.value.forEach(f => {
+    renderPointMarkers([f], f.category, 'circle', 10,
+      f.level === 'critical' ? '#f56c6c' : f.level === 'warning' ? '#e6a23c' : '#67c23a')
+  })
+
+  // 管线压力报警 - 菱形
+  pressurePoints.value.forEach(f => {
+    const pt = new BMapGL.Point(f.lng, f.lat)
+    const label = new BMapGL.Label(f.name, { position: pt, offset: new BMapGL.Size(0, -18) })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: f.level === 'critical' ? '#f56c6c' : '#e6a23c',
+      borderRadius: '2px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToOverlayStore(f.category, label)
+
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToOverlayStore(f.category, marker)
+    marker.addEventListener('click', () => {
+      currentFeature.value = f
+      detailDrawerVisible.value = true
+    })
+  })
 
-function renderMap() {
-  if (!canvasRef.value || !mapContainer.value) return
-  const canvas = canvasRef.value
-  mapWidth = mapContainer.value.clientWidth
-  mapHeight = mapContainer.value.clientHeight
-  canvas.width = mapWidth
-  canvas.height = mapHeight
-  ctx = canvas.getContext('2d')
-  drawBaseMap()
-  drawPipelines()
-  drawAllPoints()
+  // 管线流量报警 - 三角
+  flowPoints.value.forEach(f => {
+    const pt = new BMapGL.Point(f.lng, f.lat)
+    const label = new BMapGL.Label(f.name, { position: pt, offset: new BMapGL.Size(0, -18) })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: f.level === 'critical' ? '#f56c6c' : '#e6a23c',
+      borderRadius: '2px', border: 'none', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToOverlayStore(f.category, label)
+
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToOverlayStore(f.category, marker)
+    marker.addEventListener('click', () => {
+      currentFeature.value = f
+      detailDrawerVisible.value = true
+    })
+  })
+
+  // 场站燃气报警
+  stationPoints.value.forEach(f => {
+    const pt = new BMapGL.Point(f.lng, f.lat)
+    const label = new BMapGL.Label(f.name, { position: pt, offset: new BMapGL.Size(0, -18) })
+    label.setStyle({
+      color: '#fff', fontSize: '10px', padding: '3px 8px',
+      backgroundColor: f.level === 'critical' ? '#f56c6c' : '#e6a23c',
+      borderRadius: '10px', border: '2px solid #fff', whiteSpace: 'nowrap'
+    })
+    mapInstance.addOverlay(label)
+    addToOverlayStore(f.category, label)
+
+    const marker = new BMapGL.Marker(pt)
+    mapInstance.addOverlay(marker)
+    addToOverlayStore(f.category, marker)
+    marker.addEventListener('click', () => {
+      currentFeature.value = f
+      detailDrawerVisible.value = true
+    })
+  })
 }
 
-function zoomIn() { mapZoom = Math.min(mapZoom + 0.15, 2.5); renderMap() }
-function zoomOut() { mapZoom = Math.max(mapZoom - 0.15, 0.6); renderMap() }
-function fitBounds() { mapZoom = 1; offsetX = 0; offsetY = 0; renderMap() }
-function refreshData() { renderMap() }
-
-function updateCoordinates(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  const px = (mouseX - offsetX) / mapZoom
-  const py = (mouseY - offsetY) / mapZoom
-  const lng = bounds.minLng + (px / mapWidth) * (bounds.maxLng - bounds.minLng)
-  const lat = bounds.maxLat - (py / mapHeight) * (bounds.maxLat - bounds.minLat)
-  currentLng.value = lng
-  currentLat.value = lat
+function renderAllOverlays() {
+  clearOverlays()
+  renderPipelines()
+  renderAllMarkers()
+  onLayerChange()
 }
 
-function onMapClick(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-  const allFeatures = [...gasPoints.value, ...pressurePoints.value, ...flowPoints.value, ...stationPoints.value]
-  allFeatures.forEach(feature => {
-    const { x, y } = worldToCanvas(feature.lng, feature.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = feature }
+// ==================== 图层显隐 ====================
+function onLayerChange() {
+  const set = new Set(visibleLayers.value)
+  Object.entries(overlayStore).forEach(([layer, overlays]) => {
+    overlays.forEach(o => {
+      try {
+        if (set.has(layer)) o.show()
+        else o.hide()
+      } catch {}
+    })
   })
-  if (hit) { currentFeature.value = hit; detailDrawerVisible.value = true }
 }
 
-function onCanvasMouseMove(e) {
-  const rect = canvasRef.value.getBoundingClientRect()
-  const mouseX = (e.clientX - rect.left) * (mapWidth / rect.width)
-  const mouseY = (e.clientY - rect.top) * (mapHeight / rect.height)
-  let minDist = 20, hit = null
-  const allFeatures = [...gasPoints.value, ...pressurePoints.value, ...flowPoints.value, ...stationPoints.value]
-  allFeatures.forEach(feature => {
-    const { x, y } = worldToCanvas(feature.lng, feature.lat)
-    const dist = Math.hypot(mouseX - x, mouseY - y)
-    if (dist < minDist) { minDist = dist; hit = feature }
-  })
+// ==================== 地图操作 ====================
+function zoomIn() { mapInstance?.zoomIn() }
+function zoomOut() { mapInstance?.zoomOut() }
+function fitBounds() {
+  if (!mapInstance) return
+  const BMapGL = window.BMapGL
+  mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+}
+function refreshData() { ElMessage.success('数据已刷新') }
+
+// ==================== 搜索 ====================
+function onSearch() {
+  if (!mapInstance || !searchKeyword.value) return
+  const kw = searchKeyword.value.toLowerCase()
+  const hit = allAlertFeatures.value.find(f =>
+    f.name.toLowerCase().includes(kw) || f.address.toLowerCase().includes(kw))
   if (hit) {
-    hoverFeature.value = hit
-    hoverX.value = e.clientX + 12
-    hoverY.value = e.clientY - 40
-  } else { hoverFeature.value = null }
+    const BMapGL = window.BMapGL
+    mapInstance.centerAndZoom(new BMapGL.Point(hit.lng, hit.lat), 16)
+    currentFeature.value = hit
+  }
 }
 
-function initDragEvents() {
-  const canvas = canvasRef.value
-  if (!canvas) return
-  canvas.addEventListener('mousedown', (e) => {
-    dragStart = { x: e.clientX, y: e.clientY, offsetX: offsetX, offsetY: offsetY }
-    isDragging = true
-    canvas.style.cursor = 'grabbing'
-  })
-  window.addEventListener('mousemove', (e) => {
-    if (!isDragging) return
-    const dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y
-    offsetX = dragStart.offsetX + dx; offsetY = dragStart.offsetY + dy
-    renderMap()
-  })
-  window.addEventListener('mouseup', () => {
-    isDragging = false
-    if (canvasRef.value) canvasRef.value.style.cursor = 'crosshair'
-  })
-}
+// ==================== 详情抽屉 ====================
+const currentFeature = ref(null)
+const detailDrawerVisible = ref(false)
 
 const handleAlert = () => { ElMessage.info('开始处理报警') }
 const dispatchOrder = () => { ElMessage.success('已派发处置工单') }
 
-watch(() => visibleLayers.value, () => renderMap(), { deep: true })
+// ==================== 生命周期 ====================
+onMounted(async () => {
+  try {
+    await waitForBMapGL()
+    initMap()
+  } catch (e) {
+    mapError.value = '百度地图加载失败,请刷新页面重试'
+  }
+})
 
-onMounted(() => {
-  nextTick(() => { renderMap(); initDragEvents(); window.addEventListener('resize', renderMap) })
+onBeforeUnmount(() => {
+  try {
+    clearOverlays()
+    if (activeInfoWindow) { activeInfoWindow.close(); activeInfoWindow = null }
+    mapInstance?.destroy()
+    mapInstance = null
+  } catch {}
 })
-onUnmounted(() => window.removeEventListener('resize', renderMap))
 </script>
 
 <style scoped>
@@ -401,6 +469,7 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #e9ecef;
+  flex-shrink: 0;
 }
 .header-left { display: flex; align-items: center; gap: 20px; }
 .header-left .title { font-size: 1.3rem; font-weight: 600; }
@@ -429,6 +498,7 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   flex-direction: column;
   gap: 16px;
   overflow-y: auto;
+  flex-shrink: 0;
 }
 .layer-header { font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
 .layer-group { margin-bottom: 12px; }
@@ -440,9 +510,9 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
 .legend-dot.gas-critical { background: #f56c6c; }
 .legend-dot.gas-warning { background: #e6a23c; }
 .legend-dot.pressure { background: #409eff; border-radius: 2px; }
-.legend-dot.flow { background: #67c23a; clip-path: polygon(50% 0%, 0% 100%, 100% 100%); border-radius: 0; }
-.legend-dot.station-critical { background: #f56c6c; }
-.legend-dot.station-warning { background: #e6a23c; }
+.legend-dot.flow { background: #67c23a; clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
+.legend-dot.station-critical { background: #f56c6c; border-radius: 10px; }
+.legend-dot.station-warning { background: #e6a23c; border-radius: 10px; }
 .legend-line { display: inline-block; width: 30px; height: 3px; margin-right: 8px; }
 .legend-line.high { background: #f56c6c; }
 .legend-line.medium { background: #e6a23c; }
@@ -466,22 +536,13 @@ onUnmounted(() => window.removeEventListener('resize', renderMap))
   display: flex;
   justify-content: space-between;
   align-items: center;
+  flex-shrink: 0;
 }
 .coordinate-info { font-size: 12px; color: #909399; }
-.map-wrapper { flex: 1; position: relative; cursor: crosshair; }
-canvas { width: 100%; height: 100%; display: block; }
-.hover-card {
-  position: fixed;
-  background: rgba(0,0,0,0.8);
-  color: white;
-  padding: 8px 12px;
-  border-radius: 10px;
-  font-size: 12px;
-  pointer-events: none;
-  z-index: 200;
-}
-.hover-title { font-weight: 600; margin-bottom: 4px; }
+.map-wrapper { flex: 1; position: relative; overflow: hidden; }
+.map-div { width: 100%; height: 100%; }
+.map-error { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: #f56c6c; font-size: 16px; }
 .feature-detail { padding: 8px; }
 .critical-value { color: #f56c6c; font-weight: bold; }
 .action-section { margin-top: 16px; display: flex; gap: 12px; justify-content: flex-end; }
-</style>
+</style>

+ 78 - 50
src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue

@@ -130,9 +130,7 @@
         <!-- 浓度趋势图 -->
         <div class="trend-section">
           <div class="section-title">浓度变化趋势(最近30分钟)</div>
-          <div class="trend-chart">
-            <v-chart :option="trendChartOption" autoresize />
-          </div>
+          <div ref="chartRef" class="trend-chart"></div>
         </div>
 
         <!-- 辅助研判分析 -->
@@ -200,57 +198,64 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Search, Location, WarningFilled, InfoFilled, DataAnalysis, Clock, WindPower, Document } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import * as echarts from 'echarts'
 import { ElMessage, ElNotification } from 'element-plus'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟实时报警数据
+// 沅陵县相邻地下空间可燃气体实时报警数据
 const realtimeAlerts = ref([
-  {
-    id: 1, pointCode: 'MP-001', pointName: '人民广场地下管廊监测点', level: 'critical',
-    address: '人民大道200号地下综合管廊B2层', concentration: 15.8, threshold: 8,
-    alertTime: '2025-04-02 15:23:45', status: 'pending',
+  { id: 1, pointCode: 'CH4-YL-001', pointName: '辰州北路综合管廊监测点', level: 'critical',
+    address: '沅陵县太常片区辰州北路综合管廊B2层', concentration: 15.8, threshold: 10,
+    alertTime: '2026-06-13 15:23:45', status: 'pending',
     trendData: [2.1, 2.5, 3.2, 4.0, 5.1, 6.5, 8.0, 9.8, 11.2, 12.5, 13.8, 14.5, 15.2, 15.8, 15.8, 15.5, 15.0, 14.2, 13.5, 12.8, 12.0, 11.2, 10.5, 9.8],
-    historyRecords: []
-  },
-  {
-    id: 2, pointCode: 'MP-002', pointName: '世纪大道电力隧道监测点', level: 'critical',
-    address: '世纪大道福山路口地下隧道', concentration: 12.3, threshold: 8,
-    alertTime: '2025-04-02 14:58:12', status: 'pending',
+    historyRecords: [] },
+  { id: 2, pointCode: 'CH4-YL-002', pointName: '古城中路地下管沟监测点', level: 'critical',
+    address: '沅陵县太常片区古城中路电力管沟检查井', concentration: 12.3, threshold: 10,
+    alertTime: '2026-06-13 14:58:12', status: 'pending',
     trendData: [1.5, 2.2, 3.0, 4.1, 5.5, 7.0, 8.5, 9.2, 10.0, 10.8, 11.5, 12.0, 12.3, 12.3, 12.0, 11.5, 11.0, 10.2, 9.5, 8.8, 8.0, 7.2, 6.5, 5.8],
-    historyRecords: []
-  },
-  {
-    id: 3, pointCode: 'MP-003', pointName: '张杨路雨水管监测点', level: 'warning',
-    address: '张杨路松林路口雨水管网', concentration: 6.8, threshold: 8,
-    alertTime: '2025-04-02 14:32:05', status: 'pending',
+    historyRecords: [] },
+  { id: 3, pointCode: 'CH4-YL-003', pointName: '建设路雨水管廊监测点', level: 'warning',
+    address: '沅陵县太常片区建设路雨水管廊检查井', concentration: 6.8, threshold: 10,
+    alertTime: '2026-06-13 14:32:05', status: 'pending',
     trendData: [2.0, 2.5, 3.2, 4.0, 4.8, 5.5, 6.2, 6.8, 6.8, 6.5, 6.2, 5.8, 5.5, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2, 3.0, 2.8],
-    historyRecords: []
-  },
-  {
-    id: 4, pointCode: 'MP-004', pointName: '龙阳路污水管监测点', level: 'warning',
-    address: '龙阳路白杨路口污水管网', concentration: 5.2, threshold: 8,
-    alertTime: '2025-04-02 14:15:30', status: 'pending',
+    historyRecords: [] },
+  { id: 4, pointCode: 'CH4-YL-004', pointName: '天宁西路地下车库监测点', level: 'warning',
+    address: '沅陵县太常片区天宁西路商业地下停车场', concentration: 7.5, threshold: 10,
+    alertTime: '2026-06-13 14:15:30', status: 'pending',
+    trendData: [3.0, 3.5, 4.2, 5.0, 5.8, 6.5, 7.0, 7.5, 7.5, 7.2, 6.8, 6.5, 6.2, 6.0, 5.8, 5.5, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5],
+    historyRecords: [] },
+  { id: 5, pointCode: 'CH4-YL-005', pointName: '滨江东路河堤管廊监测点', level: 'critical',
+    address: '沅陵县太常片区滨江东路河堤综合管廊', concentration: 11.5, threshold: 10,
+    alertTime: '2026-06-13 13:50:00', status: 'pending',
+    trendData: [4.0, 4.5, 5.2, 6.0, 7.0, 8.0, 9.0, 9.8, 10.5, 11.0, 11.5, 11.5, 11.2, 10.8, 10.5, 10.0, 9.5, 9.0, 8.5, 8.0, 7.5, 7.0, 6.5, 6.0],
+    historyRecords: [] },
+  { id: 6, pointCode: 'CH4-YL-006', pointName: '迎宾东路地下通道监测点', level: 'warning',
+    address: '沅陵县太常片区迎宾东路地下人行通道', concentration: 5.2, threshold: 10,
+    alertTime: '2026-06-13 13:30:00', status: 'pending',
     trendData: [1.2, 1.8, 2.5, 3.2, 4.0, 4.5, 5.0, 5.2, 5.2, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2, 3.0, 2.8, 2.5, 2.2, 2.0, 1.8, 1.5],
-    historyRecords: []
-  }
+    historyRecords: [] },
+  { id: 7, pointCode: 'CH4-YL-007', pointName: '古城北路综合管沟监测点', level: 'warning',
+    address: '沅陵县太常片区古城北路综合管沟入口', concentration: 8.2, threshold: 10,
+    alertTime: '2026-06-13 12:10:00', status: 'pending',
+    trendData: [3.5, 4.0, 4.8, 5.5, 6.2, 7.0, 7.5, 8.0, 8.2, 8.2, 8.0, 7.5, 7.0, 6.5, 6.0, 5.5, 5.0, 4.8, 4.5, 4.2, 4.0, 3.8, 3.5, 3.2],
+    historyRecords: [] },
+  { id: 8, pointCode: 'CH4-YL-008', pointName: '建设东路工业园管廊监测点', level: 'critical',
+    address: '沅陵县太常片区建设东路工业园综合管廊', concentration: 14.2, threshold: 10,
+    alertTime: '2026-06-13 11:55:00', status: 'pending',
+    trendData: [5.0, 5.8, 6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.2, 13.8, 14.2, 14.2, 13.8, 13.2, 12.5, 11.8, 11.0, 10.2, 9.5, 8.8, 8.0, 7.2, 6.5],
+    historyRecords: [] }
 ])
 
 // 统计数据
-const totalPoints = computed(() => 24)
+const totalPoints = computed(() => 12)
 const warningCount = computed(() => realtimeAlerts.value.filter(a => a.level === 'warning').length)
-const todayNewCount = computed(() => 3)
+const todayNewCount = 2
 const maxConcentration = computed(() => Math.max(...realtimeAlerts.value.map(a => a.concentration)).toFixed(1))
 const maxConcentrationPoint = computed(() => realtimeAlerts.value.find(a => a.concentration === parseFloat(maxConcentration.value))?.pointName || '—')
 const avgConcentration = computed(() => (realtimeAlerts.value.reduce((sum, a) => sum + a.concentration, 0) / realtimeAlerts.value.length).toFixed(1))
-const avgTrend = computed(() => 0.8)
-const falseAlarmRate = computed(() => 5.2)
+const avgTrend = 0.6
+const falseAlarmRate = computed(() => 3.8)
 
 // 筛选
 const searchKeyword = ref('')
@@ -277,31 +282,43 @@ const paginatedAlerts = computed(() => {
   return filteredAlerts.value.slice(start, start + pageSize)
 })
 
-// 周边环境信息(模拟)
+// 周边环境信息
 const nearbyInfo = computed(() => {
   if (!selectedAlert.value) return ''
   const info = {
-    '人民广场地下管廊监测点': '周边2km内有地铁施工,近期有动土作业',
-    '世纪大道电力隧道监测点': '周边为商业区,燃气管道密集',
-    '张杨路雨水管监测点': '附近有餐饮集中区,排水系统可能存在油脂堆积'
+    '辰州北路综合管廊监测点': '周边为太常片区主干管廊,地下燃气管线交叉密集',
+    '古城中路地下管沟监测点': '邻近太常安置区,地下电力/通信管线并行铺设',
+    '建设路雨水管廊监测点': '附近县政府及商业区,排水管网收集面积大',
+    '天宁西路地下车库监测点': '商业地下停车场,通风井口邻近调压站出口',
+    '滨江东路河堤管廊监测点': '沅江沿岸河堤管廊,地下水位较高易致气体积聚',
+    '迎宾东路地下通道监测点': '城郊公交站地下人行通道,通风条件良好',
+    '古城北路综合管沟监测点': '古城北路住宅区管网交汇处,老旧管网翻新中',
+    '建设东路工业园管廊监测点': '工业园区蒸汽/燃气管网交错,存在热力管道干扰'
   }
   return info[selectedAlert.value.pointName] || '周边无异常施工活动'
 })
 
 const recentAlertCount = computed(() => {
   if (!selectedAlert.value) return 0
-  return Math.floor(Math.random() * 5) + 1
+  const counts = { 'CH4-YL-001': 5, 'CH4-YL-002': 3, 'CH4-YL-003': 2, 'CH4-YL-004': 4, 'CH4-YL-005': 6, 'CH4-YL-006': 1, 'CH4-YL-007': 3, 'CH4-YL-008': 4 }
+  return counts[selectedAlert.value.pointCode] || 2
 })
 
-// 趋势图配置
-const trendChartOption = computed(() => {
-  if (!selectedAlert.value) return {}
+// 趋势图
+const chartRef = ref(null)
+let chartInstance = null
+
+function renderChart() {
+  if (!chartRef.value || !selectedAlert.value?.trendData) return
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value)
+  }
   const times = Array.from({ length: 24 }, (_, i) => {
     const minutes = i * 5
     return `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}`
   })
   const threshold = selectedAlert.value.threshold
-  return {
+  chartInstance.setOption({
     tooltip: { trigger: 'axis' },
     xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
     yAxis: { type: 'value', name: '可燃气体浓度 (%LEL)' },
@@ -309,8 +326,10 @@ const trendChartOption = computed(() => {
       { name: '气体浓度', type: 'line', data: selectedAlert.value.trendData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.1 } },
       { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none' }
     ]
-  }
-})
+  })
+}
+
+watch(selectedAlert, () => { nextTick(() => renderChart()) }, { immediate: false })
 
 // 方法
 const selectAlert = (alert) => {
@@ -376,6 +395,15 @@ const refreshData = () => {
 const exportReport = () => {
   ElMessage.success('导出报警报表(演示)')
 }
+
+onMounted(() => {
+  nextTick(() => renderChart())
+})
+
+onBeforeUnmount(() => {
+  try { chartInstance?.dispose() } catch {}
+  chartInstance = null
+})
 </script>
 
 <style scoped>

+ 997 - 4
src/views/subSystem/gas/ghome/gHome.vue

@@ -1,14 +1,1007 @@
-<!--燃气管网安全运行监测系统-->
 <script setup>
+import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
 
+const nowText = ref('2025/12/31 12:59:59')
+const weekText = ref('星期三')
+const mapError = ref('')
+
+const baseCards = computed(() => [
+  { label: '监测点总数', value: 18, unit: '个' },
+  { label: '在线监测点', value: 16, unit: '个' },
+  { label: '离线监测点', value: 2, unit: '个' },
+  { label: '当日预警点', value: 3, unit: '个' }
+])
+
+const operationSummary = [
+  { label: '管网总长度', value: '128.6km' },
+  { label: '重点管段', value: '12段' },
+  { label: '调压站', value: '6座' }
+]
+
+const repairRecords = [
+  { month: '1月', value: 3 },
+  { month: '2月', value: 5 },
+  { month: '3月', value: 4 },
+  { month: '4月', value: 6 },
+  { month: '5月', value: 4 },
+  { month: '6月', value: 7 }
+]
+
+const pointRows = [
+  { id: 1, name: '辰州路阀井A-01', time: '12:59', value: '0.22MPa', status: '在线' },
+  { id: 2, name: '古城路调压箱B-02', time: '12:59', value: '0.24MPa', status: '在线' },
+  { id: 3, name: '迎宾路阀室C-03', time: '12:59', value: '0.18MPa', status: '预警' },
+  { id: 4, name: '建设路阀井D-04', time: '12:59', value: '0.21MPa', status: '在线' },
+  { id: 5, name: '滨江路管点E-05', time: '12:59', value: '0.23MPa', status: '在线' },
+  { id: 6, name: '天宁路调压站F-06', time: '12:59', value: '0.19MPa', status: '离线' },
+  { id: 7, name: '鸳鸯路阀井G-07', time: '12:59', value: '0.25MPa', status: '在线' },
+  { id: 8, name: '消防巷支线H-08', time: '12:59', value: '0.20MPa', status: '在线' }
+]
+
+const equipmentRows = [
+  { id: 1, name: '调压柜A', code: 'RQ-001', location: '辰州路', status: '正常' },
+  { id: 2, name: '阀门井B', code: 'RQ-002', location: '古城路', status: '正常' },
+  { id: 3, name: '流量计C', code: 'RQ-003', location: '迎宾路', status: '检修中' },
+  { id: 4, name: '压力表D', code: 'RQ-004', location: '建设路', status: '正常' },
+  { id: 5, name: '调压箱E', code: 'RQ-005', location: '滨江路', status: '正常' }
+]
+
+const warningRows = [
+  { level: '高', title: '压力波动偏高', code: '辰州路阀井A-01', time: '12:59:59', desc: '建议复核上游调压状态', tag: '压力异常' },
+  { level: '中', title: '疑似泄漏趋势', code: '迎宾路阀室C-03', time: '12:58:40', desc: '瞬时流量较近30分钟均值偏高', tag: '流量异常' },
+  { level: '高', title: '通信中断', code: '天宁路调压站F-06', time: '12:57:12', desc: '采集设备离线超过15分钟', tag: '设备离线' },
+  { level: '中', title: '设备开盖异常', code: '古城路调压箱B-02', time: '12:56:22', desc: '巡检确认现场状态', tag: '巡检关注' },
+  { level: '中', title: '阀门状态异常', code: '滨江路管点E-05', time: '12:54:16', desc: '阀门开度反馈与设定值存在偏差', tag: '阀门异常' },
+  { level: '高', title: '瞬时流量突增', code: '鸳鸯路阀井G-07', time: '12:52:48', desc: '短时流量连续3次超出阈值上限', tag: '流量异常' },
+  { level: '中', title: '电池电量偏低', code: '消防巷支线H-08', time: '12:49:05', desc: '建议尽快安排现场更换电池', tag: '设备维护' },
+  { level: '高', title: '站点离线恢复失败', code: '天宁路调压站F-06', time: '12:45:31', desc: '自动重连3次未成功,请人工排查', tag: '设备离线' }
+]
+
+const hazardSummary = [
+  { label: '一般隐患', value: 14, color: '#f3e64d' },
+  { label: '较大隐患', value: 5, color: '#ff9f1a' },
+  { label: '重大隐患', value: 2, color: '#ff3030' }
+]
+
+const mapKpis = [
+  { label: '在线率', value: '88.9%' },
+  { label: '压力均值', value: '0.21MPa' },
+  { label: '今日告警', value: '4条' }
+]
+
+const pipelineLines = [
+  {
+    name: '辰州路主干线',
+    color: '#39f5ff',
+    flow: '瞬时流量 0.52m³/min',
+    points: [
+      [110.3855, 28.4696],
+      [110.3873, 28.4668],
+      [110.3894, 28.4633],
+      [110.3922, 28.4598],
+      [110.3978, 28.4568],
+      [110.4058, 28.4558],
+      [110.4148, 28.4551]
+    ]
+  },
+  {
+    name: '古城路支线',
+    color: '#14d8ff',
+    flow: '瞬时流量 0.38m³/min',
+    points: [
+      [110.3892, 28.4728],
+      [110.3928, 28.4704],
+      [110.3964, 28.4683],
+      [110.4011, 28.4672],
+      [110.4072, 28.4669]
+    ]
+  }
+]
+
+const pipePoints = [
+  { name: 'A点', value: '0.22MPa', lng: 110.3872, lat: 28.4669 },
+  { name: 'B点', value: '0.24MPa', lng: 110.3894, lat: 28.4633 },
+  { name: 'C点', value: '0.18MPa', lng: 110.3922, lat: 28.4598 },
+  { name: 'D点', value: '0.23MPa', lng: 110.3978, lat: 28.4568 },
+  { name: 'E点', value: '0.25MPa', lng: 110.4058, lat: 28.4558 },
+  { name: 'F点', value: '0.19MPa', lng: 110.3892, lat: 28.4728 },
+  { name: 'G点', value: '0.21MPa', lng: 110.4011, lat: 28.4672 }
+]
+
+const mapWrapRef = ref(null)
+const repairChartRef = ref(null)
+const hazardChartRef = ref(null)
+let mapInstance = null
+let repairChart = null
+let hazardChart = null
+let clockTimer = null
+
+function formatWeek(day) {
+  return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
+}
+
+function pad(value) {
+  return String(value).padStart(2, '0')
+}
+
+function updateClock() {
+  const now = new Date()
+  nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+  weekText.value = formatWeek(now.getDay())
+}
+
+function initRepairChart() {
+  if (!repairChartRef.value) return
+  repairChart?.dispose()
+  repairChart = echarts.init(repairChartRef.value)
+  repairChart.setOption({
+    grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
+    xAxis: {
+      type: 'category',
+      data: repairRecords.map(item => item.month),
+      axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
+      axisLabel: { color: '#d2efff' }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
+      axisLine: { show: false },
+      axisLabel: { color: '#d2efff' }
+    },
+    tooltip: { trigger: 'axis' },
+    series: [{
+      type: 'bar',
+      barWidth: 24,
+      data: repairRecords.map(item => item.value),
+      itemStyle: {
+        borderRadius: [6, 6, 0, 0],
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#38d8ff' },
+          { offset: 1, color: '#166dff' }
+        ])
+      }
+    }]
+  })
+}
+
+function initHazardChart() {
+  if (!hazardChartRef.value) return
+  hazardChart?.dispose()
+  hazardChart = echarts.init(hazardChartRef.value)
+  hazardChart.setOption({
+    grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
+    xAxis: {
+      type: 'value',
+      splitLine: { show: false },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { show: false }
+    },
+    yAxis: {
+      type: 'category',
+      data: hazardSummary.map(item => item.label),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: '#d2efff', fontSize: 13 }
+    },
+    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+    series: [{
+      type: 'bar',
+      data: hazardSummary.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
+      barWidth: 14,
+      label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
+      showBackground: true,
+      backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
+    }]
+  })
+}
+
+function waitForBMapGL() {
+  return new Promise((resolve, reject) => {
+    if (window.BMapGL) {
+      resolve(window.BMapGL)
+      return
+    }
+    let attempts = 0
+    const timer = setInterval(() => {
+      attempts += 1
+      if (window.BMapGL) {
+        clearInterval(timer)
+        resolve(window.BMapGL)
+        return
+      }
+      if (attempts >= 100) {
+        clearInterval(timer)
+        reject(new Error('百度地图加载超时'))
+      }
+    }, 200)
+  })
+}
+
+function createFlowLabel(BMapGL, text) {
+  const label = new BMapGL.Label(text, { offset: new BMapGL.Size(-46, -18) })
+  label.setStyle({
+    color: '#fff36a',
+    background: 'rgba(255, 230, 0, 0.22)',
+    border: '1px solid rgba(255, 230, 0, 0.55)',
+    borderRadius: '8px',
+    padding: '5px 8px',
+    fontSize: '12px',
+    boxShadow: '0 0 12px rgba(255, 230, 0, 0.22)'
+  })
+  return label
+}
+
+function addPipeLine(BMapGL, pipe) {
+  const bmapPoints = pipe.points.map(([lng, lat]) => new BMapGL.Point(lng, lat))
+  const polyline = new BMapGL.Polyline(bmapPoints, {
+    strokeColor: pipe.color,
+    strokeWeight: 4,
+    strokeOpacity: 0.95
+  })
+  mapInstance.addOverlay(polyline)
+
+  bmapPoints.forEach((point, index) => {
+    const ring = new BMapGL.Circle(point, 14, {
+      strokeColor: '#39f5ff',
+      strokeWeight: 2,
+      strokeOpacity: 0.7,
+      fillColor: 'rgba(57,245,255,0.08)',
+      fillOpacity: 0.18
+    })
+    mapInstance.addOverlay(ring)
+    if (index < bmapPoints.length - 1 && index % 2 === 0) {
+      const label = createFlowLabel(BMapGL, pipe.flow)
+      const current = pipe.points[index]
+      const next = pipe.points[index + 1]
+      label.setPosition(new BMapGL.Point((current[0] + next[0]) / 2, (current[1] + next[1]) / 2))
+      mapInstance.addOverlay(label)
+    }
+  })
+}
+
+function addPipePoint(BMapGL, point) {
+  const markerPoint = new BMapGL.Point(point.lng, point.lat)
+  const marker = new BMapGL.Marker(markerPoint)
+  mapInstance.addOverlay(marker)
+  const label = new BMapGL.Label(`${point.name} ${point.value}`, { offset: new BMapGL.Size(-42, -40) })
+  label.setPosition(markerPoint)
+  label.setStyle({
+    color: '#d8faff',
+    background: 'rgba(18, 68, 112, 0.75)',
+    border: '1px solid rgba(57, 245, 255, 0.55)',
+    borderRadius: '8px',
+    padding: '5px 8px',
+    fontSize: '12px'
+  })
+  mapInstance.addOverlay(label)
+}
+
+async function initMap() {
+  if (!mapWrapRef.value) return
+  try {
+    const BMapGL = await waitForBMapGL()
+    mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
+    const center = new BMapGL.Point(110.3965, 28.4638)
+    mapInstance.centerAndZoom(center, 15)
+    mapInstance.enableScrollWheelZoom(true)
+    mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
+
+    pipelineLines.forEach(pipe => addPipeLine(BMapGL, pipe))
+    pipePoints.forEach(point => addPipePoint(BMapGL, point))
+  } catch (error) {
+    mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
+  }
+}
+
+function handleResize() {
+  repairChart?.resize()
+  hazardChart?.resize()
+}
+
+onMounted(async () => {
+  updateClock()
+  clockTimer = window.setInterval(updateClock, 1000)
+  await nextTick()
+  initRepairChart()
+  initHazardChart()
+  initMap()
+  window.addEventListener('resize', handleResize)
+})
+
+onBeforeUnmount(() => {
+  if (clockTimer) window.clearInterval(clockTimer)
+  window.removeEventListener('resize', handleResize)
+  repairChart?.dispose()
+  hazardChart?.dispose()
+  mapInstance = null
+})
 </script>
 
 <template>
-  <div class="app-container">
-    <h1>燃气管网安全运行监测系统</h1>
+  <div class="gas-home-screen">
+    <div class="screen-header">
+      <div class="header-time">{{ nowText }} {{ weekText }}</div>
+      <div class="header-title-wrap">
+        <div class="header-title">燃气管网安全运行监测系统</div>
+        <div class="header-subtitle">Gas Pipeline Operation Dashboard</div>
+      </div>
+      <div class="header-tags">
+        <span class="header-tag">实时监测</span>
+        <span class="header-tag">管网运行</span>
+        <span class="header-tag">隐患预警</span>
+      </div>
+    </div>
+
+    <div class="screen-body">
+      <div class="side-panel left-panel">
+        <section class="panel-card base-card">
+          <div class="panel-title">基础概况</div>
+          <div class="base-grid">
+            <div v-for="item in baseCards" :key="item.label" class="base-item">
+              <div class="base-icon"></div>
+              <div class="base-text">
+                <div class="base-label">{{ item.label }}</div>
+                <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
+              </div>
+            </div>
+          </div>
+        </section>
+
+        <section class="panel-card">
+          <div class="panel-title">维修趋势</div>
+          <div ref="repairChartRef" class="chart-box repair-chart"></div>
+        </section>
+
+        <section class="panel-card table-card">
+          <div class="panel-title">监测点数据</div>
+          <div class="table-wrap">
+            <table class="data-table">
+              <thead>
+                <tr>
+                  <th>序号</th>
+                  <th>监测点</th>
+                  <th>时间</th>
+                  <th>数值</th>
+                  <th>状态</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="row in pointRows" :key="row.id">
+                  <td>{{ row.id }}</td>
+                  <td>{{ row.name }}</td>
+                  <td>{{ row.time }}</td>
+                  <td>{{ row.value }}</td>
+                  <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+      </div>
+
+      <div class="center-panel">
+        <div class="map-stage">
+          <div class="map-stage-top">
+            <div class="summary-strip">
+              <div v-for="item in operationSummary" :key="item.label" class="summary-item">
+                <span class="summary-label">{{ item.label }}</span>
+                <strong class="summary-value">{{ item.value }}</strong>
+              </div>
+            </div>
+            <div class="map-kpis">
+              <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
+                <span>{{ item.label }}</span>
+                <strong>{{ item.value }}</strong>
+              </div>
+            </div>
+          </div>
+
+          <div ref="mapWrapRef" class="map-view"></div>
+          <div v-if="mapError" class="map-error">{{ mapError }}</div>
+
+          <div class="map-panel info-panel">
+            <div class="mini-title">当前管网信息</div>
+            <div class="mini-row"><span>主干线</span><strong>2条</strong></div>
+            <div class="mini-row"><span>支线</span><strong>4条</strong></div>
+            <div class="mini-row"><span>重点巡检</span><strong>3段</strong></div>
+          </div>
+
+          <div class="map-panel point-panel">
+            <div class="mini-title">当前管点信息</div>
+            <div class="mini-row"><span>在线采集</span><strong>16个</strong></div>
+            <div class="mini-row"><span>预警点位</span><strong>3个</strong></div>
+            <div class="mini-row"><span>离线点位</span><strong>2个</strong></div>
+          </div>
+        </div>
+      </div>
+
+      <div class="side-panel right-panel">
+        <section class="panel-card">
+          <div class="panel-title">设备运行</div>
+          <div class="table-wrap equipment-wrap">
+            <table class="data-table compact-table">
+              <thead>
+                <tr>
+                  <th>序号</th>
+                  <th>设备名称</th>
+                  <th>设备编号</th>
+                  <th>位置</th>
+                  <th>状态</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="row in equipmentRows" :key="row.id">
+                  <td>{{ row.id }}</td>
+                  <td>{{ row.name }}</td>
+                  <td>{{ row.code }}</td>
+                  <td>{{ row.location }}</td>
+                  <td>{{ row.status }}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="panel-card warning-card">
+          <div class="panel-title">告警动态</div>
+          <div class="warning-scroll">
+            <div class="warning-list">
+              <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
+                <div class="warning-side">
+                  <div class="warning-badge">{{ item.level }}</div>
+                  <div class="warning-tag">{{ item.tag }}</div>
+                </div>
+                <div class="warning-main">
+                  <div class="warning-title-row">
+                    <span class="warning-title">{{ item.title }}</span>
+                    <span class="warning-time">{{ item.time }}</span>
+                  </div>
+                  <div class="warning-code">{{ item.code }}</div>
+                  <div class="warning-desc">{{ item.desc }}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </section>
+
+        <section class="panel-card hazard-card">
+          <div class="panel-title">隐患统计</div>
+          <div class="hazard-summary three-col">
+            <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
+              <span class="hazard-dot" :style="{ background: item.color }"></span>
+              <span>{{ item.label }}</span>
+              <strong :style="{ color: item.color }">{{ item.value }}项</strong>
+            </div>
+          </div>
+          <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
+        </section>
+      </div>
+    </div>
   </div>
 </template>
 
 <style scoped lang="scss">
+.gas-home-screen {
+  min-height: 100vh;
+  color: #fff;
+  background:
+    linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
+    radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
+    #081425;
+}
+
+.screen-header {
+  height: 72px;
+  padding: 0 18px;
+  display: grid;
+  grid-template-columns: 320px 1fr 320px;
+  align-items: center;
+  border-bottom: 1px solid rgba(93, 194, 255, 0.28);
+  background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
+}
+
+.header-time {
+  font-size: 18px;
+  font-weight: 700;
+  letter-spacing: 1px;
+  color: #ddf6ff;
+}
+
+.header-title-wrap {
+  text-align: center;
+}
+
+.header-title {
+  font-size: 30px;
+  font-weight: 800;
+  letter-spacing: 2px;
+}
+
+.header-subtitle {
+  margin-top: 4px;
+  color: rgba(196, 234, 255, 0.78);
+  font-size: 12px;
+  letter-spacing: 1px;
+}
+
+.header-tags {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.header-tag {
+  padding: 7px 12px;
+  border: 1px solid rgba(117, 206, 255, 0.4);
+  border-radius: 16px;
+  color: #d8f5ff;
+  background: rgba(13, 77, 128, 0.45);
+  font-size: 12px;
+}
+
+.screen-body {
+  height: calc(100vh - 72px);
+  padding: 12px;
+  display: grid;
+  grid-template-columns: 360px 1fr 360px;
+  gap: 12px;
+}
+
+.side-panel,
+.center-panel {
+  min-height: 0;
+}
+
+.left-panel,
+.right-panel {
+  display: grid;
+  gap: 12px;
+}
+
+.left-panel {
+  grid-template-rows: 220px 220px 1fr;
+}
+
+.right-panel {
+  grid-template-rows: 250px 260px 1fr;
+}
+
+.panel-card {
+  min-height: 0;
+  padding: 12px;
+  background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
+  border: 1px solid rgba(65, 158, 219, 0.28);
+  border-radius: 10px;
+  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
+}
+
+.panel-title {
+  position: relative;
+  padding-left: 14px;
+  margin-bottom: 12px;
+  color: #8fe5ff;
+  font-size: 18px;
+  font-weight: 800;
+}
+
+.panel-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 4px;
+  width: 4px;
+  height: 18px;
+  border-radius: 2px;
+  background: linear-gradient(180deg, #28d7ff, #1776ff);
+}
+
+.base-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.base-card {
+  background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
+}
+
+.base-grid {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.base-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-height: 72px;
+  padding: 12px;
+  border-radius: 14px;
+  background: linear-gradient(180deg, rgba(20, 57, 89, 0.55), rgba(18, 44, 72, 0.45));
+  border: 1px solid rgba(97, 190, 255, 0.16);
+}
+
+.base-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 16px;
+  background: linear-gradient(180deg, rgba(108, 233, 255, 0.96), rgba(26, 111, 245, 0.96));
+  box-shadow: 0 0 18px rgba(30, 168, 255, 0.28);
+  flex: 0 0 48px;
+}
+
+.base-text {
+  min-width: 0;
+}
+
+.base-label {
+  color: #d6efff;
+  font-size: 13px;
+  letter-spacing: 0.5px;
+}
+
+.base-value {
+  margin-top: 4px;
+  color: #fff;
+  font-size: 30px;
+  font-weight: 800;
+  line-height: 1;
+}
+
+.base-value span {
+  margin-left: 4px;
+  color: #90d9ff;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.chart-box {
+  width: 100%;
+  height: calc(100% - 36px);
+}
+
+.table-card,
+.warning-card,
+.hazard-card {
+  overflow: hidden;
+}
+
+.table-wrap {
+  height: calc(100% - 38px);
+  overflow: auto;
+}
+
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  table-layout: fixed;
+}
+
+.data-table thead {
+  background: rgba(24, 122, 198, 0.55);
+}
+
+.data-table th,
+.data-table td {
+  padding: 10px 6px;
+  color: #f4fbff;
+  text-align: center;
+  font-size: 13px;
+  border-bottom: 1px solid rgba(130, 194, 231, 0.12);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.data-table tbody tr:nth-child(odd) {
+  background: rgba(133, 196, 242, 0.12);
+}
+
+.data-table tbody tr:nth-child(even) {
+  background: rgba(133, 196, 242, 0.06);
+}
+
+.compact-table th,
+.compact-table td {
+  padding: 9px 4px;
+}
+
+.status-cell.ok {
+  color: #6effc4;
+}
+
+.status-cell.warn {
+  color: #ffe16a;
+}
+
+.status-cell.off {
+  color: #ff8e8e;
+}
+
+.center-panel {
+  position: relative;
+}
+
+.map-stage {
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 12px;
+  border: 1px solid rgba(56, 159, 223, 0.2);
+  background: rgba(8, 20, 37, 0.64);
+}
+
+.map-stage-top {
+  position: absolute;
+  top: 14px;
+  left: 14px;
+  right: 14px;
+  z-index: 3;
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  pointer-events: none;
+}
+
+.summary-strip,
+.map-kpis {
+  display: flex;
+  gap: 10px;
+}
+
+.summary-item,
+.map-kpi {
+  min-width: 110px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background: rgba(8, 31, 54, 0.78);
+  border: 1px solid rgba(79, 177, 238, 0.22);
+}
+
+.summary-label,
+.map-kpi span {
+  display: block;
+  color: #91d7ff;
+  font-size: 12px;
+}
+
+.summary-value,
+.map-kpi strong {
+  display: block;
+  margin-top: 6px;
+  color: #fff;
+  font-size: 20px;
+}
+
+.map-view {
+  width: 100%;
+  height: 100%;
+}
+
+.map-error {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffd3d3;
+  background: rgba(5, 10, 20, 0.72);
+  z-index: 4;
+}
+
+.map-panel {
+  position: absolute;
+  z-index: 3;
+  width: 190px;
+  padding: 12px;
+  border-radius: 10px;
+  background: rgba(8, 31, 54, 0.78);
+  border: 1px solid rgba(79, 177, 238, 0.22);
+}
+
+.info-panel {
+  left: 18px;
+  bottom: 20px;
+}
+
+.point-panel {
+  right: 18px;
+  bottom: 20px;
+}
+
+.mini-title {
+  margin-bottom: 10px;
+  color: #8fe5ff;
+  font-size: 14px;
+  font-weight: 700;
+}
+
+.mini-row {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 8px;
+  color: #d6efff;
+  font-size: 13px;
+}
+
+.warning-scroll {
+  height: calc(100% - 38px);
+  overflow-y: auto;
+  padding-right: 4px;
+}
+
+.warning-scroll::-webkit-scrollbar {
+  width: 6px;
+}
+
+.warning-scroll::-webkit-scrollbar-thumb {
+  border-radius: 999px;
+  background: rgba(96, 180, 235, 0.38);
+}
+
+.warning-scroll::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.warning-list {
+  display: grid;
+  gap: 12px;
+}
+
+.warning-item {
+  display: flex;
+  gap: 12px;
+  align-items: stretch;
+  padding: 12px;
+  border-radius: 14px;
+  border: 1px solid rgba(255, 108, 108, 0.1);
+  background: linear-gradient(180deg, rgba(72, 24, 38, 0.48), rgba(49, 19, 31, 0.36));
+}
+
+.warning-item.notice {
+  border-color: rgba(255, 201, 92, 0.1);
+  background: linear-gradient(180deg, rgba(78, 63, 23, 0.34), rgba(48, 43, 20, 0.24));
+}
+
+.warning-side {
+  width: 56px;
+  flex: 0 0 56px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  align-items: center;
+}
+
+.warning-badge {
+  width: 32px;
+  height: 32px;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  font-weight: 800;
+  color: #fff;
+}
+
+.warning-tag {
+  width: 100%;
+  padding: 3px 0;
+  border-radius: 999px;
+  text-align: center;
+  font-size: 11px;
+  color: #dcefff;
+  background: rgba(255, 255, 255, 0.08);
+}
+
+.warning-item.danger .warning-badge {
+  background: rgba(255, 58, 58, 0.24);
+  color: #ff8787;
+}
+
+.warning-item.danger .warning-title {
+  color: #ffd8d8;
+}
+
+.warning-item.notice .warning-badge {
+  background: rgba(255, 198, 53, 0.18);
+  color: #ffd45d;
+}
+
+.warning-item.notice .warning-title {
+  color: #fff2c4;
+}
+
+.warning-main {
+  min-width: 0;
+  flex: 1;
+}
+
+.warning-title-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.warning-title {
+  font-weight: 700;
+  font-size: 16px;
+  line-height: 1.2;
+}
+
+.warning-time,
+.warning-code,
+.warning-desc {
+  color: #dcefff;
+  font-size: 12px;
+}
+
+.warning-time {
+  color: #b6dfff;
+  white-space: nowrap;
+}
+
+.warning-code {
+  margin: 6px 0 4px;
+  color: #9ad8ff;
+}
+
+.warning-desc {
+  line-height: 1.5;
+}
+
+.hazard-summary {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 8px;
+}
+
+.hazard-summary.three-col {
+  justify-content: space-between;
+}
+
+.hazard-summary-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 13px;
+}
+
+.hazard-dot {
+  width: 8px;
+  height: 24px;
+  display: inline-block;
+  border-radius: 4px;
+}
+
+.hazard-chart {
+  height: 160px;
+}
+
+.equipment-wrap {
+  height: calc(100% - 38px);
+}
+
+:deep(.BMap_cpyCtrl),
+:deep(.anchorBL) {
+  display: none !important;
+}
+
+@media (max-width: 1500px) {
+  .screen-body {
+    grid-template-columns: 320px 1fr 320px;
+  }
+
+  .header-title {
+    font-size: 24px;
+  }
 
-</style>
+  .summary-item,
+  .map-kpi {
+    min-width: 96px;
+  }
+}
+</style>

+ 4 - 4
src/views/subSystem/hidden/HazardList.vue

@@ -3,22 +3,22 @@
     <el-card class="search-card">
       <el-form :inline="true" :model="searchForm">
         <el-form-item label="隐患类型">
-          <el-select v-model="searchForm.hazardType" placeholder="请选择" clearable>
+          <el-select v-model="searchForm.hazardType" placeholder="请选择" clearable style="width:140px">
             <el-option v-for="d in hazardTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
           </el-select>
         </el-form-item>
         <el-form-item label="隐患等级">
-          <el-select v-model="searchForm.hazardLevel" placeholder="请选择" clearable>
+          <el-select v-model="searchForm.hazardLevel" placeholder="请选择" clearable style="width:140px">
             <el-option v-for="d in hazardLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
           </el-select>
         </el-form-item>
         <el-form-item label="状态">
-          <el-select v-model="searchForm.hazardStatus" placeholder="请选择" clearable>
+          <el-select v-model="searchForm.hazardStatus" placeholder="请选择" clearable style="width:140px">
             <el-option v-for="d in hazardStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
           </el-select>
         </el-form-item>
         <el-form-item label="报送状态">
-          <el-select v-model="searchForm.isSubmitted" placeholder="请选择" clearable>
+          <el-select v-model="searchForm.isSubmitted" placeholder="请选择" clearable style="width:140px">
             <el-option label="未报送" :value="0" />
             <el-option label="已报送" :value="1" />
           </el-select>

+ 533 - 53
src/views/subSystem/hidden/HazardMap.vue

@@ -1,15 +1,94 @@
 <template>
-  <div class="hazard-map-container">
-    <el-card class="legend-card">
+  <div class="hazard-map-page">
+    <!-- 顶部搜索栏 -->
+    <div class="top-bar">
+      <el-card shadow="never" class="search-card">
+        <el-form :inline="true">
+          <el-form-item label="隐患类型">
+            <el-select v-model="filterType" placeholder="全部" clearable style="width:140px" @change="applyFilter">
+              <el-option v-for="d in hazardTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="隐患等级">
+            <el-select v-model="filterLevel" placeholder="全部" clearable style="width:140px" @change="applyFilter">
+              <el-option v-for="d in hazardLevelDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="整改状态">
+            <el-select v-model="filterStatus" placeholder="全部" clearable style="width:140px" @change="onStatusFilterChange">
+              <el-option v-for="d in hazardStatusDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="applyFilter">查询</el-button>
+            <el-button @click="resetFilter">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </div>
+
+    <!-- 地图容器 -->
+    <div id="hazardBaiduMap" class="map-container"></div>
+    <div v-if="mapError" class="map-error">{{ mapError }}</div>
+
+    <!-- 左侧隐患列表 -->
+    <el-card class="left-list-card" shadow="hover">
+      <template #header>
+        <div class="left-header">
+          <span>隐患列表</span>
+          <span class="list-count">共 {{ filteredHazards.length }} 条</span>
+        </div>
+        <div class="status-tabs">
+          <span class="status-tab" :class="{ active: listStatusTab === '' }" @click="switchStatusTab('')">全部</span>
+          <span
+            v-for="d in hazardStatusDict"
+            :key="d.dictValue"
+            class="status-tab"
+            :class="{ active: listStatusTab === d.dictValue }"
+            :style="listStatusTab === d.dictValue ? { color: statusTabColor(d.dictValue), borderBottomColor: statusTabColor(d.dictValue) } : {}"
+            @click="switchStatusTab(d.dictValue)"
+          >{{ d.dictLabel }}</span>
+        </div>
+      </template>
+      <div class="list-body">
+        <div
+          v-for="h in filteredHazards"
+          :key="h.hazardId"
+          class="list-item"
+          :class="{ active: activeHazardId === h.hazardId }"
+          @click="focusHazard(h)"
+        >
+          <div class="item-top">
+            <span class="dot" :style="{ background: levelColors[h.hazardLevel] }"></span>
+            <span class="item-no">{{ h.hazardNo }}</span>
+            <el-tag :type="statusTag(h.hazardStatus)" size="small">{{ getDictLabel(hazardStatusDict, h.hazardStatus) }}</el-tag>
+          </div>
+          <div class="item-info">
+            <span>{{ getDictLabel(hazardTypeDict, h.hazardType) }}</span>
+            <span class="divider">|</span>
+            <span :style="{ color: levelColors[h.hazardLevel], fontWeight: '500' }">{{ getDictLabel(hazardLevelDict, h.hazardLevel) }}隐患</span>
+          </div>
+          <div class="item-addr">{{ h.address }}</div>
+          <div class="item-time">{{ h.reportTime }}</div>
+        </div>
+        <el-empty v-if="filteredHazards.length === 0" description="暂无匹配隐患" :image-size="60" />
+      </div>
+    </el-card>
+
+    <!-- 右侧图例 -->
+    <el-card class="legend-card" shadow="hover">
       <template #header><span>图例说明</span></template>
       <div class="legend-items">
         <div class="legend-item" v-for="item in levelLegend" :key="item.value">
-          <span class="dot" :style="{ background: item.color }"></span> {{ item.label }}隐患
+          <span class="legend-dot" :style="{ background: item.color }"></span>
+          <span>{{ item.label }}隐患</span>
+          <span class="legend-count">{{ item.count }}</span>
         </div>
       </div>
     </el-card>
-    <div id="hazardMap" class="map-container"></div>
-    <el-drawer v-model="detailVisible" title="隐患详情" size="400px">
+
+    <!-- 隐患详情抽屉 -->
+    <el-drawer v-model="detailVisible" title="隐患详情" size="420px">
       <template v-if="currentHazard">
         <el-descriptions :column="1" border size="small">
           <el-descriptions-item label="隐患编号">{{ currentHazard.hazardNo }}</el-descriptions-item>
@@ -17,10 +96,13 @@
           <el-descriptions-item label="隐患等级">
             <el-tag :type="levelTag(currentHazard.hazardLevel)">{{ getDictLabel(hazardLevelDict, currentHazard.hazardLevel) }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="状态">{{ getDictLabel(hazardStatusDict, currentHazard.hazardStatus) }}</el-descriptions-item>
+          <el-descriptions-item label="整改状态">{{ getDictLabel(hazardStatusDict, currentHazard.hazardStatus) }}</el-descriptions-item>
+          <el-descriptions-item label="上报单位">{{ currentHazard.reportUnit }}</el-descriptions-item>
+          <el-descriptions-item label="上报时间">{{ currentHazard.reportTime }}</el-descriptions-item>
           <el-descriptions-item label="隐患描述">{{ currentHazard.hazardDesc }}</el-descriptions-item>
+          <el-descriptions-item label="详细地址">{{ currentHazard.address }}</el-descriptions-item>
+          <el-descriptions-item label="整改方案">{{ currentHazard.rectifyPlan || '暂无' }}</el-descriptions-item>
           <el-descriptions-item label="整改反馈">{{ currentHazard.rectifyFeedback || '暂无' }}</el-descriptions-item>
-          <el-descriptions-item label="上报时间">{{ currentHazard.reportTime }}</el-descriptions-item>
         </el-descriptions>
       </template>
     </el-drawer>
@@ -28,82 +110,480 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, nextTick } from 'vue'
-import { getHazardMapPoints } from '@/api/pipeNetwork/hazard'
+import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
 import { getDicts } from '@/api/system/dict/data'
 
 const detailVisible = ref(false)
 const currentHazard = ref(null)
+const activeHazardId = ref('')
+const mapError = ref('')
 let mapInstance = null
+let activeInfoWindow = null
+let mapMarkers = []
 
 const hazardTypeDict = ref([])
 const hazardLevelDict = ref([])
 const hazardStatusDict = ref([])
 
+const filterType = ref('')
+const filterLevel = ref('')
+const filterStatus = ref('')
+const listStatusTab = ref('')
+
 function getDictLabel(dict, value) {
-  const item = dict.find(d => d.dictValue === value)
+  const item = dict.find(d => d.dictValue === String(value))
   return item ? item.dictLabel : value
 }
 
 const levelColors = { '0': '#f5222d', '1': '#fa8c16', '2': '#1890ff', '3': '#52c41a' }
+
 function levelTag(level) {
   const map = { '0': 'danger', '1': 'warning', '2': '', '3': 'success' }
-  return map[level] || ''
+  return map[String(level)] || ''
+}
+function statusTag(status) {
+  const map = { '1': 'danger', '2': 'warning', '3': 'success' }
+  return map[String(status)] || 'info'
+}
+function statusTabColor(status) {
+  const map = { '1': '#f56c6c', '2': '#e6a23c', '3': '#67c23a' }
+  return map[String(status)] || '#409eff'
 }
+function switchStatusTab(val) {
+  listStatusTab.value = val
+  filterStatus.value = val
+  applyFilter()
+}
+
+// ==================== 沅陵县静态隐患数据 ====================
+const mockHazards = [
+  { hazardId: 'HZ001', hazardNo: 'YL-2026-001', hazardType: '0', hazardLevel: '0', hazardStatus: '1', reportUnit: '太常片区管理站', reportTime: '2026-06-12 09:15', hazardDesc: '辰州北路太常段DN200高压管道被第三方道路施工挖掘机挖伤,管体出现明显划痕及凹坑,深度约3mm,存在泄漏风险', address: '沅陵县太常片区辰州北路与建设路交叉口北200米', lng: 110.3938, lat: 28.4685, rectifyPlan: '立即停气更换受损管段,长度约3米', rectifyFeedback: '' },
+  { hazardId: 'HZ002', hazardNo: 'YL-2026-002', hazardType: '1', hazardLevel: '1', hazardStatus: '1', reportUnit: '沅陵镇街道办', reportTime: '2026-06-11 14:30', hazardDesc: '古城南路太常安置区段中压管道外防腐层大面积脱落,管体局部锈蚀,最深处2.1mm', address: '沅陵县太常片区古城南路太常安置区东侧', lng: 110.3964, lat: 28.4605, rectifyPlan: '对锈蚀管段进行除锈处理,重新做3PE防腐层', rectifyFeedback: '材料已采购' },
+  { hazardId: 'HZ005', hazardNo: 'YL-2026-005', hazardType: '4', hazardLevel: '2', hazardStatus: '3', reportUnit: '燃气公司巡检队', reportTime: '2026-06-07 08:20', hazardDesc: '天宁东路末端DN80低压阀门操作卡涩,阀杆填料老化泄漏,开关力矩超出正常值3倍', address: '沅陵县太常片区天宁东路与滨江路交叉口', lng: 110.4065, lat: 28.4692, rectifyPlan: '更换阀门填料及O型密封圈,阀杆除锈润滑', rectifyFeedback: '已完成阀门维修,运行正常' },
+  { hazardId: 'HZ006', hazardNo: 'YL-2026-006', hazardType: '1', hazardLevel: '1', hazardStatus: '1', reportUnit: '太常片区管理站', reportTime: '2026-06-06 11:30', hazardDesc: '辰州中路太常段DN200高压管道阴极保护电位异常,实测电位-0.75V(标准≤-0.85V)', address: '沅陵县太常片区辰州中路与天宁路交叉口南100米', lng: 110.3935, lat: 28.4670, rectifyPlan: '增设牺牲阳极,调整恒电位仪输出', rectifyFeedback: '' },
+  { hazardId: 'HZ008', hazardNo: 'YL-2026-008', hazardType: '0', hazardLevel: '2', hazardStatus: '3', reportUnit: '燃气公司工程部', reportTime: '2026-05-25 09:45', hazardDesc: '太常安置区新建道路施工中挖掘机在未探明管线情况下作业,距DN90低压管道仅0.5米', address: '沅陵县太常片区太常安置区三期工地', lng: 110.3982, lat: 28.4628, rectifyPlan: '设置警示标识,施工期间安排专人监护', rectifyFeedback: '管线标识已补全' },
+  { hazardId: 'HZ009', hazardNo: 'YL-2026-009', hazardType: '4', hazardLevel: '2', hazardStatus: '2', reportUnit: '太常社区网格员', reportTime: '2026-05-20 13:10', hazardDesc: '迎宾东路太常段阀门井盖破损,井内积水严重,阀门操作机构锈蚀', address: '沅陵县太常片区迎宾东路城郊公交站旁', lng: 110.4072, lat: 28.4626, rectifyPlan: '更换阀门井盖,排空积水,井内做防水', rectifyFeedback: '' },
+  { hazardId: 'HZ010', hazardNo: 'YL-2026-010', hazardType: '3', hazardLevel: '3', hazardStatus: '3', reportUnit: '沅陵镇街道办', reportTime: '2026-05-18 10:30', hazardDesc: '建设西路沿线绿化树根系侵入管道保护范围,树木距DN63低压管道仅1.2米', address: '沅陵县太常片区建设西路人行道绿化带', lng: 110.3805, lat: 28.4584, rectifyPlan: '修剪侵入根系,设置根系隔离板', rectifyFeedback: '已修剪根系,隔离板安装完成' },
+  { hazardId: 'HZ011', hazardNo: 'YL-2026-011', hazardType: '0', hazardLevel: '0', hazardStatus: '1', reportUnit: '县应急管理局', reportTime: '2026-05-15 07:50', hazardDesc: '古城北路DN150中压管道被附近工地打桩机震动导致焊缝开裂,发生燃气泄漏', address: '沅陵县太常片区古城北路在建商业广场工地', lng: 110.3978, lat: 28.4765, rectifyPlan: '立即疏散周边人员,氮气置换后重新焊接', rectifyFeedback: '' },
+  { hazardId: 'HZ012', hazardNo: 'YL-2026-012', hazardType: '2', hazardLevel: '2', hazardStatus: '3', reportUnit: '燃气公司巡检队', reportTime: '2026-05-12 14:20', hazardDesc: '滨江东路太常段河堤路面出现轻微裂缝,DN90低压管道埋深仅0.4米(标准≥0.9米)', address: '沅陵县太常片区滨江东路河堤段', lng: 110.4035, lat: 28.4558, rectifyPlan: '增加覆土厚度至标准深度,加装管道保护盖板', rectifyFeedback: '已完成覆土回填,加装混凝土盖板' },
+  { hazardId: 'HZ014', hazardNo: 'YL-2026-014', hazardType: '3', hazardLevel: '0', hazardStatus: '1', reportUnit: '太常社区网格员', reportTime: '2026-05-08 16:40', hazardDesc: '辰州北路高压管道正上方有居民违规搭建临时早餐棚,占压长度约15米,且使用明火经营', address: '沅陵县太常片区辰州北路太常菜市场门口', lng: 110.3928, lat: 28.4725, rectifyPlan: '联合公安、城管部门取缔占压经营,拆除违章搭建', rectifyFeedback: '' },
+  { hazardId: 'HZ015', hazardNo: 'YL-2026-015', hazardType: '0', hazardLevel: '2', hazardStatus: '2', reportUnit: '县住建局', reportTime: '2026-05-05 09:00', hazardDesc: '迎宾路太常段电力电缆与DN110中压燃气管道同沟敷设,间距仅0.2米(标准≥0.5米)', address: '沅陵县太常片区迎宾中路综合管沟', lng: 110.3945, lat: 28.4640, rectifyPlan: '增设绝缘隔离板,加装绝缘接头及排流装置', rectifyFeedback: '' },
+  { hazardId: 'HZ016', hazardNo: 'YL-2026-016', hazardType: '1', hazardLevel: '1', hazardStatus: '1', reportUnit: '沅陵镇街道办', reportTime: '2026-05-03 10:30', hazardDesc: '古城南路DN110中压管道穿越排水涵洞段,涵洞内常年积水浸泡管道,防腐层起泡脱落', address: '沅陵县太常片区古城南路排水涵洞内', lng: 110.3960, lat: 28.4580, rectifyPlan: '导流涵洞积水,重新做防腐处理,加装防水套管', rectifyFeedback: '' },
+  { hazardId: 'HZ017', hazardNo: 'YL-2026-017', hazardType: '4', hazardLevel: '3', hazardStatus: '3', reportUnit: '燃气公司工程部', reportTime: '2026-04-28 13:45', hazardDesc: '太常安置区调压柜门锁损坏,柜体锈蚀,调压器出口压力波动超出正常范围', address: '沅陵县太常片区太常安置区一期院内', lng: 110.3970, lat: 28.4648, rectifyPlan: '更换调压器皮膜及弹簧,柜体除锈刷漆', rectifyFeedback: '已完成维修' },
+  { hazardId: 'HZ019', hazardNo: 'YL-2026-019', hazardType: '0', hazardLevel: '1', hazardStatus: '2', reportUnit: '县应急管理局', reportTime: '2026-04-22 11:00', hazardDesc: '辰州中路DN200高压管道标识桩缺失5根,标识带破损严重,管道走向标识不清', address: '沅陵县太常片区辰州中路(建设路至天宁路段)', lng: 110.3936, lat: 28.4660, rectifyPlan: '补设管道标识桩,更换标识带', rectifyFeedback: '' },
+  { hazardId: 'HZ020', hazardNo: 'YL-2026-020', hazardType: '3', hazardLevel: '3', hazardStatus: '3', reportUnit: '燃气公司巡检队', reportTime: '2026-04-20 15:30', hazardDesc: '滨江西路DN63低压管道被附近居民种菜挖土导致埋深不足,局部覆土仅0.2米', address: '沅陵县太常片区滨江西路空地菜园', lng: 110.3820, lat: 28.4568, rectifyPlan: '回填覆土至标准深度,设置警示标识', rectifyFeedback: '已回填完成' },
+  { hazardId: 'HZ022', hazardNo: 'YL-2026-022', hazardType: '4', hazardLevel: '2', hazardStatus: '2', reportUnit: '太常社区网格员', reportTime: '2026-04-15 14:00', hazardDesc: '辰州北路阀门井被附近施工渣土掩埋,无法正常开启巡检,阀门状态不明', address: '沅陵县太常片区辰州北路施工围挡内', lng: 110.3930, lat: 28.4750, rectifyPlan: '清理渣土,修复阀门井,检查阀门状态', rectifyFeedback: '' },
+  { hazardId: 'HZ023', hazardNo: 'YL-2026-023', hazardType: '2', hazardLevel: '3', hazardStatus: '3', reportUnit: '沅陵镇街道办', reportTime: '2026-04-10 10:00', hazardDesc: '迎宾东路绿化改造施工中挖掘机在DN160中压管道上方1米处作业,无保护措施', address: '沅陵县太常片区迎宾东路绿化带', lng: 110.4095, lat: 28.4625, rectifyPlan: '设置管道保护钢板,安排巡线员现场监护', rectifyFeedback: '施工安全完成' },
+  { hazardId: 'HZ024', hazardNo: 'YL-2026-024', hazardType: '0', hazardLevel: '0', hazardStatus: '1', reportUnit: '县住建局', reportTime: '2026-04-08 16:20', hazardDesc: '建设东路DN200高压管道上方被倾倒大量建筑垃圾,堆积高度约3米,可能导致管道受压变形', address: '沅陵县太常片区建设东路空地', lng: 110.4050, lat: 28.4577, rectifyPlan: '立即清除建筑垃圾,对管道进行变形检测', rectifyFeedback: '' },
+  { hazardId: 'HZ025', hazardNo: 'YL-2026-025', hazardType: '1', hazardLevel: '3', hazardStatus: '3', reportUnit: '燃气公司巡检队', reportTime: '2026-04-05 08:40', hazardDesc: '天宁东路末端调压箱过滤器压差异常升高,疑似过滤器堵塞', address: '沅陵县太常片区天宁东路末端', lng: 110.4088, lat: 28.4690, rectifyPlan: '清洗或更换过滤器滤芯', rectifyFeedback: '滤芯已更换,压差恢复正常' },
+]
+
+const filteredHazards = computed(() => {
+  let data = mockHazards
+  if (filterType.value) data = data.filter(h => h.hazardType === filterType.value)
+  if (filterLevel.value) data = data.filter(h => h.hazardLevel === filterLevel.value)
+  if (listStatusTab.value) data = data.filter(h => h.hazardStatus === listStatusTab.value)
+  return data
+})
 
 const levelLegend = computed(() =>
-  hazardLevelDict.value.map(d => ({ value: d.dictValue, label: d.dictLabel, color: levelColors[d.dictValue] || '#999' }))
+  hazardLevelDict.value.map(d => ({
+    value: d.dictValue,
+    label: d.dictLabel,
+    color: levelColors[d.dictValue] || '#999',
+    count: mockHazards.filter(h => h.hazardLevel === d.dictValue).length
+  }))
 )
 
-async function loadDicts() {
-  const [typeRes, levelRes, statusRes] = await Promise.all([
-    getDicts('hazard_type'),
-    getDicts('hazard_level'),
-    getDicts('hazard_status')
-  ])
-  hazardTypeDict.value = typeRes.data
-  hazardLevelDict.value = levelRes.data
-  hazardStatusDict.value = statusRes.data
+// ==================== 百度地图 ====================
+const MAP_CENTER = { lng: 110.394, lat: 28.464 }
+const MAP_DEFAULT_ZOOM = 14
+
+function waitForBMapGL() {
+  if (typeof BMapGL !== 'undefined') { initMap(); return }
+  let attempts = 0
+  const timer = setInterval(() => {
+    attempts++
+    if (typeof BMapGL !== 'undefined') { clearInterval(timer); initMap(); return }
+    if (attempts >= 50) { clearInterval(timer); mapError.value = '百度地图脚本加载超时,请刷新页面重试。' }
+  }, 200)
 }
 
-async function initMap() {
-  await nextTick()
-  const el = document.getElementById('hazardMap')
-  if (!el) return
+function closeInfoWindow() {
+  if (activeInfoWindow) {
+    try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}
+    activeInfoWindow = null
+  }
+}
+
+function initMap() {
+  const el = document.getElementById('hazardBaiduMap')
+  if (!el) { mapError.value = '地图容器未找到。'; return }
+
+  try {
+    mapInstance = new BMapGL.Map('hazardBaiduMap', { enableMapClick: true })
+  } catch (e) {
+    console.error('创建地图实例失败:', e)
+    mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
+    return
+  }
+
   try {
-    const L = await import('leaflet')
-    mapInstance = L.map('hazardMap').setView([34.77, 113.65], 12)
-    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-      attribution: '&copy; OpenStreetMap contributors'
-    }).addTo(mapInstance)
-
-    const res = await getHazardMapPoints()
-    const points = res.data || []
-    for (const p of points) {
-      if (!p.longitude || !p.latitude) continue
-      const color = levelColors[p.hazardLevel] || '#999'
-      const marker = L.circleMarker([p.latitude, p.longitude], {
-        radius: 10, fillColor: color, color: '#fff', weight: 2, fillOpacity: 0.8
-      }).addTo(mapInstance)
-      const typeLabel = getDictLabel(hazardTypeDict, p.hazardType)
-      const levelLabel = getDictLabel(hazardLevelDict, p.hazardLevel)
-      const statusLabel = getDictLabel(hazardStatusDict, p.hazardStatus)
-      marker.bindPopup(`<b>${p.hazardNo}</b><br/>${typeLabel} | ${levelLabel}<br/>${statusLabel}`)
-      marker.on('click', () => { currentHazard.value = p; detailVisible.value = true })
-    }
+    mapInstance.centerAndZoom(new BMapGL.Point(MAP_CENTER.lng, MAP_CENTER.lat), MAP_DEFAULT_ZOOM)
+    mapInstance.enableScrollWheelZoom(true)
+    mapInstance.setMinZoom(12)
+    mapInstance.setMaxZoom(18)
   } catch (e) {
-    el.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;">请安装Leaflet: npm install leaflet</div>'
+    console.error('设置地图中心失败:', e)
+  }
+
+  mapInstance.addEventListener('click', () => {
+    closeInfoWindow()
+    activeHazardId.value = ''
+  })
+
+  renderMapMarkers()
+}
+
+function renderMapMarkers() {
+  if (!mapInstance || typeof BMapGL === 'undefined') return
+  closeInfoWindow()
+  mapMarkers.forEach(m => { try { mapInstance.removeOverlay(m) } catch (_) {} })
+  mapMarkers = []
+
+  filteredHazards.value.forEach(h => {
+    const pt = new BMapGL.Point(h.lng, h.lat)
+    const color = levelColors[h.hazardLevel] || '#999'
+    const isActive = activeHazardId.value === h.hazardId
+    const size = isActive ? 16 : 12
+
+    const label = new BMapGL.Label('', {
+      position: pt,
+      offset: new BMapGL.Size(-size, -size)
+    })
+    label.setStyle({
+      color: '#fff',
+      fontSize: '10px',
+      fontWeight: 'bold',
+      padding: '0',
+      backgroundColor: color,
+      borderRadius: '50%',
+      border: isActive ? '3px solid #fff' : '2px solid #fff',
+      boxShadow: isActive ? '0 0 12px rgba(0,0,0,0.5)' : '0 2px 6px rgba(0,0,0,0.3)',
+      textAlign: 'center',
+      lineHeight: (size * 2) + 'px',
+      width: (size * 2) + 'px',
+      height: (size * 2) + 'px',
+      cursor: 'pointer'
+    })
+
+    label.addEventListener('click', (e) => {
+      if (e && e.domEvent) e.domEvent.stopPropagation()
+      openHazardInfo(h)
+    })
+
+    mapInstance.addOverlay(label)
+    mapMarkers.push({ overlay: label, hazard: h })
+  })
+}
+
+function openHazardInfo(h) {
+  closeInfoWindow()
+  currentHazard.value = h
+  activeHazardId.value = h.hazardId
+  detailVisible.value = true
+
+  const pt = new BMapGL.Point(h.lng, h.lat)
+  const color = levelColors[h.hazardLevel] || '#999'
+  const levelLabel = getDictLabel(hazardLevelDict.value, h.hazardLevel)
+  const typeLabel = getDictLabel(hazardTypeDict.value, h.hazardType)
+  const statusLabel = getDictLabel(hazardStatusDict.value, h.hazardStatus)
+
+  const html = `<div style="font-size:13px;max-width:260px;line-height:1.8;">
+    <b>${h.hazardNo}</b><br/>
+    隐患类型:${typeLabel}<br/>
+    隐患等级:<span style="color:${color};font-weight:bold;">${levelLabel}</span><br/>
+    整改状态:${statusLabel}<br/>
+    上报单位:${h.reportUnit}<br/>
+    地址:${h.address}
+  </div>`
+  activeInfoWindow = new BMapGL.InfoWindow(html, { width: 260, height: 150 })
+  mapInstance.openInfoWindow(activeInfoWindow, pt)
+  mapInstance.panTo(pt)
+
+  // 更新标记点样式
+  renderMapMarkers()
+}
+
+// ==================== 列表-地图交互 ====================
+function focusHazard(h) {
+  if (!mapInstance) return
+  openHazardInfo(h)
+
+  // 滚动列表项到可见区域
+  const el = document.querySelector(`.list-item.active`)
+  if (el) {
+    el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
   }
 }
 
-onMounted(async () => { await loadDicts(); initMap() })
+function onStatusFilterChange(val) {
+  listStatusTab.value = val || ''
+  applyFilter()
+}
+function applyFilter() {
+  activeHazardId.value = ''
+  renderMapMarkers()
+}
+function resetFilter() {
+  filterType.value = ''
+  filterLevel.value = ''
+  filterStatus.value = ''
+  listStatusTab.value = ''
+  applyFilter()
+}
+
+// ==================== 生命周期 ====================
+async function loadDicts() {
+  try {
+    const [typeRes, levelRes, statusRes] = await Promise.all([
+      getDicts('hazard_type'),
+      getDicts('hazard_level'),
+      getDicts('hazard_status')
+    ])
+    hazardTypeDict.value = typeRes.data || []
+    hazardLevelDict.value = levelRes.data || []
+    hazardStatusDict.value = statusRes.data || []
+  } catch (e) {
+    hazardTypeDict.value = [
+      { dictValue: '0', dictLabel: '第三方施工破坏' },
+      { dictValue: '1', dictLabel: '管道老化腐蚀' },
+      { dictValue: '2', dictLabel: '地面沉降' },
+      { dictValue: '3', dictLabel: '违章占压' },
+      { dictValue: '4', dictLabel: '设备故障' }
+    ]
+    hazardLevelDict.value = [
+      { dictValue: '0', dictLabel: '重大' },
+      { dictValue: '1', dictLabel: '较大' },
+      { dictValue: '2', dictLabel: '一般' },
+      { dictValue: '3', dictLabel: '低风险' }
+    ]
+    hazardStatusDict.value = [
+      { dictValue: '1', dictLabel: '待整改' },
+      { dictValue: '2', dictLabel: '整改中' },
+      { dictValue: '3', dictLabel: '已整改' }
+    ]
+  }
+}
+
+onMounted(async () => {
+  await loadDicts()
+  await nextTick()
+  waitForBMapGL()
+})
+
+onBeforeUnmount(() => {
+  if (mapInstance) {
+    closeInfoWindow()
+    try { mapInstance.destroy() } catch (_) {}
+    mapInstance = null
+  }
+})
 </script>
 
 <style scoped>
-.hazard-map-container { position: relative; height: calc(100vh - 100px); padding: 16px; }
-.legend-card { position: absolute; top: 32px; right: 32px; z-index: 1000; width: 160px; }
-.legend-items { display: flex; flex-direction: column; gap: 6px; }
-.legend-item { display: flex; align-items: center; gap: 6px; font-size: 13px; }
-.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
-.map-container { width: 100%; height: 100%; border-radius: 4px; }
+.hazard-map-page {
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 100px);
+  overflow: hidden;
+}
+
+/* 顶部搜索栏 */
+.top-bar {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  right: 16px;
+  z-index: 1000;
+}
+.search-card {
+  border-radius: 8px;
+}
+.search-card :deep(.el-card__body) {
+  padding: 12px 16px 0;
+}
+
+/* 地图 */
+.map-container {
+  width: 100%;
+  height: 100%;
+}
+.map-error {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #999;
+  font-size: 16px;
+  z-index: 1001;
+}
+
+/* 左侧隐患列表 */
+.left-list-card {
+  position: absolute;
+  left: 16px;
+  top: 110px;
+  width: 340px;
+  max-height: calc(100vh - 200px);
+  z-index: 1000;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+}
+.left-list-card :deep(.el-card__header) {
+  padding: 10px 14px;
+}
+.left-list-card :deep(.el-card__body) {
+  padding: 0;
+  flex: 1;
+  overflow: hidden;
+}
+.left-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-weight: 600;
+  font-size: 14px;
+}
+.list-count {
+  font-size: 12px;
+  color: #999;
+  font-weight: 400;
+}
+.status-tabs {
+  display: flex;
+  gap: 6px;
+  margin-top: 10px;
+  padding-top: 8px;
+  border-top: 1px solid #f0f0f0;
+}
+.status-tab {
+  font-size: 12px;
+  padding: 3px 10px;
+  cursor: pointer;
+  color: #909399;
+  border-radius: 3px;
+  border-bottom: 2px solid transparent;
+  transition: all 0.2s;
+}
+.status-tab:hover {
+  color: #409eff;
+}
+.status-tab.active {
+  color: #409eff;
+  font-weight: 500;
+  border-bottom-color: #409eff;
+}
+.list-body {
+  max-height: calc(100vh - 320px);
+  overflow-y: auto;
+  padding: 4px 0;
+}
+.list-item {
+  padding: 10px 14px;
+  cursor: pointer;
+  border-left: 3px solid transparent;
+  border-bottom: 1px solid #f0f0f0;
+  transition: all 0.2s;
+}
+.list-item:hover {
+  background: #f5f7fa;
+}
+.list-item.active {
+  background: #ecf5ff;
+  border-left-color: #409eff;
+}
+.item-top {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 4px;
+}
+.item-top .dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.item-no {
+  font-weight: 600;
+  font-size: 13px;
+  color: #303133;
+  flex: 1;
+}
+.item-info {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 4px;
+  padding-left: 18px;
+}
+.divider {
+  margin: 0 6px;
+  color: #ddd;
+}
+.item-addr {
+  font-size: 12px;
+  color: #909399;
+  padding-left: 18px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.item-time {
+  font-size: 11px;
+  color: #c0c4cc;
+  padding-left: 18px;
+  margin-top: 2px;
+}
+
+/* 右侧图例 */
+.legend-card {
+  position: absolute;
+  right: 16px;
+  top: 110px;
+  width: 160px;
+  z-index: 1000;
+  border-radius: 8px;
+}
+.legend-card :deep(.el-card__header) {
+  padding: 10px 14px;
+  font-weight: 600;
+  font-size: 14px;
+}
+.legend-card :deep(.el-card__body) {
+  padding: 10px 14px;
+}
+.legend-items {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 13px;
+}
+.legend-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.legend-count {
+  margin-left: auto;
+  color: #999;
+  font-size: 12px;
+}
 </style>

+ 299 - 53
src/views/subSystem/hidden/HazardStatistics.vue

@@ -1,16 +1,52 @@
 <template>
-  <div class="statistics-container">
-    <el-row :gutter="16">
+  <div class="statistics-page">
+    <!-- 顶部:时间筛选 -->
+    <el-card class="search-card">
+      <el-form :inline="true">
+        <el-form-item label="统计时间">
+          <el-button-group class="date-shortcuts">
+            <el-button :type="activeShortcut === 7 ? 'primary' : ''" @click="setDateRange(7)">最近7天</el-button>
+            <el-button :type="activeShortcut === 30 ? 'primary' : ''" @click="setDateRange(30)">最近30天</el-button>
+            <el-button :type="activeShortcut === 90 ? 'primary' : ''" @click="setDateRange(90)">最近90天</el-button>
+          </el-button-group>
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            @change="activeShortcut = 0"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 中部:隐患类型分布(多折线) + 隐患整改统计(分组柱状) -->
+    <el-row :gutter="16" class="chart-row">
       <el-col :span="12">
         <el-card class="chart-card">
-          <template #header><span>按隐患类型统计</span></template>
-          <div ref="chartTypeRef" class="chart"></div>
+          <template #header><span>隐患类型分布</span></template>
+          <div ref="chartTypeRef" class="chart" v-loading="typeLoading"></div>
         </el-card>
       </el-col>
       <el-col :span="12">
         <el-card class="chart-card">
-          <template #header><span>按整改状态统计</span></template>
-          <div ref="chartStatusRef" class="chart"></div>
+          <template #header><span>隐患整改统计</span></template>
+          <div ref="chartStatusRef" class="chart" v-loading="statusLoading"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 底部:各区域隐患统计 -->
+    <el-row :gutter="16" class="chart-row">
+      <el-col :span="24">
+        <el-card class="chart-card">
+          <template #header><span>各区域隐患统计</span></template>
+          <div ref="chartRegionRef" class="chart" v-loading="regionLoading"></div>
         </el-card>
       </el-col>
     </el-row>
@@ -18,78 +54,288 @@
 </template>
 
 <script setup>
-import { ref, onMounted, nextTick } from 'vue'
-import { getHazardStatisticsByType, getHazardStatisticsByStatus } from '@/api/pipeNetwork/hazard'
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import {
+  getHazardStatisticsTypeTimeline,
+  getHazardStatisticsStatusTimeline,
+  getHazardStatisticsByRegion
+} from '@/api/pipeNetwork/hazard'
 import { getDicts } from '@/api/system/dict/data'
 
 const chartTypeRef = ref(null)
 const chartStatusRef = ref(null)
+const chartRegionRef = ref(null)
+const typeLoading = ref(false)
+const statusLoading = ref(false)
+const regionLoading = ref(false)
 
 const hazardTypeDict = ref([])
 const hazardStatusDict = ref([])
+const dateRange = ref([])
+const activeShortcut = ref(7)
+
+function formatDateStr(d) {
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  return `${y}-${m}-${day}`
+}
+function daysAgo(n) {
+  const e = new Date()
+  const s = new Date()
+  s.setDate(s.getDate() - n)
+  return [formatDateStr(s), formatDateStr(e)]
+}
+function setDateRange(days) {
+  activeShortcut.value = days
+  dateRange.value = daysAgo(days)
+  loadAllCharts()
+}
+// 默认选中最近7天
+dateRange.value = daysAgo(7)
+
+const TYPE_COLORS = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4']
+const STATUS_BAR_COLORS = [
+  ['#fac858', '#d4a017'],  // 待整改 橙
+  ['#409eff', '#1a6dd4'],  // 整改中 蓝
+  ['#52c41a', '#2d8c0e'],  // 已整改 绿
+  ['#ee6666', '#c93737'],
+  ['#73c0de', '#4a9ec9'],
+  ['#9a60b4', '#7a3d9a']
+]
 
 function getDictLabel(dict, value) {
+  if (!dict || !dict.length) return value
   const item = dict.find(d => d.dictValue === value)
   return item ? item.dictLabel : value
 }
 
-async function loadDicts() {
-  const [typeRes, statusRes] = await Promise.all([
-    getDicts('hazard_type'),
-    getDicts('hazard_status')
-  ])
-  hazardTypeDict.value = typeRes.data
-  hazardStatusDict.value = statusRes.data
+function getDateParams() {
+  if (dateRange.value && dateRange.value.length === 2) {
+    return { startDate: dateRange.value[0], endDate: dateRange.value[1] }
+  }
+  return {}
 }
 
-async function initCharts() {
-  await nextTick()
+async function loadDicts() {
   try {
-    const echarts = (await import('echarts')).default
     const [typeRes, statusRes] = await Promise.all([
-      getHazardStatisticsByType(), getHazardStatisticsByStatus()
+      getDicts('hazard_type'),
+      getDicts('hazard_status')
     ])
+    hazardTypeDict.value = typeRes.data || []
+    hazardStatusDict.value = statusRes.data || []
+  } catch (e) {
+    console.error('加载字典失败', e)
+  }
+}
 
-    // 按类型统计饼图
-    const typeData = typeRes.data || []
-    if (chartTypeRef.value) {
-      const chart1 = echarts.init(chartTypeRef.value)
-      chart1.setOption({
-        tooltip: { trigger: 'item' },
-        series: [{
-          type: 'pie', radius: ['40%', '70%'],
-          data: typeData.map(d => ({ name: getDictLabel(hazardTypeDict, d.name), value: d.value })),
-          label: { formatter: '{b}: {c}' }
-        }]
-      })
-      window.addEventListener('resize', () => chart1.resize())
-    }
-
-    // 按状态统计柱状图
-    const statusData = statusRes.data || []
-    if (chartStatusRef.value) {
-      const chart2 = echarts.init(chartStatusRef.value)
-      chart2.setOption({
-        tooltip: { trigger: 'axis' },
-        xAxis: { type: 'category', data: statusData.map(d => getDictLabel(hazardStatusDict, d.name)) },
-        yAxis: { type: 'value' },
-        series: [{
-          type: 'bar', data: statusData.map(d => d.value),
-          itemStyle: { color: '#1890ff', borderRadius: [4, 4, 0, 0] }
-        }]
+// --- 图表管理 ---
+let chartInstances = []
+let resizeHandlers = []
+
+function registerChart(instance) {
+  chartInstances.push(instance)
+  const handler = () => instance.resize()
+  resizeHandlers.push(handler)
+  window.addEventListener('resize', handler)
+}
+
+function disposeCharts() {
+  chartInstances.forEach(c => { try { c.dispose() } catch { /* ignore */ } })
+  chartInstances = []
+  resizeHandlers.forEach(h => window.removeEventListener('resize', h))
+  resizeHandlers = []
+}
+
+async function initTypeChart() {
+  typeLoading.value = true
+  try {
+    await nextTick()
+    const echartsModule = await import('echarts')
+    const echarts = echartsModule.default || echartsModule
+    const res = await getHazardStatisticsTypeTimeline(getDateParams())
+    const payload = res.data || {}
+    const categories = payload.categories || []
+    const rawSeries = payload.series || []
+    if (!chartTypeRef.value) return
+
+    const c = echarts.init(chartTypeRef.value)
+    c.setOption({
+      tooltip: { trigger: 'axis' },
+      legend: {
+        data: rawSeries.map(s => getDictLabel(hazardTypeDict, s.name)),
+        bottom: 0,
+        type: 'scroll'
+      },
+      grid: { left: '3%', right: '6%', top: 20, bottom: 40, containLabel: true },
+      xAxis: {
+        type: 'category',
+        data: categories,
+        boundaryGap: false,
+        axisLine: { lineStyle: { color: '#ccc' } },
+        axisTick: { show: false }
+      },
+      yAxis: {
+        type: 'value',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { lineStyle: { color: '#f0f0f0' } }
+      },
+      series: rawSeries.map((s, i) => ({
+        type: 'line',
+        name: getDictLabel(hazardTypeDict, s.name),
+        data: s.data || [],
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 6,
+        lineStyle: { width: 2 },
+        itemStyle: { color: TYPE_COLORS[i % TYPE_COLORS.length] }
+      }))
+    })
+    registerChart(c)
+  } catch (e) {
+    console.error('隐患类型分布图表加载失败', e)
+  } finally {
+    typeLoading.value = false
+  }
+}
+
+async function initStatusChart() {
+  statusLoading.value = true
+  try {
+    await nextTick()
+    const echartsModule = await import('echarts')
+    const echarts = echartsModule.default || echartsModule
+    const res = await getHazardStatisticsStatusTimeline(getDateParams())
+    const payload = res.data || {}
+    const categories = payload.categories || []
+    const rawSeries = payload.series || []
+    if (!chartStatusRef.value) return
+
+    const c = echarts.init(chartStatusRef.value)
+    c.setOption({
+      tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+      legend: {
+        data: rawSeries.map(s => getDictLabel(hazardStatusDict, s.name)),
+        bottom: 0,
+        type: 'scroll'
+      },
+      grid: { left: '3%', right: '4%', top: 20, bottom: 40, containLabel: true },
+      xAxis: {
+        type: 'category',
+        data: categories,
+        axisLine: { lineStyle: { color: '#ccc' } },
+        axisTick: { show: false }
+      },
+      yAxis: {
+        type: 'value',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { lineStyle: { color: '#f0f0f0' } }
+      },
+      series: rawSeries.map((s, i) => {
+        const [c1, c2] = STATUS_BAR_COLORS[i % STATUS_BAR_COLORS.length]
+        return {
+          type: 'bar',
+          name: getDictLabel(hazardStatusDict, s.name),
+          data: s.data || [],
+          barWidth: 16,
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: c1 },
+              { offset: 1, color: c2 }
+            ]),
+            borderRadius: [4, 4, 0, 0]
+          }
+        }
       })
-      window.addEventListener('resize', () => chart2.resize())
-    }
+    })
+    registerChart(c)
+  } catch (e) {
+    console.error('隐患整改统计图表加载失败', e)
+  } finally {
+    statusLoading.value = false
+  }
+}
+
+async function initRegionChart() {
+  regionLoading.value = true
+  try {
+    await nextTick()
+    const echartsModule = await import('echarts')
+    const echarts = echartsModule.default || echartsModule
+    const res = await getHazardStatisticsByRegion(getDateParams())
+    const rawData = (res.data || []).sort((a, b) => b.value - a.value)
+    if (!chartRegionRef.value) return
+
+    const c = echarts.init(chartRegionRef.value)
+    c.setOption({
+      tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+      grid: { left: '3%', right: '4%', top: 20, bottom: 10, containLabel: true },
+      xAxis: {
+        type: 'category',
+        data: rawData.map(d => d.name),
+        axisLine: { lineStyle: { color: '#ccc' } },
+        axisTick: { show: false }
+      },
+      yAxis: {
+        type: 'value',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { lineStyle: { color: '#f0f0f0' } }
+      },
+      series: [{
+        type: 'bar',
+        data: rawData.map(d => ({
+          value: d.value,
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: '#409eff' }, { offset: 1, color: '#a0d2ff' }
+            ]),
+            borderRadius: [4, 4, 0, 0]
+          }
+        })),
+        barWidth: 32,
+        label: { show: true, position: 'top', color: '#333' }
+      }]
+    })
+    registerChart(c)
   } catch (e) {
-    console.error('图表加载失败', e)
+    console.error('各区域隐患统计图表加载失败', e)
+  } finally {
+    regionLoading.value = false
   }
 }
 
-onMounted(async () => { await loadDicts(); initCharts() })
+function loadAllCharts() {
+  disposeCharts()
+  initTypeChart()
+  initStatusChart()
+  initRegionChart()
+}
+
+function handleSearch() {
+  loadAllCharts()
+}
+
+onMounted(async () => {
+  await loadDicts()
+  loadAllCharts()
+})
+
+onBeforeUnmount(() => {
+  disposeCharts()
+})
 </script>
 
 <style scoped>
-.statistics-container { padding: 16px; }
-.chart-card { margin-bottom: 16px; }
-.chart { width: 100%; height: 400px; }
+.statistics-page { padding: 16px; min-height: 100%; background: #f0f2f5; }
+
+.search-card { margin-bottom: 16px; }
+.date-shortcuts { margin-right: 12px; }
+
+.chart-row { margin-bottom: 16px; }
+.chart-card { border-radius: 8px; }
+.chart { width: 100%; height: 380px; }
 </style>

+ 0 - 151
src/views/subSystem/monitor/Dashboard.vue

@@ -1,151 +0,0 @@
-<template>
-  <div class="dashboard-container">
-    <el-row :gutter="16" class="stat-row">
-      <el-col :span="6">
-        <el-card class="stat-card">
-          <div class="stat-value" style="color:#1890ff">{{ pressureValue }}</div>
-          <div class="stat-label">实时压力 (MPa)</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card class="stat-card">
-          <div class="stat-value" style="color:#52c41a">{{ flowValue }}</div>
-          <div class="stat-label">实时流量 (m³/h)</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card class="stat-card">
-          <div class="stat-value" style="color:#faad14">{{ gasValue }}</div>
-          <div class="stat-label">气体浓度 (%)</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card class="stat-card">
-          <div class="stat-value" style="color:#f5222d">{{ alarmCount }}</div>
-          <div class="stat-label">今日报警次数</div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="16" style="margin-top: 16px">
-      <el-col :span="12">
-        <el-card>
-          <template #header><span>压力历史曲线</span></template>
-          <div ref="pressureChart" class="chart"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card>
-          <template #header><span>流量历史曲线</span></template>
-          <div ref="flowChart" class="chart"></div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="16" style="margin-top: 16px">
-      <el-col :span="12">
-        <el-card>
-          <template #header><span>视频监控列表</span></template>
-          <el-table :data="videoList" border size="small">
-            <el-table-column prop="deviceName" label="摄像头名称" />
-            <el-table-column prop="deviceCode" label="设备编码" />
-            <el-table-column prop="location" label="安装位置" />
-            <el-table-column prop="status" label="状态" width="80">
-              <template #default="{ row }">
-                <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'" size="small">{{ row.status === 'ONLINE' ? '在线' : '离线' }}</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card>
-          <template #header><span>最新气体浓度报警</span></template>
-          <el-table :data="gasAlarms" border size="small" empty-text="暂无报警数据">
-            <el-table-column prop="deviceCode" label="设备编码" />
-            <el-table-column prop="actualValue" label="实际值" width="90" />
-            <el-table-column prop="alarmTime" label="报警时间" width="160" />
-          </el-table>
-        </el-card>
-      </el-col>
-    </el-row>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, onUnmounted, nextTick } from 'vue'
-import { getPressureHistory, getFlowHistory, getGasAlarmLatest, getVideoList } from '@/api/pipeNetwork/hazard'
-
-const pressureValue = ref('0.35')
-const flowValue = ref('78.5')
-const gasValue = ref('0.12')
-const alarmCount = ref(3)
-const videoList = ref([])
-const gasAlarms = ref([])
-const pressureChart = ref(null)
-const flowChart = ref(null)
-let timer = null
-
-async function loadData() {
-  try {
-    const [pRes, fRes, gRes, vRes] = await Promise.all([
-      getPressureHistory(null, 24), getFlowHistory(null, 24), getGasAlarmLatest(), getVideoList()
-    ])
-    videoList.value = vRes.data || []
-    gasAlarms.value = gRes.data || []
-    renderPressureChart(pRes.data || [])
-    renderFlowChart(fRes.data || [])
-  } catch (e) { console.error(e) }
-}
-
-async function renderPressureChart(data) {
-  await nextTick()
-  if (!pressureChart.value) return
-  try {
-    const echarts = (await import('echarts')).default
-    const chart = echarts.init(pressureChart.value)
-    chart.setOption({
-      tooltip: { trigger: 'axis' },
-      xAxis: { type: 'time', name: '时间' },
-      yAxis: { type: 'value', name: 'MPa' },
-      series: [{ type: 'line', smooth: true, data: data.map(d => [d.time, d.value]), areaStyle: {} }]
-    })
-    window.addEventListener('resize', () => chart.resize())
-  } catch (e) { console.error(e) }
-}
-
-async function renderFlowChart(data) {
-  await nextTick()
-  if (!flowChart.value) return
-  try {
-    const echarts = (await import('echarts')).default
-    const chart = echarts.init(flowChart.value)
-    chart.setOption({
-      tooltip: { trigger: 'axis' },
-      xAxis: { type: 'time', name: '时间' },
-      yAxis: { type: 'value', name: 'm³/h' },
-      series: [{ type: 'line', smooth: true, data: data.map(d => [d.time, d.value]), areaStyle: {} }]
-    })
-    window.addEventListener('resize', () => chart.resize())
-  } catch (e) { console.error(e) }
-}
-
-onMounted(() => {
-  loadData()
-  timer = setInterval(() => {
-    pressureValue.value = (0.3 + Math.random() * 0.2).toFixed(2)
-    flowValue.value = (60 + Math.random() * 50).toFixed(1)
-    gasValue.value = (Math.random() * 0.2).toFixed(2)
-  }, 5000)
-})
-
-onUnmounted(() => { clearInterval(timer) })
-</script>
-
-<style scoped>
-.dashboard-container { padding: 16px; }
-.stat-card { text-align: center; }
-.stat-value { font-size: 32px; font-weight: bold; }
-.stat-label { font-size: 14px; color: #999; margin-top: 8px; }
-.chart { width: 100%; height: 320px; }
-</style>

+ 0 - 40
src/views/subSystem/monitor/MonitorMap.vue

@@ -1,40 +0,0 @@
-<template>
-  <div class="monitor-map-container">
-    <div id="monitorMap" class="map"></div>
-    <el-drawer v-model="chartVisible" title="实时数据曲线" size="500px">
-      <div ref="detailChart" class="detail-chart"></div>
-    </el-drawer>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, nextTick } from 'vue'
-import { getPointsWithLatestData } from '@/api/pipeNetwork/hazard'
-
-const chartVisible = ref(false)
-const detailChart = ref(null)
-let mapInstance = null
-
-async function initMap() {
-  await nextTick()
-  const el = document.getElementById('monitorMap')
-  if (!el) return
-  try {
-    const L = await import('leaflet')
-    mapInstance = L.map('monitorMap').setView([34.77, 113.65], 13)
-    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-      attribution: '&copy; OpenStreetMap contributors'
-    }).addTo(mapInstance)
-  } catch (e) {
-    el.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;">请安装Leaflet: npm install leaflet</div>'
-  }
-}
-
-onMounted(() => { initMap() })
-</script>
-
-<style scoped>
-.monitor-map-container { height: calc(100vh - 100px); padding: 16px; }
-.map { width: 100%; height: 100%; border-radius: 4px; }
-.detail-chart { width: 100%; height: 400px; }
-</style>

+ 1 - 1
vite.config.js

@@ -31,7 +31,7 @@ export default defineConfig(({ mode, command }) => {
       proxy: {
         '/dev-api': {
           // target: 'http://192.168.110.235:8300/pipe',
-          target: 'http://111.23.174.45:8301',
+          target: 'http://localhost:8301',
           changeOrigin: true,
           rewrite: (p) => p.replace(/^\/dev-api/, '')
         }