|
|
@@ -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>
|