|
|
@@ -1,476 +1,505 @@
|
|
|
-<!--窨井盖安全实时监测一张图-->
|
|
|
+<!--窨井盖安全实时监测一张图(参考 equipmentGIS.vue,仅展示 rootTypeId=4 窨井设备)-->
|
|
|
<template>
|
|
|
- <div class="gis-monitor-container">
|
|
|
- <!-- 头部面板 -->
|
|
|
- <div class="header-panel">
|
|
|
- <div class="title-logo">
|
|
|
- <el-icon :size="26"><Monitor /></el-icon>
|
|
|
- <span class="title">窨井盖安全实时监测一张图</span>
|
|
|
- <span class="version">GIS动态态势平台</span>
|
|
|
- </div>
|
|
|
- <div class="stats-badge">
|
|
|
- <div class="stat-badge-item"><span class="dot online"></span> 在线 {{ stats.online }}</div>
|
|
|
- <div class="stat-badge-item"><span class="dot alert"></span> 告警 {{ stats.alarm }}</div>
|
|
|
- <div class="stat-badge-item"><span class="dot offline"></span> 离线 {{ stats.offline }}</div>
|
|
|
- <div class="stat-badge-item">总计 {{ stats.total }}</div>
|
|
|
- <el-button size="small" :icon="Refresh" @click="loadData" :loading="loading">刷新</el-button>
|
|
|
+ <div class="water-page gis-page">
|
|
|
+ <div class="page-head">
|
|
|
+ <div class="page-head-titles">
|
|
|
+ <div class="page-title">
|
|
|
+ <el-icon class="page-title-icon"><MapLocation /></el-icon>
|
|
|
+ <span>窨井盖安全实时监测一张图</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 主内容区:左侧图层控制 + 右侧GIS地图 -->
|
|
|
- <div class="main-layout">
|
|
|
- <!-- 左侧图层控制面板 -->
|
|
|
- <div class="layer-panel">
|
|
|
- <div class="layer-header"><el-icon><Location /></el-icon> 动态监测图层</div>
|
|
|
- <!-- 类型筛选 -->
|
|
|
- <div class="layer-section">
|
|
|
- <div class="layer-section-title">设备类型</div>
|
|
|
- <el-checkbox-group v-model="typeFilter" @change="refreshMarkers">
|
|
|
- <el-checkbox label="5">供水窨井</el-checkbox>
|
|
|
- <el-checkbox label="9">排水窨井</el-checkbox>
|
|
|
- <el-checkbox label="10">燃气窨井</el-checkbox>
|
|
|
- </el-checkbox-group>
|
|
|
- </div>
|
|
|
- <!-- 状态筛选 -->
|
|
|
- <div class="layer-section">
|
|
|
- <div class="layer-section-title">设备状态</div>
|
|
|
- <el-checkbox-group v-model="statusFilter" @change="refreshMarkers">
|
|
|
- <el-checkbox label="normal">正常</el-checkbox>
|
|
|
- <el-checkbox label="alert">告警</el-checkbox>
|
|
|
- <el-checkbox label="offline">离线</el-checkbox>
|
|
|
- </el-checkbox-group>
|
|
|
+ <!-- 设备统计 -->
|
|
|
+ <div class="kpi-row">
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon total"><el-icon><Monitor /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">设备总数(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.total }}</div>
|
|
|
</div>
|
|
|
- <!-- 图例 -->
|
|
|
- <div class="legend-box">
|
|
|
- <div class="legend-title">图例</div>
|
|
|
- <div class="legend-item"><span class="legend-icon normal-icon"></span> 正常设备</div>
|
|
|
- <div class="legend-item"><span class="legend-icon alert-icon"></span> 告警设备</div>
|
|
|
- <div class="legend-item"><span class="legend-icon offline-icon"></span> 离线设备</div>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon online"><el-icon><CircleCheck /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">在线设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.online }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧 GIS 地图 -->
|
|
|
- <div class="gis-stage">
|
|
|
- <div class="map-toolbar">
|
|
|
- <span class="map-title">窨井设备分布图</span>
|
|
|
- <div class="legend">
|
|
|
- <span><i class="legend-dot online"></i> 在线</span>
|
|
|
- <span><i class="legend-dot alert"></i> 告警</span>
|
|
|
- <span><i class="legend-dot offline"></i> 离线</span>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon offline"><el-icon><CircleClose /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">离线设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.offline }}</div>
|
|
|
</div>
|
|
|
- <div class="map-area">
|
|
|
- <div id="manholeStateMap" class="bmap-container"></div>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon alarm"><el-icon><Warning /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">报警设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.alarm }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 设备详情抽屉 -->
|
|
|
- <el-drawer v-model="drawerVisible" :title="selectedDevice?.name + ' - 监测数据详情'" direction="rtl" size="520px" class="detail-drawer">
|
|
|
- <div v-if="selectedDevice" v-loading="detailLoading">
|
|
|
- <!-- 基本信息 -->
|
|
|
- <el-descriptions :column="1" border size="small">
|
|
|
- <el-descriptions-item label="设备编号">{{ selectedDevice.code }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备名称">{{ selectedDevice.name }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="详细地址">{{ selectedDevice.location }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="经纬度" v-if="selectedDevice.lng">{{ selectedDevice.lng }}, {{ selectedDevice.lat }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="运行状态">
|
|
|
- <el-tag :type="getStatusTagType(selectedDevice.status)" size="small">{{ getStatusText(selectedDevice.status) }}</el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
-
|
|
|
- <!-- 井盖监测数据 (jg_device_data) -->
|
|
|
- <template v-if="selectedDevice.manholeData">
|
|
|
- <el-divider content-position="left">井盖监测数据</el-divider>
|
|
|
- <el-row :gutter="12">
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">倾斜角度</div>
|
|
|
- <div class="metric-value" :class="{ 'metric-alert': isTiltAlert(selectedDevice.manholeData) }">
|
|
|
- {{ selectedDevice.manholeData.tiltAngle || '0' }}°
|
|
|
- </div>
|
|
|
- <div class="metric-sub">阈值: {{ selectedDevice.manholeData.angleAlarmThreshold || '-' }}°</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">电池电量</div>
|
|
|
- <div class="metric-value" :class="{ 'metric-warn': Number(selectedDevice.manholeData.batteryLevel) <= 20 }">
|
|
|
- {{ selectedDevice.manholeData.batteryLevel || '0' }}%
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">信号强度</div>
|
|
|
- <div class="metric-value" :class="{ 'metric-warn': Number(selectedDevice.manholeData.signalStrength) <= 20 }">
|
|
|
- {{ selectedDevice.manholeData.signalStrength || '0' }}%
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">温度值</div>
|
|
|
- <div class="metric-value">{{ selectedDevice.manholeData.temperatureValue || '-' }}°C</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">水浸状态</div>
|
|
|
- <div class="metric-value" :class="{ 'metric-alert': Number(selectedDevice.manholeData.waterInfiltrationAlarmStatus) === 1 }">
|
|
|
- {{ Number(selectedDevice.manholeData.waterInfiltrationAlarmStatus) === 1 ? '报警' : '正常' }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="8">
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-label">水位状态</div>
|
|
|
- <div class="metric-value" :class="{ 'metric-alert': Number(selectedDevice.manholeData.waterLevelAlarmStatus) === 1 }">
|
|
|
- {{ Number(selectedDevice.manholeData.waterLevelAlarmStatus) === 1 ? '报警' : '正常' }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <div class="upload-time">数据上传时间: {{ selectedDevice.manholeData.uploadTime || '-' }}</div>
|
|
|
- </template>
|
|
|
- <el-empty v-else description="暂无井盖监测数据" :image-size="60" />
|
|
|
+ <!-- 地图 -->
|
|
|
+ <el-card shadow="never" class="map-card">
|
|
|
+ <div class="map-wrap">
|
|
|
+ <GisMapCanvas
|
|
|
+ ref="gisMapRef"
|
|
|
+ :points="mapPoints"
|
|
|
+ cluster-label="设备集合"
|
|
|
+ :center="mapCenter"
|
|
|
+ :zoom="15"
|
|
|
+ @point-click="openDetail($event.equipmentId)"
|
|
|
+ @error="mapError = $event"
|
|
|
+ />
|
|
|
+ <div class="map-legend" @mousedown.stop @click.stop @dblclick.stop>
|
|
|
+ <div class="legend-title">点击切换显示</div>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.online }" @click="toggleLegend('online')">
|
|
|
+ <i class="legend-dot online"></i>在线
|
|
|
+ </button>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.offline }" @click="toggleLegend('offline')">
|
|
|
+ <i class="legend-dot offline"></i>离线
|
|
|
+ </button>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.alarm }" @click="toggleLegend('alarm')">
|
|
|
+ <i class="legend-dot alarm"></i>报警(在线/离线时叠加)
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <el-alert
|
|
|
+ v-if="mapError"
|
|
|
+ class="map-error"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ :title="mapError"
|
|
|
+ >
|
|
|
+ <el-button link type="primary" @click="loadData">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+ <el-empty
|
|
|
+ v-if="!loading && !mapError && !mapPoints.length"
|
|
|
+ class="map-empty"
|
|
|
+ description="当前没有可显示的监测设备"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
|
|
|
- <!-- 关联井下设备监测数据 -->
|
|
|
- <el-divider content-position="left">关联井下设备监测</el-divider>
|
|
|
- <div v-if="relDevices.length === 0" class="empty-rel">暂无关联井下设备</div>
|
|
|
- <div v-else class="rel-device-list">
|
|
|
- <div v-for="rel in relDevices" :key="rel.relId" class="rel-device-card">
|
|
|
- <div class="rel-device-header">
|
|
|
- <span class="rel-device-name">{{ rel.equipmentName || '未知设备' }}</span>
|
|
|
- <el-tag size="small" effect="plain">{{ rel.equipmentTypeName || '未知类型' }}</el-tag>
|
|
|
+ <!-- 设备采集记录抽屉 -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="detailVisible"
|
|
|
+ :title="(currentDevice?.equipmentName || currentDevice?.equipmentCode || '设备') + ' - 采集记录'"
|
|
|
+ direction="rtl"
|
|
|
+ size="900px"
|
|
|
+ class="detail-drawer"
|
|
|
+ >
|
|
|
+ <div v-loading="detailLoading">
|
|
|
+ <template v-if="currentDevice">
|
|
|
+ <div class="dlg-summary">
|
|
|
+ <div class="summary-name">{{ currentDevice.equipmentName || '未命名设备' }}</div>
|
|
|
+ <div class="summary-code">{{ currentDevice.equipmentCode }}</div>
|
|
|
+ <span class="summary-tag">{{ currentDevice.equipmentTypeName || '未分类' }}</span>
|
|
|
+ <div class="status-group">
|
|
|
+ <span class="badge" :class="Number(currentDevice.onlineStatus) === 1 ? 'online' : 'offline'">
|
|
|
+ <span class="badge-dot"></span>
|
|
|
+ <span class="badge-text">{{ Number(currentDevice.onlineStatus) === 1 ? '在线' : '离线' }}</span>
|
|
|
+ </span>
|
|
|
+ <span v-if="Number(currentDevice.alarmStatus) === 1" class="badge alarm">
|
|
|
+ <el-icon class="badge-icon"><WarningFilled /></el-icon>
|
|
|
+ <span class="badge-text">报警</span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- <div class="rel-device-location" v-if="rel.equipmentLocation">
|
|
|
- <el-icon><Location /></el-icon> {{ rel.equipmentLocation }}
|
|
|
- </div>
|
|
|
- <!-- 雷达流量计数据 -->
|
|
|
- <template v-if="rel.monitorData?.dataType === '雷达流量计'">
|
|
|
- <el-row :gutter="8" class="rel-metrics">
|
|
|
- <el-col :span="6"><div class="rel-metric"><span class="rm-label">瞬时流量</span><span class="rm-value">{{ rel.monitorData.instantFlow || '-' }}</span></div></el-col>
|
|
|
- <el-col :span="6"><div class="rel-metric"><span class="rm-label">流速</span><span class="rm-value">{{ rel.monitorData.flowSpeed || '-' }}</span></div></el-col>
|
|
|
- <el-col :span="6"><div class="rel-metric"><span class="rm-label">水位</span><span class="rm-value">{{ rel.monitorData.waterLevel || '-' }}</span></div></el-col>
|
|
|
- <el-col :span="6"><div class="rel-metric"><span class="rm-label">累计流量</span><span class="rm-value">{{ rel.monitorData.totalFlow || '-' }}</span></div></el-col>
|
|
|
- </el-row>
|
|
|
- </template>
|
|
|
- <!-- 环境监测数据 -->
|
|
|
- <template v-else-if="rel.monitorData?.dataType === '环境监测'">
|
|
|
- <el-row :gutter="8" class="rel-metrics">
|
|
|
- <el-col :span="8"><div class="rel-metric"><span class="rm-label">温度</span><span class="rm-value">{{ rel.monitorData.temperature ?? '-' }}°C</span></div></el-col>
|
|
|
- <el-col :span="8"><div class="rel-metric"><span class="rm-label">{{ rel.monitorData.metricName || '湿度' }}</span><span class="rm-value">{{ rel.monitorData.metricValue ?? rel.monitorData.humidity ?? '-' }}</span></div></el-col>
|
|
|
- <el-col :span="8"><div class="rel-metric"><span class="rm-label">节点</span><span class="rm-value">{{ rel.monitorData.createTime || '-' }}</span></div></el-col>
|
|
|
- </el-row>
|
|
|
- </template>
|
|
|
- <div v-else class="rel-no-data">暂无监测数据</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+
|
|
|
+ <div class="form-section-title detail-section-title">采集记录(最近 50 条)</div>
|
|
|
+ <div class="hist-wrap">
|
|
|
+ <el-table :data="pagedRecordRows" max-height="420" size="small" border>
|
|
|
+ <el-table-column
|
|
|
+ v-for="c in recordColumns"
|
|
|
+ :key="c.key"
|
|
|
+ :label="c.label"
|
|
|
+ :min-width="c.minWidth"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="isStatusKey(c.key)" :class="{ 'cell-alarm': row[c.key] === '1' }">
|
|
|
+ {{ row[c.key] === '1' ? '报警' : '正常' }}
|
|
|
+ </span>
|
|
|
+ <span v-else>{{ row[c.key] != null && row[c.key] !== '' ? row[c.key] : '—' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <template #empty><div class="rec-empty">暂无采集记录</div></template>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination
|
|
|
+ v-if="recordRows.length"
|
|
|
+ class="rec-pager"
|
|
|
+ layout="prev, pager, next"
|
|
|
+ :total="recordRows.length"
|
|
|
+ :page-size="recordPageSize"
|
|
|
+ small
|
|
|
+ v-model:current-page="recordPage"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-empty v-else description="未选中设备" />
|
|
|
</div>
|
|
|
</el-drawer>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
|
-import { Monitor, Location, Refresh } from '@element-plus/icons-vue'
|
|
|
-import { getManholeLayerData, getManholeRelDeviceData } from '@/api/pipeNetwork/basic'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import locationIcon from '@/assets/images/location.png'
|
|
|
-// 台账存 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
|
|
|
-import { convertCoord } from '@/utils/coordTransform'
|
|
|
+import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
+import { CircleCheck, CircleClose, MapLocation, Monitor, Refresh, Warning, WarningFilled } from '@element-plus/icons-vue'
|
|
|
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
+import { getEquipmentGisPoints, getManholeDataPage } from '@/api/pipeNetwork/basic'
|
|
|
+import { gisPointStatusIcons } from '@/utils/gisMapColors'
|
|
|
|
|
|
-// ==================== 数据状态 ====================
|
|
|
-const loading = ref(false)
|
|
|
-const allDevices = ref([])
|
|
|
-const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
|
|
|
+// 窨井设备固定为 rootTypeId=4
|
|
|
+const ROOT_TYPE_ID = 4
|
|
|
|
|
|
-// 筛选条件
|
|
|
-const typeFilter = ref(['5', '9', '10'])
|
|
|
-const statusFilter = ref(['normal', 'alert', 'offline'])
|
|
|
+const mapCenter = { lng: 110.3965, lat: 28.4638 }
|
|
|
|
|
|
-// 选中设备 & 详情
|
|
|
-const drawerVisible = ref(false)
|
|
|
+const allDevices = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const mapError = ref('')
|
|
|
+const gisMapRef = ref(null)
|
|
|
+const detailVisible = ref(false)
|
|
|
const detailLoading = ref(false)
|
|
|
-const selectedDevice = ref(null)
|
|
|
-const relDevices = ref([])
|
|
|
+const currentDevice = ref(null)
|
|
|
+const recordRows = ref([])
|
|
|
+const recordPage = ref(1)
|
|
|
+const recordPageSize = 10
|
|
|
|
|
|
-// ==================== 加载数据 ====================
|
|
|
-async function loadData() {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const res = await getManholeLayerData({ isQueryManholeData: true })
|
|
|
- const devices = res.data?.devices || []
|
|
|
- allDevices.value = devices.map(d => {
|
|
|
- const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
|
|
|
- const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
|
|
|
- const manholeAlarm = Number(d.manholeData?.alarmStatus)
|
|
|
- const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
|
|
|
- Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
|
|
|
- Number(d.manholeData?.waterLevelAlarmStatus) === 1
|
|
|
- const isOnline = onlineStatus === 1
|
|
|
- let status = 'offline'
|
|
|
- if (isAlert) status = 'alert'
|
|
|
- else if (isOnline) status = 'normal'
|
|
|
- return {
|
|
|
- id: d.equipmentId,
|
|
|
- code: d.equipmentCode,
|
|
|
- name: d.equipmentName,
|
|
|
- typeId: d.equipmentTypeId,
|
|
|
- typeName: d.equipmentTypeName,
|
|
|
- location: d.equipmentLocation,
|
|
|
- lng: d.longitude != null ? Number(d.longitude) : null,
|
|
|
- lat: d.latitude != null ? Number(d.latitude) : null,
|
|
|
- status,
|
|
|
- manholeData: d.manholeData || null
|
|
|
- }
|
|
|
- })
|
|
|
- const statistics = res.data?.statistics || {}
|
|
|
- stats.total = statistics.total || allDevices.value.length
|
|
|
- stats.online = statistics.online || 0
|
|
|
- stats.offline = statistics.offline || 0
|
|
|
- stats.alarm = statistics.alarm || 0
|
|
|
- refreshMarkers()
|
|
|
- } catch (e) {
|
|
|
- ElMessage.error('设备数据加载失败')
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
+const statistics = computed(() => ({
|
|
|
+ total: allDevices.value.length,
|
|
|
+ online: allDevices.value.filter((device) => Number(device.onlineStatus) === 1).length,
|
|
|
+ offline: allDevices.value.filter((device) => Number(device.onlineStatus) !== 1).length,
|
|
|
+ alarm: allDevices.value.filter((device) => Number(device.alarmStatus) === 1).length
|
|
|
+}))
|
|
|
+
|
|
|
+// ============ 地图标记图标(在线=绿、离线=灰,报警叠加红点) ============
|
|
|
+const iconsReady = ref(false)
|
|
|
+const markerImages = {
|
|
|
+ green: typeof Image !== 'undefined' ? new Image() : null,
|
|
|
+ grey: typeof Image !== 'undefined' ? new Image() : null,
|
|
|
+ red: typeof Image !== 'undefined' ? new Image() : null
|
|
|
}
|
|
|
+const iconCache = new Map()
|
|
|
|
|
|
-// ==================== 筛选后的设备 ====================
|
|
|
-function getFilteredDevices() {
|
|
|
- return allDevices.value.filter(d => {
|
|
|
- if (!typeFilter.value.includes(d.typeId)) return false
|
|
|
- if (!statusFilter.value.includes(d.status)) return false
|
|
|
- return true
|
|
|
+function loadMarkerImages() {
|
|
|
+ const entries = [
|
|
|
+ ['green', gisPointStatusIcons.normal],
|
|
|
+ ['grey', gisPointStatusIcons.offline],
|
|
|
+ ['red', gisPointStatusIcons.alarm]
|
|
|
+ ]
|
|
|
+ let pending = entries.length
|
|
|
+ if (!pending) {
|
|
|
+ iconsReady.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const done = () => {
|
|
|
+ pending -= 1
|
|
|
+ if (pending <= 0) iconsReady.value = true
|
|
|
+ }
|
|
|
+ entries.forEach(([key, url]) => {
|
|
|
+ const img = markerImages[key]
|
|
|
+ if (!img) return done()
|
|
|
+ img.onload = done
|
|
|
+ img.onerror = done
|
|
|
+ img.src = url
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-// ==================== 百度地图 ====================
|
|
|
-let mapInstance = null
|
|
|
-const markerMap = new Map()
|
|
|
-const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
|
|
|
+function composeAlarmIcon(online) {
|
|
|
+ const base = online ? markerImages.green : markerImages.grey
|
|
|
+ const red = markerImages.red
|
|
|
+ if (!base || !red || !base.complete || !red.complete) {
|
|
|
+ return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ }
|
|
|
+ const size = 32
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = size
|
|
|
+ canvas.height = size
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ ctx.drawImage(base, 0, 0, size, size)
|
|
|
+ const badge = Math.round(size * 0.5)
|
|
|
+ ctx.drawImage(red, size - badge + 2, 1, badge, badge)
|
|
|
+ return canvas.toDataURL()
|
|
|
+}
|
|
|
|
|
|
-/** 业务坐标(WGS84)→ 百度坐标点 */
|
|
|
-function toBdPoint(lng, lat) {
|
|
|
- const [bdLng, bdLat] = convertCoord(Number(lng), Number(lat))
|
|
|
- return new BMapGL.Point(bdLng, bdLat)
|
|
|
+function getDeviceIcon(device) {
|
|
|
+ const online = Number(device.onlineStatus) === 1
|
|
|
+ const alarm = Number(device.alarmStatus) === 1
|
|
|
+ if (!alarm) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ if (!iconsReady.value) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ const key = online ? 'online' : 'offline'
|
|
|
+ if (!iconCache.has(key)) iconCache.set(key, composeAlarmIcon(online))
|
|
|
+ return iconCache.get(key)
|
|
|
}
|
|
|
|
|
|
-function initMap() {
|
|
|
- const container = document.getElementById('manholeStateMap')
|
|
|
- if (!container || typeof BMapGL === 'undefined') {
|
|
|
- console.warn('BMapGL 未加载或容器不存在')
|
|
|
- return
|
|
|
- }
|
|
|
- try {
|
|
|
- mapInstance = new BMapGL.Map('manholeStateMap')
|
|
|
- mapInstance.centerAndZoom(toBdPoint(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
|
|
|
- mapInstance.enableScrollWheelZoom(true)
|
|
|
- } catch (e) {
|
|
|
- console.error('百度地图初始化失败:', e)
|
|
|
- }
|
|
|
+// ============ 图例状态筛选 ============
|
|
|
+const legendVisible = reactive({ online: true, offline: true, alarm: true })
|
|
|
+
|
|
|
+function toggleLegend(key) {
|
|
|
+ legendVisible[key] = !legendVisible[key]
|
|
|
}
|
|
|
|
|
|
-function getStatusColor(status) {
|
|
|
- if (status === 'alert') return '#F56C6C'
|
|
|
- if (status === 'offline') return '#909399'
|
|
|
- return '#67C23A'
|
|
|
+function isDeviceVisible(device) {
|
|
|
+ const online = Number(device.onlineStatus) === 1
|
|
|
+ const alarm = Number(device.alarmStatus) === 1
|
|
|
+ if (online && !legendVisible.online) return false
|
|
|
+ if (!online && !legendVisible.offline) return false
|
|
|
+ if (alarm && !legendVisible.alarm) return false
|
|
|
+ return true
|
|
|
}
|
|
|
|
|
|
-function addMapMarker(device) {
|
|
|
- if (!mapInstance || device.lng == null || device.lat == null) return
|
|
|
- const color = getStatusColor(device.status)
|
|
|
- const bPoint = toBdPoint(device.lng, device.lat)
|
|
|
- const html = `<div style="text-align:center;cursor:pointer;">
|
|
|
- <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
|
|
|
- ${device.name}
|
|
|
- </div>
|
|
|
- <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
|
|
|
- </div>`
|
|
|
- const label = new BMapGL.Label(html, {
|
|
|
- position: bPoint,
|
|
|
- offset: new BMapGL.Size(-30, -50)
|
|
|
- })
|
|
|
- label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
|
|
|
- label.addEventListener('click', () => selectDevice(device))
|
|
|
- mapInstance.addOverlay(label)
|
|
|
- markerMap.set(device.id, label)
|
|
|
+const mapPoints = computed(() => {
|
|
|
+ void iconsReady.value
|
|
|
+ void legendVisible.online
|
|
|
+ void legendVisible.offline
|
|
|
+ void legendVisible.alarm
|
|
|
+ return allDevices.value
|
|
|
+ .filter(isDeviceVisible)
|
|
|
+ .map((device) => ({
|
|
|
+ ...device,
|
|
|
+ lng: device._longitude,
|
|
|
+ lat: device._latitude,
|
|
|
+ name: device.equipmentName || device.equipmentCode,
|
|
|
+ iconUrl: getDeviceIcon(device)
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+// ============ 采集记录列(样式对齐 remoteControl.vue 采集记录) ============
|
|
|
+const recordColumns = [
|
|
|
+ { key: 'createTime', label: '采集时间', minWidth: 158 },
|
|
|
+ { key: 'tiltAngle', label: '倾斜角度(°)', minWidth: 104 },
|
|
|
+ { key: 'waterInfiltrationAlarmStatus', label: '水浸报警状态', minWidth: 104 },
|
|
|
+ { key: 'waterLevelAlarmStatus', label: '水位报警状态', minWidth: 104 },
|
|
|
+ { key: 'temperatureValue', label: '温度(℃)', minWidth: 104 },
|
|
|
+ { key: 'batteryLevel', label: '电池电量(%)', minWidth: 104 },
|
|
|
+ { key: 'signalStrength', label: '信号强度(%)', minWidth: 104 }
|
|
|
+]
|
|
|
+const statusKeys = ['waterInfiltrationAlarmStatus', 'waterLevelAlarmStatus']
|
|
|
+function isStatusKey(key) {
|
|
|
+ return statusKeys.includes(key)
|
|
|
+}
|
|
|
+const pagedRecordRows = computed(() => {
|
|
|
+ const start = (recordPage.value - 1) * recordPageSize
|
|
|
+ return recordRows.value.slice(start, start + recordPageSize)
|
|
|
+})
|
|
|
+
|
|
|
+// ============ 数据加载 ============
|
|
|
+function normalizeEquipment(row) {
|
|
|
+ return {
|
|
|
+ equipmentId: row.equipmentId,
|
|
|
+ equipmentCode: row.equipmentCode,
|
|
|
+ equipmentName: row.equipmentName,
|
|
|
+ equipmentTypeId: row.equipmentTypeId,
|
|
|
+ equipmentTypeName: row.equipmentTypeName,
|
|
|
+ onlineStatus: row.onlineStatus,
|
|
|
+ alarmStatus: row.alarmStatus,
|
|
|
+ _longitude: row.longitude != null ? Number(row.longitude) : null,
|
|
|
+ _latitude: row.latitude != null ? Number(row.latitude) : null
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function refreshMarkers() {
|
|
|
- if (!mapInstance) return
|
|
|
- markerMap.forEach(label => mapInstance.removeOverlay(label))
|
|
|
- markerMap.clear()
|
|
|
- getFilteredDevices().forEach(device => addMapMarker(device))
|
|
|
+async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ mapError.value = ''
|
|
|
+ try {
|
|
|
+ const allResponse = await getEquipmentGisPoints({ rootTypeId: ROOT_TYPE_ID })
|
|
|
+ const list = allResponse?.data ?? allResponse
|
|
|
+ allDevices.value = (Array.isArray(list) ? list : []).map(normalizeEquipment)
|
|
|
+ await nextTick()
|
|
|
+ gisMapRef.value?.fitBounds()
|
|
|
+ } catch (error) {
|
|
|
+ mapError.value = '监测设备数据加载失败,请点击右上角“刷新”重试'
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// ==================== 选中设备 & 详情 ====================
|
|
|
-async function selectDevice(device) {
|
|
|
- selectedDevice.value = device
|
|
|
- drawerVisible.value = true
|
|
|
- relDevices.value = []
|
|
|
+async function openDetail(equipmentId) {
|
|
|
+ if (!equipmentId) return
|
|
|
+ const device = allDevices.value.find((d) => d.equipmentId === equipmentId)
|
|
|
+ if (!device) return
|
|
|
+ currentDevice.value = device
|
|
|
+ detailVisible.value = true
|
|
|
detailLoading.value = true
|
|
|
- if (mapInstance && device.lng && device.lat) {
|
|
|
- mapInstance.centerAndZoom(toBdPoint(device.lng, device.lat), 16)
|
|
|
- }
|
|
|
- highlightMarker(device.id)
|
|
|
+ recordRows.value = []
|
|
|
+ recordPage.value = 1
|
|
|
try {
|
|
|
- const res = await getManholeRelDeviceData(device.id)
|
|
|
- relDevices.value = res.data || []
|
|
|
+ const res = await getManholeDataPage({ imeiCardNumber: device.equipmentCode, pageNum: 1, pageSize: 50 })
|
|
|
+ const page = res.data || {}
|
|
|
+ recordRows.value = page.records || []
|
|
|
} catch (e) {
|
|
|
- ElMessage.error('关联设备数据加载失败')
|
|
|
+ recordRows.value = []
|
|
|
} finally {
|
|
|
detailLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function highlightMarker(id) {
|
|
|
- markerMap.forEach((label, key) => {
|
|
|
- const dom = label.getContentContainer ? label.getContentContainer() : null
|
|
|
- if (dom) {
|
|
|
- const isSelected = key === id
|
|
|
- dom.style.zIndex = isSelected ? '999' : '10'
|
|
|
- const title = dom.querySelector('div')
|
|
|
- if (title) {
|
|
|
- title.style.transform = isSelected ? 'scale(1.15)' : 'scale(1)'
|
|
|
- title.style.transition = 'transform 0.2s'
|
|
|
- title.style.boxShadow = isSelected ? '0 0 0 2px #409eff' : '0 1px 4px rgba(0,0,0,0.3)'
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 辅助方法 ====================
|
|
|
-function isTiltAlert(manholeData) {
|
|
|
- const tilt = Number(manholeData?.tiltAngle)
|
|
|
- const threshold = Number(manholeData?.angleAlarmThreshold)
|
|
|
- return threshold > 0 && tilt > threshold
|
|
|
-}
|
|
|
-function getStatusText(status) {
|
|
|
- return { normal: '正常', alert: '告警', offline: '离线' }[status] || status
|
|
|
-}
|
|
|
-function getStatusTagType(status) {
|
|
|
- return { normal: 'success', alert: 'danger', offline: 'info' }[status] || ''
|
|
|
-}
|
|
|
-
|
|
|
-const handleResize = () => { mapInstance && mapInstance.resize && mapInstance.resize() }
|
|
|
-
|
|
|
-// ==================== 生命周期 ====================
|
|
|
-onMounted(async () => {
|
|
|
- await nextTick()
|
|
|
- initMap()
|
|
|
- window.addEventListener('resize', handleResize)
|
|
|
+onMounted(() => {
|
|
|
+ loadMarkerImages()
|
|
|
loadData()
|
|
|
})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- window.removeEventListener('resize', handleResize)
|
|
|
- if (mapInstance) {
|
|
|
- mapInstance.clearOverlays && mapInstance.clearOverlays()
|
|
|
- mapInstance = null
|
|
|
- }
|
|
|
- markerMap.clear()
|
|
|
-})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.gis-monitor-container {
|
|
|
- background: #eef2f0;
|
|
|
- height: 100vh;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
- font-family: 'Segoe UI', 'PingFang SC', Roboto, sans-serif;
|
|
|
+.gis-page {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f9f9f9;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
}
|
|
|
-.header-panel {
|
|
|
- background: white;
|
|
|
+
|
|
|
+/* 页头 */
|
|
|
+.page-head {
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
justify-content: space-between;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+.page-head-titles {
|
|
|
+ display: flex;
|
|
|
align-items: center;
|
|
|
- padding: 8px 24px;
|
|
|
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
|
- z-index: 10;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+.page-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0a1628;
|
|
|
+}
|
|
|
+.page-title-icon {
|
|
|
+ color: #014db2;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+/* KPI */
|
|
|
+.kpi-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
-.title-logo { display: flex; align-items: center; gap: 12px; }
|
|
|
-.title {
|
|
|
- font-size: 1.3rem; font-weight: 600;
|
|
|
- background: linear-gradient(135deg, #2e7d32, #1b5e20);
|
|
|
- -webkit-background-clip: text; background-clip: text; color: transparent;
|
|
|
+.kpi {
|
|
|
+ flex: 1 1 0;
|
|
|
+ min-width: 0;
|
|
|
+ border-radius: 12px;
|
|
|
}
|
|
|
-.version { background: #e9f5e9; padding: 2px 8px; border-radius: 20px; font-size: 12px; }
|
|
|
-.stats-badge { display: flex; gap: 16px; align-items: center; font-size: 13px; }
|
|
|
-.stat-badge-item { display: flex; align-items: center; gap: 4px; }
|
|
|
-.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
|
-.dot.online { background: #67c23a; }
|
|
|
-.dot.alert { background: #f56c6c; }
|
|
|
-.dot.offline { background: #909399; }
|
|
|
-.main-layout { display: flex; flex: 1; overflow: hidden; gap: 12px; padding: 12px; }
|
|
|
-.layer-panel {
|
|
|
- width: 240px; background: white; border-radius: 16px; padding: 16px;
|
|
|
- box-shadow: 0 4px 12px rgba(0,0,0,0.05); display: flex; flex-direction: column;
|
|
|
+.kpi :deep(.el-card__body) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 20px;
|
|
|
}
|
|
|
-.layer-header {
|
|
|
- font-weight: bold; font-size: 15px; margin-bottom: 16px;
|
|
|
- display: flex; align-items: center; gap: 8px;
|
|
|
+.kpi-icon {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ flex: 0 0 44px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 22px;
|
|
|
}
|
|
|
-.layer-section { margin-bottom: 16px; }
|
|
|
-.layer-section-title { font-size: 13px; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
|
|
-.legend-box { margin-top: auto; border-top: 1px solid #e9ecef; padding-top: 12px; }
|
|
|
-.legend-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
|
|
|
-.legend-item { font-size: 12px; color: #606266; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
|
|
|
-.legend-icon { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
|
|
|
-.normal-icon { background: #67c23a; }
|
|
|
-.alert-icon { background: #f56c6c; }
|
|
|
-.offline-icon { background: #909399; }
|
|
|
-.gis-stage {
|
|
|
- flex: 1; background: white; border-radius: 16px;
|
|
|
- display: flex; flex-direction: column; overflow: hidden;
|
|
|
- box-shadow: 0 6px 14px rgba(0,0,0,0.08);
|
|
|
+.kpi-icon.total { background: #edf3fc; color: #014db2; }
|
|
|
+.kpi-icon.online { background: #e7f8f1; color: #10b981; }
|
|
|
+.kpi-icon.offline { background: #fef4e2; color: #f59e0b; }
|
|
|
+.kpi-icon.alarm { background: #fdecec; color: #ef4444; }
|
|
|
+.kpi-label { font-size: 13px; font-weight: 400; color: #6b7280; }
|
|
|
+.kpi-value { margin-top: 6px; font-size: 28px; font-weight: 700; line-height: 1; color: #0a1628; }
|
|
|
+
|
|
|
+/* 地图 */
|
|
|
+.map-card { border-radius: 12px; }
|
|
|
+.map-card :deep(.el-card__body) { padding: 12px; }
|
|
|
+.map-wrap {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100vh - 340px);
|
|
|
+ min-height: 480px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.map-legend {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ left: 12px;
|
|
|
+ z-index: 9999;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(255, 255, 255, 0.92);
|
|
|
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.12);
|
|
|
+ pointer-events: auto;
|
|
|
}
|
|
|
-.map-toolbar {
|
|
|
- padding: 10px 16px; display: flex; justify-content: space-between;
|
|
|
- border-bottom: 1px solid #f0f0f0; background: white; z-index: 2;
|
|
|
+.legend-title { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: #94a3b8; }
|
|
|
+.legend-row {
|
|
|
+ display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
|
|
|
+ padding: 0; border: none; background: transparent; font: inherit;
|
|
|
+ font-size: 13px; color: #4d667d; cursor: pointer; transition: opacity 0.15s, color 0.15s;
|
|
|
}
|
|
|
-.map-title { font-weight: 600; color: #303133; font-size: 14px; }
|
|
|
-.legend { display: flex; gap: 16px; font-size: 12px; align-items: center; }
|
|
|
-.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
|
|
|
-.legend-dot.online { background: #67C23A; }
|
|
|
-.legend-dot.alert { background: #F56C6C; }
|
|
|
-.legend-dot.offline { background: #909399; }
|
|
|
-.map-area { flex: 1; position: relative; min-height: 0; }
|
|
|
-.bmap-container { width: 100%; height: 100%; }
|
|
|
+.legend-row:last-child { margin-bottom: 0; }
|
|
|
+.legend-row:hover { color: #1f3b57; }
|
|
|
+.legend-row.is-off { opacity: 0.4; text-decoration: line-through; }
|
|
|
+.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
|
+.legend-dot.online { background: #2ecc71; }
|
|
|
+.legend-dot.offline { background: #95a5a6; }
|
|
|
+.legend-dot.alarm { background: #e74c3c; }
|
|
|
+.map-error { position: absolute; left: 16px; bottom: 16px; width: 360px; z-index: 2; }
|
|
|
+.map-empty { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; }
|
|
|
|
|
|
-/* 详情抽屉样式 */
|
|
|
-.metric-card {
|
|
|
- background: #f8f9fc; border-radius: 8px; padding: 10px;
|
|
|
- text-align: center; margin-bottom: 8px;
|
|
|
+/* 详情抽屉摘要 */
|
|
|
+.dlg-summary {
|
|
|
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
|
+ padding: 14px; border-radius: 8px; background: #f5f9ff;
|
|
|
+}
|
|
|
+.summary-name { font-size: 15px; font-weight: 500; color: #014db2; }
|
|
|
+.summary-code { font-size: 12px; font-weight: 400; color: #6b7280; }
|
|
|
+.summary-tag {
|
|
|
+ display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px;
|
|
|
+ background: #fff; font-size: 11px; font-weight: 500; color: #014db2;
|
|
|
}
|
|
|
-.metric-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
|
|
|
-.metric-value { font-size: 18px; font-weight: bold; color: #303133; }
|
|
|
-.metric-alert { color: #f56c6c; }
|
|
|
-.metric-warn { color: #e6a23c; }
|
|
|
-.metric-sub { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
|
|
|
-.upload-time { font-size: 12px; color: #909399; margin-top: 8px; text-align: right; }
|
|
|
-.empty-rel { text-align: center; color: #909399; padding: 20px; font-size: 13px; }
|
|
|
-.rel-device-list { display: flex; flex-direction: column; gap: 12px; }
|
|
|
-.rel-device-card {
|
|
|
- background: #f8f9fc; border-radius: 8px; padding: 12px;
|
|
|
- border: 1px solid #ebeef5;
|
|
|
+.form-section-title {
|
|
|
+ margin: 8px 0 12px; padding-left: 10px; border-left: 3px solid #014db2;
|
|
|
+ color: #1f2937; font-size: 14px; font-weight: 600;
|
|
|
}
|
|
|
-.rel-device-header {
|
|
|
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;
|
|
|
+.detail-section-title { margin-top: 18px; }
|
|
|
+.status-group { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
|
+.badge { display: flex; align-items: center; gap: 5px; }
|
|
|
+.badge-dot { width: 6px; height: 6px; border-radius: 3px; flex-shrink: 0; }
|
|
|
+.badge-text { font-size: 12px; font-weight: 500; }
|
|
|
+.badge.online .badge-dot { background: #10b981; }
|
|
|
+.badge.online .badge-text { color: #10b981; }
|
|
|
+.badge.offline .badge-dot { background: #f59e0b; }
|
|
|
+.badge.offline .badge-text { color: #f59e0b; }
|
|
|
+.badge.alarm .badge-text { color: #ef4444; }
|
|
|
+.badge-icon { font-size: 13px; color: #ef4444; }
|
|
|
+
|
|
|
+/* 采集记录表格(对齐 remoteControl.vue) */
|
|
|
+.hist-wrap { overflow: auto; border-radius: 8px; background: #fff; }
|
|
|
+.hist-wrap :deep(.el-table) { font-size: 12px; border: 1px solid #e3e9f2; border-radius: 8px; }
|
|
|
+.hist-wrap :deep(.el-table th.el-table__cell) { background: #f7f9fc; color: #5e6d82; font-weight: 600; }
|
|
|
+.hist-wrap :deep(.el-table td.el-table__cell) { padding: 6px 0; border-bottom: 1px solid #eef2f7; }
|
|
|
+.hist-wrap :deep(.el-table .cell) { padding: 0 10px; line-height: 1.5; }
|
|
|
+.hist-wrap :deep(.el-table td.el-table__cell .cell) { white-space: nowrap; }
|
|
|
+.hist-wrap :deep(.el-table__header th.el-table__cell) { border-bottom: 1px solid #e3e9f2; }
|
|
|
+.hist-wrap :deep(.el-table::before),
|
|
|
+.hist-wrap :deep(.el-table::after),
|
|
|
+.hist-wrap :deep(.el-table__inner-wrapper::before),
|
|
|
+.hist-wrap :deep(.el-table__inner-wrapper::after) { display: none; }
|
|
|
+.hist-wrap :deep(.el-table .el-table__row:last-child td.el-table__cell) { border-bottom: none; }
|
|
|
+.rec-empty { color: #9aa7b8; font-size: 12px; padding: 28px 0; }
|
|
|
+.cell-alarm { color: #ef4444; font-weight: 600; }
|
|
|
+.rec-pager { margin-top: 10px; justify-content: flex-end; display: flex; }
|
|
|
+
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .kpi-row { flex-wrap: wrap; }
|
|
|
+ .kpi { flex: 1 1 45%; }
|
|
|
+ .map-wrap { height: 520px; }
|
|
|
}
|
|
|
-.rel-device-name { font-weight: 600; font-size: 13px; }
|
|
|
-.rel-device-location { font-size: 12px; color: #606266; display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
|
|
|
-.rel-metrics { margin-top: 4px; }
|
|
|
-.rel-metric {
|
|
|
- text-align: center; background: white; border-radius: 6px; padding: 6px;
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .kpi { flex: 1 1 100%; }
|
|
|
+ .page-head { flex-direction: column; align-items: flex-start; gap: 12px; }
|
|
|
}
|
|
|
-.rm-label { display: block; font-size: 11px; color: #909399; }
|
|
|
-.rm-value { display: block; font-size: 14px; font-weight: 600; color: #303133; margin-top: 2px; }
|
|
|
-.rel-no-data { font-size: 12px; color: #c0c4cc; text-align: center; padding: 8px; }
|
|
|
</style>
|