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