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