|
|
@@ -0,0 +1,1326 @@
|
|
|
+<template>
|
|
|
+ <div class="gis-dashboard">
|
|
|
+ <div class="dashboard-header">
|
|
|
+ <div class="header-title">
|
|
|
+ <div class="title-main">
|
|
|
+ <el-icon><MapLocation /></el-icon>
|
|
|
+ <span>燃气管网GIS一张图</span>
|
|
|
+ </div>
|
|
|
+ <div class="title-metrics">
|
|
|
+ <span class="metric metric-danger">管线 {{ summaryStats.pipelineLength }}km</span>
|
|
|
+ <span class="metric metric-primary">燃气管点 {{ summaryStats.pipePointCount }}个</span>
|
|
|
+ <span class="metric metric-warning">窨井 {{ summaryStats.manholeCount }}个</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ class="search-input"
|
|
|
+ placeholder="搜索管点/井盖编号/位置"
|
|
|
+ clearable
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <el-button type="primary" @click="handleSearch">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="refreshScene">
|
|
|
+ <el-icon><RefreshRight /></el-icon>
|
|
|
+ 刷新
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="toggleFullscreen">
|
|
|
+ <el-icon><FullScreen /></el-icon>
|
|
|
+ 全图
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div ref="screenRef" class="dashboard-content">
|
|
|
+ <aside class="dashboard-sidebar">
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <span>图层控制</span>
|
|
|
+ </template>
|
|
|
+ <div class="layer-group">
|
|
|
+ <div class="layer-title">燃气管线</div>
|
|
|
+ <el-checkbox v-model="layerState.pipelines.high">高压管线</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipelines.subHigh">次高压管线</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipelines.medium">中压管线</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipelines.low">低压管线</el-checkbox>
|
|
|
+ </div>
|
|
|
+ <div class="layer-group">
|
|
|
+ <div class="layer-title">燃气管点</div>
|
|
|
+ <el-checkbox v-model="layerState.pipePoints.valve">阀门</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipePoints.regulator">调压器</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipePoints.flowmeter">流量计</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.pipePoints.terminal">终端节点</el-checkbox>
|
|
|
+ </div>
|
|
|
+ <div class="layer-group layer-group-last">
|
|
|
+ <div class="layer-title">窨井设施</div>
|
|
|
+ <el-checkbox v-model="layerState.manholes.cover">窨井盖</el-checkbox>
|
|
|
+ <el-checkbox v-model="layerState.manholes.inspection">检查井</el-checkbox>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <span>图例说明</span>
|
|
|
+ </template>
|
|
|
+ <div class="legend-list">
|
|
|
+ <div class="legend-item"><span class="legend-line legend-high"></span><span>高压管线</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-line legend-sub-high"></span><span>次高压管线</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-line legend-medium"></span><span>中压管线</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-line legend-low"></span><span>低压管线</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-dot legend-valve"></span><span>阀门</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-dot legend-regulator"></span><span>调压器</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-dot legend-flowmeter"></span><span>流量计</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-dot legend-terminal"></span><span>终端节点</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-square legend-cover"></span><span>窨井盖</span></div>
|
|
|
+ <div class="legend-item"><span class="legend-square legend-inspection"></span><span>检查井</span></div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="panel-card stats-card">
|
|
|
+ <template #header>
|
|
|
+ <span>测绘数据统计</span>
|
|
|
+ </template>
|
|
|
+ <div class="stats-list">
|
|
|
+ <div class="stats-row"><span>管线总长</span><strong>{{ summaryStats.pipelineLength }} km</strong></div>
|
|
|
+ <div class="stats-row"><span>管点总数</span><strong>{{ summaryStats.pipePointCount }}</strong></div>
|
|
|
+ <div class="stats-row"><span>窨井总数</span><strong>{{ summaryStats.manholeCount }}</strong></div>
|
|
|
+ <div class="stats-row"><span>覆盖面积</span><strong>{{ summaryStats.coverageArea }} km²</strong></div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="panel-card detail-card">
|
|
|
+ <template #header>
|
|
|
+ <span>要素详情</span>
|
|
|
+ </template>
|
|
|
+ <div v-if="selectedFeature" class="detail-body">
|
|
|
+ <div class="detail-name">{{ selectedFeature.name }}</div>
|
|
|
+ <div class="detail-tag">{{ selectedFeature.category }}</div>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item
|
|
|
+ v-for="item in selectedFeatureDetails"
|
|
|
+ :key="item.label"
|
|
|
+ :label="item.label"
|
|
|
+ >
|
|
|
+ {{ item.value }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ <el-empty v-else description="点击地图要素查看详情" :image-size="72" />
|
|
|
+ </el-card>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <section class="dashboard-map-panel">
|
|
|
+ <div class="map-toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-button @click="zoomIn">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ 放大
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="zoomOut">
|
|
|
+ <el-icon><Minus /></el-icon>
|
|
|
+ 缩小
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="locateCenter">
|
|
|
+ <el-icon><Location /></el-icon>
|
|
|
+ 定位
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar-right">当前坐标: {{ currentCoordinate }}</div>
|
|
|
+ </div>
|
|
|
+ <div id="gisMap" ref="mapRef" class="gis-map"></div>
|
|
|
+ <div v-if="mapError" class="map-error">{{ mapError }}</div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
|
+import { FullScreen, Location, MapLocation, Minus, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+const centerPoint = { lng: 110.3937, lat: 28.4640 }
|
|
|
+
|
|
|
+const MAP_BOUNDS = {
|
|
|
+ southWest: { lng: 110.365, lat: 28.452 },
|
|
|
+ northEast: { lng: 110.420, lat: 28.482 }
|
|
|
+}
|
|
|
+const MAP_MIN_ZOOM = 13
|
|
|
+const MAP_MAX_ZOOM = 19
|
|
|
+const MAP_DEFAULT_ZOOM = 15
|
|
|
+
|
|
|
+const defaultLayerState = () => ({
|
|
|
+ pipelines: { high: true, subHigh: true, medium: true, low: true },
|
|
|
+ pipePoints: { valve: true, regulator: true, flowmeter: true, terminal: true },
|
|
|
+ manholes: { cover: true, inspection: true }
|
|
|
+})
|
|
|
+
|
|
|
+const boundaryPoints = [
|
|
|
+ { lng: 110.368, lat: 28.478 },
|
|
|
+ { lng: 110.385, lat: 28.480 },
|
|
|
+ { lng: 110.402, lat: 28.480 },
|
|
|
+ { lng: 110.416, lat: 28.476 },
|
|
|
+ { lng: 110.418, lat: 28.466 },
|
|
|
+ { lng: 110.415, lat: 28.459 },
|
|
|
+ { lng: 110.408, lat: 28.455 },
|
|
|
+ { lng: 110.395, lat: 28.456 },
|
|
|
+ { lng: 110.382, lat: 28.458 },
|
|
|
+ { lng: 110.372, lat: 28.462 },
|
|
|
+ { lng: 110.367, lat: 28.470 }
|
|
|
+]
|
|
|
+
|
|
|
+const pipelineData = [
|
|
|
+ {
|
|
|
+ id: 'pipe-high-01',
|
|
|
+ level: 'high',
|
|
|
+ name: '辰州路高压燃气管线',
|
|
|
+ code: 'GX-HP-001',
|
|
|
+ length: 4.1,
|
|
|
+ material: 'L290N 无缝钢管',
|
|
|
+ diameter: 'DN300',
|
|
|
+ pressure: '高压A (4.0MPa)',
|
|
|
+ road: '辰州北路 — 辰州中路',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '直埋敷设',
|
|
|
+ depth: '1.8m',
|
|
|
+ commissioningDate: '2026-06',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3918, lat: 28.4788 },
|
|
|
+ { lng: 110.3920, lat: 28.4765 },
|
|
|
+ { lng: 110.3924, lat: 28.4743 },
|
|
|
+ { lng: 110.3927, lat: 28.4721 },
|
|
|
+ { lng: 110.3931, lat: 28.4698 },
|
|
|
+ { lng: 110.3934, lat: 28.4675 },
|
|
|
+ { lng: 110.3936, lat: 28.4652 },
|
|
|
+ { lng: 110.3937, lat: 28.4630 },
|
|
|
+ { lng: 110.3935, lat: 28.4607 },
|
|
|
+ { lng: 110.3934, lat: 28.4585 },
|
|
|
+ { lng: 110.3935, lat: 28.4576 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'pipe-subhigh-01',
|
|
|
+ level: 'subHigh',
|
|
|
+ name: '古城路次高压管线',
|
|
|
+ code: 'GX-SHP-002',
|
|
|
+ length: 3.8,
|
|
|
+ material: 'L245N 无缝钢管',
|
|
|
+ diameter: 'DN250',
|
|
|
+ pressure: '次高压A (1.6MPa)',
|
|
|
+ road: '古城北路 — 古城中路',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '直埋敷设',
|
|
|
+ depth: '1.6m',
|
|
|
+ commissioningDate: '2026-03',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3980, lat: 28.4790 },
|
|
|
+ { lng: 110.3979, lat: 28.4765 },
|
|
|
+ { lng: 110.3977, lat: 28.4741 },
|
|
|
+ { lng: 110.3975, lat: 28.4717 },
|
|
|
+ { lng: 110.3973, lat: 28.4693 },
|
|
|
+ { lng: 110.3970, lat: 28.4669 },
|
|
|
+ { lng: 110.3968, lat: 28.4645 },
|
|
|
+ { lng: 110.3965, lat: 28.4622 },
|
|
|
+ { lng: 110.3963, lat: 28.4598 },
|
|
|
+ { lng: 110.3962, lat: 28.4575 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'pipe-medium-01',
|
|
|
+ level: 'medium',
|
|
|
+ name: '迎宾路中压管线',
|
|
|
+ code: 'GX-MP-003',
|
|
|
+ length: 5.5,
|
|
|
+ material: 'PE100 SDR11',
|
|
|
+ diameter: 'DN200',
|
|
|
+ pressure: '中压A (0.4MPa)',
|
|
|
+ road: '迎宾西路 — 迎宾东路',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '直埋敷设',
|
|
|
+ depth: '1.4m',
|
|
|
+ commissioningDate: '2025-09',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3732, lat: 28.4650 },
|
|
|
+ { lng: 110.3778, lat: 28.4648 },
|
|
|
+ { lng: 110.3824, lat: 28.4645 },
|
|
|
+ { lng: 110.3870, lat: 28.4642 },
|
|
|
+ { lng: 110.3916, lat: 28.4640 },
|
|
|
+ { lng: 110.3962, lat: 28.4637 },
|
|
|
+ { lng: 110.4008, lat: 28.4634 },
|
|
|
+ { lng: 110.4054, lat: 28.4630 },
|
|
|
+ { lng: 110.4098, lat: 28.4627 },
|
|
|
+ { lng: 110.4120, lat: 28.4625 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'pipe-medium-02',
|
|
|
+ level: 'medium',
|
|
|
+ name: '建设路中压管线',
|
|
|
+ code: 'GX-MP-004',
|
|
|
+ length: 4.8,
|
|
|
+ material: 'PE100 SDR11',
|
|
|
+ diameter: 'DN160',
|
|
|
+ pressure: '中压B (0.2MPa)',
|
|
|
+ road: '建设西路 — 建设东路',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '定向钻穿越',
|
|
|
+ depth: '1.5m',
|
|
|
+ commissioningDate: '2025-04',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3748, lat: 28.4583 },
|
|
|
+ { lng: 110.3794, lat: 28.4586 },
|
|
|
+ { lng: 110.3840, lat: 28.4588 },
|
|
|
+ { lng: 110.3886, lat: 28.4589 },
|
|
|
+ { lng: 110.3912, lat: 28.4586 },
|
|
|
+ { lng: 110.3937, lat: 28.4582 },
|
|
|
+ { lng: 110.3966, lat: 28.4579 },
|
|
|
+ { lng: 110.4002, lat: 28.4581 },
|
|
|
+ { lng: 110.4038, lat: 28.4578 },
|
|
|
+ { lng: 110.4076, lat: 28.4575 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'pipe-low-01',
|
|
|
+ level: 'low',
|
|
|
+ name: '滨江路低压管线',
|
|
|
+ code: 'GX-LP-005',
|
|
|
+ length: 4.0,
|
|
|
+ material: 'PE80 SDR17.6',
|
|
|
+ diameter: 'DN110',
|
|
|
+ pressure: '低压 (5kPa)',
|
|
|
+ road: '滨江路 — 沿江北岸',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '直埋敷设',
|
|
|
+ depth: '1.2m',
|
|
|
+ commissioningDate: '2025-11',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3762, lat: 28.4569 },
|
|
|
+ { lng: 110.3804, lat: 28.4570 },
|
|
|
+ { lng: 110.3846, lat: 28.4571 },
|
|
|
+ { lng: 110.3882, lat: 28.4568 },
|
|
|
+ { lng: 110.3918, lat: 28.4565 },
|
|
|
+ { lng: 110.3954, lat: 28.4562 },
|
|
|
+ { lng: 110.3990, lat: 28.4560 },
|
|
|
+ { lng: 110.4028, lat: 28.4559 },
|
|
|
+ { lng: 110.4066, lat: 28.4557 },
|
|
|
+ { lng: 110.4092, lat: 28.4556 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'pipe-low-02',
|
|
|
+ level: 'low',
|
|
|
+ name: '天宁路低压管线',
|
|
|
+ code: 'GX-LP-006',
|
|
|
+ length: 3.5,
|
|
|
+ material: 'PE80 SDR17.6',
|
|
|
+ diameter: 'DN90',
|
|
|
+ pressure: '低压 (3kPa)',
|
|
|
+ road: '天宁西路 — 天宁东路',
|
|
|
+ status: '运行正常',
|
|
|
+ layingMethod: '直埋敷设',
|
|
|
+ depth: '1.0m',
|
|
|
+ commissioningDate: '2026-05',
|
|
|
+ points: [
|
|
|
+ { lng: 110.3828, lat: 28.4704 },
|
|
|
+ { lng: 110.3870, lat: 28.4702 },
|
|
|
+ { lng: 110.3912, lat: 28.4700 },
|
|
|
+ { lng: 110.3954, lat: 28.4697 },
|
|
|
+ { lng: 110.3996, lat: 28.4695 },
|
|
|
+ { lng: 110.4038, lat: 28.4693 },
|
|
|
+ { lng: 110.4078, lat: 28.4691 },
|
|
|
+ { lng: 110.4092, lat: 28.4690 }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const pipePointData = [
|
|
|
+ {
|
|
|
+ id: 'point-valve-01', type: 'valve', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州北路阀门', code: 'GD-FM-001', road: '辰州北路',
|
|
|
+ status: '在线', deviceModel: 'Z45X-16Q', caliber: 'DN300',
|
|
|
+ lng: 110.3924, lat: 28.4743
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-valve-02', type: 'valve', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州中路阀门', code: 'GD-FM-002', road: '辰州中路',
|
|
|
+ status: '在线', deviceModel: 'Z41H-25C', caliber: 'DN300',
|
|
|
+ lng: 110.3936, lat: 28.4652
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-valve-03', type: 'valve', pipelineId: 'pipe-subhigh-01',
|
|
|
+ name: '古城北路阀门', code: 'GD-FM-003', road: '古城北路',
|
|
|
+ status: '在线', deviceModel: 'Z45X-16Q', caliber: 'DN250',
|
|
|
+ lng: 110.3977, lat: 28.4741
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-valve-04', type: 'valve', pipelineId: 'pipe-medium-01',
|
|
|
+ name: '迎宾东路阀门', code: 'GD-FM-004', road: '迎宾东路',
|
|
|
+ status: '在线', deviceModel: 'Z41H-16C', caliber: 'DN200',
|
|
|
+ lng: 110.4054, lat: 28.4630
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-regulator-01', type: 'regulator', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州路调压站', code: 'GD-TY-001', road: '辰州中路',
|
|
|
+ status: '在线', deviceModel: 'RTZ-80/0.4F', caliber: 'DN300→DN200',
|
|
|
+ lng: 110.3934, lat: 28.4585
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-regulator-02', type: 'regulator', pipelineId: 'pipe-subhigh-01',
|
|
|
+ name: '古城路调压站', code: 'GD-TY-002', road: '古城中路',
|
|
|
+ status: '在线', deviceModel: 'RTZ-50/0.4F', caliber: 'DN250→DN160',
|
|
|
+ lng: 110.3963, lat: 28.4598
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-regulator-03', type: 'regulator', pipelineId: 'pipe-medium-02',
|
|
|
+ name: '建设路调压柜', code: 'GD-TY-003', road: '建设西路',
|
|
|
+ status: '在线', deviceModel: 'RTZ-31/0.4Q', caliber: 'DN160→DN110',
|
|
|
+ lng: 110.3840, lat: 28.4588
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-flowmeter-01', type: 'flowmeter', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州北路计量站', code: 'GD-LL-001', road: '辰州北路',
|
|
|
+ status: '在线', deviceModel: 'LWQZ-150', caliber: 'DN300',
|
|
|
+ lng: 110.3927, lat: 28.4721
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-flowmeter-02', type: 'flowmeter', pipelineId: 'pipe-medium-01',
|
|
|
+ name: '迎宾西路计量站', code: 'GD-LL-002', road: '迎宾西路',
|
|
|
+ status: '在线', deviceModel: 'LWQZ-100', caliber: 'DN200',
|
|
|
+ lng: 110.3824, lat: 28.4645
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-flowmeter-03', type: 'flowmeter', pipelineId: 'pipe-medium-02',
|
|
|
+ name: '建设东路计量柜', code: 'GD-LL-003', road: '建设东路',
|
|
|
+ status: '在线', deviceModel: 'LWQZ-80', caliber: 'DN110',
|
|
|
+ lng: 110.4038, lat: 28.4578
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-terminal-01', type: 'terminal', pipelineId: 'pipe-medium-02',
|
|
|
+ name: '沅陵县政府终端', code: 'GD-ZD-001', road: '辰州中街',
|
|
|
+ status: '在线', deviceModel: 'RTU-GPRS-4G', caliber: '—',
|
|
|
+ lng: 110.3937, lat: 28.4582
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'point-terminal-02', type: 'terminal', pipelineId: 'pipe-low-02',
|
|
|
+ name: '天宁东路终端', code: 'GD-ZD-002', road: '天宁东路',
|
|
|
+ status: '在线', deviceModel: 'RTU-GPRS-4G', caliber: '—',
|
|
|
+ lng: 110.4092, lat: 28.4690
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const manholeData = [
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-01', type: 'cover', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州北路窨井盖', code: 'YJ-JG-001', road: '辰州北路',
|
|
|
+ status: '完好', material: '球墨铸铁', size: 'Φ700',
|
|
|
+ lng: 110.3920, lat: 28.4765
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-02', type: 'cover', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州中路窨井盖', code: 'YJ-JG-002', road: '辰州中路',
|
|
|
+ status: '完好', material: '球墨铸铁', size: 'Φ700',
|
|
|
+ lng: 110.3935, lat: 28.4607
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-03', type: 'cover', pipelineId: 'pipe-subhigh-01',
|
|
|
+ name: '古城北路窨井盖', code: 'YJ-JG-003', road: '古城北路',
|
|
|
+ status: '完好', material: '复合树脂', size: 'Φ700',
|
|
|
+ lng: 110.3979, lat: 28.4765
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-04', type: 'cover', pipelineId: 'pipe-medium-01',
|
|
|
+ name: '迎宾路窨井盖', code: 'YJ-JG-004', road: '迎宾中路',
|
|
|
+ status: '轻微破损', material: '球墨铸铁', size: 'Φ700',
|
|
|
+ lng: 110.3962, lat: 28.4637
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-05', type: 'cover', pipelineId: 'pipe-medium-02',
|
|
|
+ name: '建设路窨井盖', code: 'YJ-JG-005', road: '建设东路',
|
|
|
+ status: '完好', material: '球墨铸铁', size: 'Φ600',
|
|
|
+ lng: 110.4002, lat: 28.4581
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-cover-06', type: 'cover', pipelineId: 'pipe-low-01',
|
|
|
+ name: '滨江路窨井盖', code: 'YJ-JG-006', road: '滨江路',
|
|
|
+ status: '完好', material: '复合树脂', size: 'Φ700',
|
|
|
+ lng: 110.3954, lat: 28.4562
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-inspection-01', type: 'inspection', pipelineId: 'pipe-high-01',
|
|
|
+ name: '辰州路检查井', code: 'YJ-JC-001', road: '辰州中路',
|
|
|
+ status: '巡检正常', material: '钢筋混凝土', size: '1000×1000',
|
|
|
+ lng: 110.3934, lat: 28.4675
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-inspection-02', type: 'inspection', pipelineId: 'pipe-subhigh-01',
|
|
|
+ name: '古城路检查井', code: 'YJ-JC-002', road: '古城中路',
|
|
|
+ status: '巡检正常', material: '钢筋混凝土', size: '1200×1000',
|
|
|
+ lng: 110.3970, lat: 28.4669
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'manhole-inspection-03', type: 'inspection', pipelineId: 'pipe-low-02',
|
|
|
+ name: '天宁路检查井', code: 'YJ-JC-003', road: '天宁西路',
|
|
|
+ status: '需维护', material: '钢筋混凝土', size: '1000×1000',
|
|
|
+ lng: 110.3954, lat: 28.4697
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const colorMap = {
|
|
|
+ high: '#e63946',
|
|
|
+ subHigh: '#f77f00',
|
|
|
+ medium: '#fcbf49',
|
|
|
+ low: '#2a9d8f',
|
|
|
+ valve: '#457b9d',
|
|
|
+ regulator: '#e76f51',
|
|
|
+ flowmeter: '#2d6a4f',
|
|
|
+ terminal: '#6c757d',
|
|
|
+ cover: '#7f4f24',
|
|
|
+ inspection: '#b08968'
|
|
|
+}
|
|
|
+
|
|
|
+const labelMap = {
|
|
|
+ high: '高压管线',
|
|
|
+ subHigh: '次高压管线',
|
|
|
+ medium: '中压管线',
|
|
|
+ low: '低压管线',
|
|
|
+ valve: '阀门',
|
|
|
+ regulator: '调压器',
|
|
|
+ flowmeter: '流量计',
|
|
|
+ terminal: '终端节点',
|
|
|
+ cover: '窨井盖',
|
|
|
+ inspection: '检查井'
|
|
|
+}
|
|
|
+
|
|
|
+const mapRef = ref(null)
|
|
|
+const screenRef = ref(null)
|
|
|
+const searchKeyword = ref('')
|
|
|
+const currentCoordinate = ref(`${centerPoint.lng}, ${centerPoint.lat}`)
|
|
|
+const selectedFeature = ref(null)
|
|
|
+const mapError = ref('')
|
|
|
+const layerState = reactive(defaultLayerState())
|
|
|
+
|
|
|
+let mapInstance = null
|
|
|
+let activeInfoWindow = null
|
|
|
+let overlayClicked = false
|
|
|
+
|
|
|
+const summaryStats = computed(() => ({
|
|
|
+ pipelineLength: pipelineData.reduce((sum, item) => sum + item.length, 0).toFixed(1),
|
|
|
+ pipePointCount: pipePointData.length,
|
|
|
+ manholeCount: manholeData.length,
|
|
|
+ coverageArea: '18.6'
|
|
|
+}))
|
|
|
+
|
|
|
+const selectedFeatureDetails = computed(() => {
|
|
|
+ if (!selectedFeature.value) return []
|
|
|
+ return Object.entries(selectedFeature.value.details).map(([label, value]) => ({ label, value }))
|
|
|
+})
|
|
|
+
|
|
|
+const searchableFeatures = computed(() => {
|
|
|
+ const pipelines = pipelineData.map(item => ({
|
|
|
+ kind: 'pipeline',
|
|
|
+ type: item.level,
|
|
|
+ name: item.name,
|
|
|
+ code: item.code,
|
|
|
+ road: item.road,
|
|
|
+ lng: getMidPoint(item.points).lng,
|
|
|
+ lat: getMidPoint(item.points).lat,
|
|
|
+ raw: item
|
|
|
+ }))
|
|
|
+ const pipePoints = pipePointData.map(item => ({
|
|
|
+ kind: 'pipePoint',
|
|
|
+ type: item.type,
|
|
|
+ name: item.name,
|
|
|
+ code: item.code,
|
|
|
+ road: item.road,
|
|
|
+ lng: item.lng,
|
|
|
+ lat: item.lat,
|
|
|
+ raw: item
|
|
|
+ }))
|
|
|
+ const manholes = manholeData.map(item => ({
|
|
|
+ kind: 'manhole',
|
|
|
+ type: item.type,
|
|
|
+ name: item.name,
|
|
|
+ code: item.code,
|
|
|
+ road: item.road,
|
|
|
+ lng: item.lng,
|
|
|
+ lat: item.lat,
|
|
|
+ raw: item
|
|
|
+ }))
|
|
|
+ return [...pipelines, ...pipePoints, ...manholes]
|
|
|
+})
|
|
|
+
|
|
|
+watch(layerState, () => {
|
|
|
+ renderMapScene()
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await nextTick()
|
|
|
+ waitForBMapGL()
|
|
|
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
|
+ if (mapInstance) {
|
|
|
+ mapInstance.clearOverlays()
|
|
|
+ mapInstance = null
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+function waitForBMapGL() {
|
|
|
+ if (typeof BMapGL !== 'undefined') {
|
|
|
+ initMap()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let attempts = 0
|
|
|
+ const maxAttempts = 50
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ attempts++
|
|
|
+ if (typeof BMapGL !== 'undefined') {
|
|
|
+ clearInterval(timer)
|
|
|
+ initMap()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (attempts >= maxAttempts) {
|
|
|
+ clearInterval(timer)
|
|
|
+ mapError.value = '百度地图脚本加载超时,请刷新页面重试。'
|
|
|
+ }
|
|
|
+ }, 200)
|
|
|
+}
|
|
|
+
|
|
|
+function initMap() {
|
|
|
+ const el = document.getElementById('gisMap')
|
|
|
+ if (!el) {
|
|
|
+ mapError.value = '地图容器未找到。'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ mapInstance = new BMapGL.Map('gisMap', { enableMapClick: true })
|
|
|
+ } catch (e) {
|
|
|
+ console.error('创建地图实例失败:', e)
|
|
|
+ mapError.value = '百度地图初始化失败,请检查AK和网络连接。'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const centerBMapPoint = new BMapGL.Point(centerPoint.lng, centerPoint.lat)
|
|
|
+ mapInstance.centerAndZoom(centerBMapPoint, MAP_DEFAULT_ZOOM)
|
|
|
+ mapInstance.enableScrollWheelZoom(true)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('设置地图中心失败:', e)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (typeof mapInstance.setMinZoom === 'function') {
|
|
|
+ mapInstance.setMinZoom(MAP_MIN_ZOOM)
|
|
|
+ }
|
|
|
+ if (typeof mapInstance.setMaxZoom === 'function') {
|
|
|
+ mapInstance.setMaxZoom(MAP_MAX_ZOOM)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('设置缩放限制失败:', e)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (typeof mapInstance.setMaxBounds === 'function') {
|
|
|
+ const sw = new BMapGL.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
|
|
|
+ const ne = new BMapGL.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
|
|
|
+ mapInstance.setMaxBounds(new BMapGL.Bounds(sw, ne))
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('设置地图范围限制失败:', e)
|
|
|
+ }
|
|
|
+
|
|
|
+ mapInstance.addEventListener('click', () => {
|
|
|
+ if (overlayClicked) {
|
|
|
+ overlayClicked = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ closeInfoWindow()
|
|
|
+ selectedFeature.value = null
|
|
|
+ })
|
|
|
+
|
|
|
+ mapInstance.addEventListener('mousemove', (event) => {
|
|
|
+ try {
|
|
|
+ if (event && event.latlng) {
|
|
|
+ currentCoordinate.value = `${event.latlng.lng.toFixed(4)}, ${event.latlng.lat.toFixed(4)}`
|
|
|
+ }
|
|
|
+ } catch (_) { /* ignore mousemove errors */ }
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ renderMapScene()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('渲染地图要素失败:', e)
|
|
|
+ mapError.value = '地图要素渲染失败。'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderMapScene() {
|
|
|
+ if (!mapInstance || typeof BMapGL === 'undefined') return
|
|
|
+
|
|
|
+ closeInfoWindow()
|
|
|
+ try { mapInstance.clearOverlays() } catch (_) { /* ignore */ }
|
|
|
+ try { drawBoundary() } catch (e) { console.warn('绘制围栏失败:', e) }
|
|
|
+ try { drawPipelines() } catch (e) { console.warn('绘制管线失败:', e) }
|
|
|
+ try { drawPipePoints() } catch (e) { console.warn('绘制管点失败:', e) }
|
|
|
+ try { drawManholes() } catch (e) { console.warn('绘制窨井失败:', e) }
|
|
|
+}
|
|
|
+
|
|
|
+function drawBoundary() {
|
|
|
+ const pts = boundaryPoints.map(item => new BMapGL.Point(item.lng, item.lat))
|
|
|
+ const polygon = new BMapGL.Polygon(pts, {
|
|
|
+ strokeColor: '#52b788',
|
|
|
+ strokeWeight: 2.5,
|
|
|
+ strokeOpacity: 0.85,
|
|
|
+ strokeStyle: 'dashed',
|
|
|
+ fillColor: '#d8f3dc',
|
|
|
+ fillOpacity: 0.22
|
|
|
+ })
|
|
|
+ mapInstance.addOverlay(polygon)
|
|
|
+}
|
|
|
+
|
|
|
+function drawPipelines() {
|
|
|
+ pipelineData.forEach(item => {
|
|
|
+ try {
|
|
|
+ if (!layerState.pipelines[item.level]) return
|
|
|
+ const pts = item.points.map(p => new BMapGL.Point(p.lng, p.lat))
|
|
|
+ const polyline = new BMapGL.Polyline(pts, {
|
|
|
+ strokeColor: colorMap[item.level],
|
|
|
+ strokeWeight: 6,
|
|
|
+ strokeOpacity: 0.92
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ polyline.addEventListener('click', (e) => {
|
|
|
+ overlayClicked = true
|
|
|
+ e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
|
|
|
+ showPopup(item.points[0].lng, item.points[0].lat, buildPopupHTML(buildPipelineDetail(item)))
|
|
|
+ selectedFeature.value = buildPipelineDetail(item)
|
|
|
+ })
|
|
|
+ } catch (_) { /* polyline click listener failed */ }
|
|
|
+ mapInstance.addOverlay(polyline)
|
|
|
+
|
|
|
+ const mid = getMidPoint(item.points)
|
|
|
+ addPipeLabel(mid.lng, mid.lat, item.name, colorMap[item.level], item)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`绘制管线 ${item.name} 失败:`, e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function drawPipePoints() {
|
|
|
+ pipePointData.forEach(item => {
|
|
|
+ try {
|
|
|
+ if (!layerState.pipePoints[item.type]) return
|
|
|
+ addPointMarker(item.lng, item.lat, colorMap[item.type], item.name, 'dot', () => {
|
|
|
+ showPopup(item.lng, item.lat, buildPopupHTML(buildPipePointDetail(item)))
|
|
|
+ selectedFeature.value = buildPipePointDetail(item)
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`绘制管点 ${item.name} 失败:`, e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function drawManholes() {
|
|
|
+ manholeData.forEach(item => {
|
|
|
+ try {
|
|
|
+ if (!layerState.manholes[item.type]) return
|
|
|
+ addPointMarker(item.lng, item.lat, colorMap[item.type], item.name, 'square', () => {
|
|
|
+ showPopup(item.lng, item.lat, buildPopupHTML(buildManholeDetail(item)))
|
|
|
+ selectedFeature.value = buildManholeDetail(item)
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`绘制窨井 ${item.name} 失败:`, e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function addPipeLabel(lng, lat, text, color, rawItem) {
|
|
|
+ const point = new BMapGL.Point(lng, lat)
|
|
|
+ const html = `<div class="pipe-label" style="color:${color}">${text}</div>`
|
|
|
+ const label = new BMapGL.Label(html, {
|
|
|
+ position: point,
|
|
|
+ offset: new BMapGL.Size(-28, -8)
|
|
|
+ })
|
|
|
+ label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
|
|
|
+ try {
|
|
|
+ label.addEventListener('click', (e) => {
|
|
|
+ overlayClicked = true
|
|
|
+ e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
|
|
|
+ showPopup(lng, lat, buildPopupHTML(buildPipelineDetail(rawItem)))
|
|
|
+ selectedFeature.value = buildPipelineDetail(rawItem)
|
|
|
+ })
|
|
|
+ } catch (_) { /* label click listener failed */ }
|
|
|
+ mapInstance.addOverlay(label)
|
|
|
+}
|
|
|
+
|
|
|
+function addPointMarker(lng, lat, color, name, shape, onClick) {
|
|
|
+ const point = new BMapGL.Point(lng, lat)
|
|
|
+ const size = 22
|
|
|
+ const borderRadius = shape === 'square' ? '3px' : '50%'
|
|
|
+ const html = `<div class="marker-wrap" style="cursor:pointer">
|
|
|
+ <div style="width:${size}px;height:${size}px;background:${color};border-radius:${borderRadius};border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.25);margin:0 auto;"></div>
|
|
|
+ <div class="marker-text">${name}</div>
|
|
|
+ </div>`
|
|
|
+ const label = new BMapGL.Label(html, {
|
|
|
+ position: point,
|
|
|
+ offset: new BMapGL.Size(-size / 2 - 3, -size - 8)
|
|
|
+ })
|
|
|
+ label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
|
|
|
+ try {
|
|
|
+ label.addEventListener('click', (e) => {
|
|
|
+ overlayClicked = true
|
|
|
+ e.domEvent && e.domEvent.stopPropagation && e.domEvent.stopPropagation()
|
|
|
+ onClick && onClick()
|
|
|
+ })
|
|
|
+ } catch (_) { /* marker click listener failed */ }
|
|
|
+ mapInstance.addOverlay(label)
|
|
|
+}
|
|
|
+
|
|
|
+function showPopup(lng, lat, html) {
|
|
|
+ try {
|
|
|
+ closeInfoWindow()
|
|
|
+ const point = new BMapGL.Point(lng, lat)
|
|
|
+ const infoWindow = new BMapGL.InfoWindow(html, {
|
|
|
+ width: 310,
|
|
|
+ title: '',
|
|
|
+ enableMessage: false
|
|
|
+ })
|
|
|
+ activeInfoWindow = infoWindow
|
|
|
+ mapInstance.openInfoWindow(infoWindow, point)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('弹出信息窗口失败:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function closeInfoWindow() {
|
|
|
+ if (activeInfoWindow && mapInstance) {
|
|
|
+ mapInstance.closeInfoWindow()
|
|
|
+ activeInfoWindow = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildPopupHTML(detail) {
|
|
|
+ const rows = Object.entries(detail.details)
|
|
|
+ .map(([k, v]) => `<div class="popup-row"><span class="popup-label">${k}</span><span class="popup-value">${v}</span></div>`)
|
|
|
+ .join('')
|
|
|
+ return `<div class="info-popup">
|
|
|
+ <div class="popup-header">${detail.name}</div>
|
|
|
+ <div class="popup-tag">${detail.category}</div>
|
|
|
+ <div class="popup-divider"></div>
|
|
|
+ ${rows}
|
|
|
+ </div>`
|
|
|
+}
|
|
|
+
|
|
|
+function buildPipelineDetail(item) {
|
|
|
+ return {
|
|
|
+ name: item.name,
|
|
|
+ category: labelMap[item.level],
|
|
|
+ details: {
|
|
|
+ '管线编号': item.code,
|
|
|
+ '所属道路': item.road,
|
|
|
+ '管线长度': `${item.length} km`,
|
|
|
+ '管径': item.diameter,
|
|
|
+ '材质': item.material,
|
|
|
+ '压力等级': item.pressure,
|
|
|
+ '敷设方式': item.layingMethod,
|
|
|
+ '埋深': item.depth,
|
|
|
+ '投产日期': item.commissioningDate,
|
|
|
+ '运行状态': item.status
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getPipelineName(pipelineId) {
|
|
|
+ const found = pipelineData.find(p => p.id === pipelineId)
|
|
|
+ return found ? found.name : '—'
|
|
|
+}
|
|
|
+
|
|
|
+function buildPipePointDetail(item) {
|
|
|
+ return {
|
|
|
+ name: item.name,
|
|
|
+ category: labelMap[item.type],
|
|
|
+ details: {
|
|
|
+ '设备编号': item.code,
|
|
|
+ '所属管线': getPipelineName(item.pipelineId),
|
|
|
+ '所属道路': item.road,
|
|
|
+ '设备型号': item.deviceModel,
|
|
|
+ '口径': item.caliber,
|
|
|
+ '运行状态': item.status,
|
|
|
+ '经度': item.lng.toFixed(5),
|
|
|
+ '纬度': item.lat.toFixed(5)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildManholeDetail(item) {
|
|
|
+ return {
|
|
|
+ name: item.name,
|
|
|
+ category: labelMap[item.type],
|
|
|
+ details: {
|
|
|
+ '井盖编号': item.code,
|
|
|
+ '所属管线': getPipelineName(item.pipelineId),
|
|
|
+ '所属道路': item.road,
|
|
|
+ '材质': item.material,
|
|
|
+ '规格': item.size,
|
|
|
+ '巡检状态': item.status,
|
|
|
+ '经度': item.lng.toFixed(5),
|
|
|
+ '纬度': item.lat.toFixed(5)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getMidPoint(points) {
|
|
|
+ return points[Math.floor(points.length / 2)]
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ const keyword = searchKeyword.value.trim()
|
|
|
+ if (!keyword) {
|
|
|
+ ElMessage.warning('请输入搜索关键字')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const matched = searchableFeatures.value.find(item =>
|
|
|
+ [item.name, item.code, item.road].some(text => text?.includes(keyword))
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!matched) {
|
|
|
+ ElMessage.info('未找到匹配要素')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(matched.lng, matched.lat), 18)
|
|
|
+
|
|
|
+ if (matched.kind === 'pipeline') {
|
|
|
+ selectedFeature.value = buildPipelineDetail(matched.raw)
|
|
|
+ showPopup(matched.lng, matched.lat, buildPopupHTML(selectedFeature.value))
|
|
|
+ } else if (matched.kind === 'pipePoint') {
|
|
|
+ selectedFeature.value = buildPipePointDetail(matched.raw)
|
|
|
+ showPopup(matched.lng, matched.lat, buildPopupHTML(selectedFeature.value))
|
|
|
+ } else {
|
|
|
+ selectedFeature.value = buildManholeDetail(matched.raw)
|
|
|
+ showPopup(matched.lng, matched.lat, buildPopupHTML(selectedFeature.value))
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success(`已定位到 ${matched.name}`)
|
|
|
+}
|
|
|
+
|
|
|
+function refreshScene() {
|
|
|
+ layerState.pipelines = { high: true, subHigh: true, medium: true, low: true }
|
|
|
+ layerState.pipePoints = { valve: true, regulator: true, flowmeter: true, terminal: true }
|
|
|
+ layerState.manholes = { cover: true, inspection: true }
|
|
|
+ searchKeyword.value = ''
|
|
|
+ selectedFeature.value = null
|
|
|
+ closeInfoWindow()
|
|
|
+ locateCenter()
|
|
|
+}
|
|
|
+
|
|
|
+function locateCenter() {
|
|
|
+ if (!mapInstance) return
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(centerPoint.lng, centerPoint.lat), MAP_DEFAULT_ZOOM)
|
|
|
+}
|
|
|
+
|
|
|
+function zoomIn() {
|
|
|
+ if (!mapInstance) return
|
|
|
+ const z = mapInstance.getZoom()
|
|
|
+ if (z < MAP_MAX_ZOOM) mapInstance.zoomIn()
|
|
|
+ else ElMessage.info('已放大至最大级别')
|
|
|
+}
|
|
|
+
|
|
|
+function zoomOut() {
|
|
|
+ if (!mapInstance) return
|
|
|
+ const z = mapInstance.getZoom()
|
|
|
+ if (z > MAP_MIN_ZOOM) mapInstance.zoomOut()
|
|
|
+ else ElMessage.info('已缩小至最小级别')
|
|
|
+}
|
|
|
+
|
|
|
+function toggleFullscreen() {
|
|
|
+ const target = screenRef.value
|
|
|
+ if (!target) return
|
|
|
+ if (!document.fullscreenElement) {
|
|
|
+ target.requestFullscreen?.()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ document.exitFullscreen?.()
|
|
|
+}
|
|
|
+
|
|
|
+function handleFullscreenChange() {
|
|
|
+ if (!mapInstance) return
|
|
|
+ setTimeout(() => {
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(centerPoint.lng, centerPoint.lat), mapInstance.getZoom())
|
|
|
+ }, 200)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.gis-dashboard {
|
|
|
+ height: calc(100vh - 84px);
|
|
|
+ padding: 16px;
|
|
|
+ background: #f2f5f9;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 10px 16px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 16px rgba(15, 35, 95, 0.05);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.title-main {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #1f2d3d;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.title-metrics {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.metric {
|
|
|
+ position: relative;
|
|
|
+ padding-left: 14px;
|
|
|
+ color: #44546a;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 50%;
|
|
|
+ width: 9px;
|
|
|
+ height: 9px;
|
|
|
+ border-radius: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.metric-danger::before { background: #e63946; }
|
|
|
+.metric-primary::before { background: #457b9d; }
|
|
|
+.metric-warning::before { background: #7f4f24; }
|
|
|
+
|
|
|
+.header-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input {
|
|
|
+ width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-content {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ gap: 14px;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-sidebar {
|
|
|
+ width: 320px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-card {
|
|
|
+ border: none;
|
|
|
+ border-radius: 14px;
|
|
|
+ box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.layer-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 9px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #eef2f8;
|
|
|
+}
|
|
|
+
|
|
|
+.layer-group-last {
|
|
|
+ padding-bottom: 0;
|
|
|
+ margin-bottom: 0;
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.layer-title {
|
|
|
+ color: #303133;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ color: #4e5969;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-line {
|
|
|
+ width: 34px;
|
|
|
+ height: 0;
|
|
|
+ border-top-width: 3px;
|
|
|
+ border-top-style: solid;
|
|
|
+ border-radius: 999px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-high { border-top-color: #e63946; }
|
|
|
+.legend-sub-high { border-top-color: #f77f00; }
|
|
|
+.legend-medium { border-top-color: #fcbf49; }
|
|
|
+.legend-low { border-top-color: #2a9d8f; }
|
|
|
+
|
|
|
+.legend-dot,
|
|
|
+.legend-square {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-dot { border-radius: 50%; }
|
|
|
+.legend-square { border-radius: 2px; }
|
|
|
+.legend-valve { background: #457b9d; }
|
|
|
+.legend-regulator { background: #e76f51; }
|
|
|
+.legend-flowmeter { background: #2d6a4f; }
|
|
|
+.legend-terminal { background: #6c757d; }
|
|
|
+.legend-cover { background: #7f4f24; }
|
|
|
+.legend-inspection { background: #b08968; }
|
|
|
+
|
|
|
+.stats-card {
|
|
|
+ background: linear-gradient(180deg, #f8fbff 0%, #f3f7fb 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.stats-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ color: #4e5969;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-row strong {
|
|
|
+ color: #1f2d3d;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-card {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 280px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-name {
|
|
|
+ color: #1f2d3d;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ width: fit-content;
|
|
|
+ padding: 3px 10px;
|
|
|
+ color: #2d6cdf;
|
|
|
+ background: #edf4ff;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-map-panel {
|
|
|
+ position: relative;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 14px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05);
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.map-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 10px 16px;
|
|
|
+ border-bottom: 1px solid #edf2f7;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-right {
|
|
|
+ color: #7a8699;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.gis-map {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ background: #e9f0e5;
|
|
|
+}
|
|
|
+
|
|
|
+.map-error {
|
|
|
+ position: absolute;
|
|
|
+ inset: 52px 0 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909399;
|
|
|
+ background: rgba(255, 255, 255, 0.85);
|
|
|
+ font-size: 15px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<style>
|
|
|
+.pipe-label {
|
|
|
+ padding: 2px 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1.3;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-shadow: 0 0 4px #fff, 0 0 4px #fff;
|
|
|
+ background: rgba(255, 255, 255, 0.7);
|
|
|
+ border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.marker-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.marker-text {
|
|
|
+ margin-top: 3px;
|
|
|
+ padding: 1px 7px;
|
|
|
+ color: #2f3a4f;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.3;
|
|
|
+ white-space: nowrap;
|
|
|
+ background: rgba(255, 255, 255, 0.78);
|
|
|
+ border-radius: 10px;
|
|
|
+ text-shadow: 0 0 3px #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.info-popup {
|
|
|
+ padding: 4px 2px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-header {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1a1a2e;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-tag {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 2px 10px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #1d4ed8;
|
|
|
+ background: #eff6ff;
|
|
|
+ border-radius: 999px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-divider {
|
|
|
+ height: 1px;
|
|
|
+ background: #e5e7eb;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 5px 0;
|
|
|
+ border-bottom: 1px dashed #f0f0f0;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-label {
|
|
|
+ color: #6b7280;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-right: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-value {
|
|
|
+ color: #1f2937;
|
|
|
+ font-weight: 500;
|
|
|
+ text-align: right;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.BMap_cpyCtrl,
|
|
|
+.anchorBL {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+</style>
|