|
|
@@ -1,188 +1,236 @@
|
|
|
<template>
|
|
|
<div class="gis-page">
|
|
|
- <!-- 左侧浮动面板 -->
|
|
|
- <div class="side-panel">
|
|
|
+ <!-- ==================== 左侧浮动面板 ==================== -->
|
|
|
+ <aside class="side-panel">
|
|
|
<div class="panel-header">
|
|
|
- <span class="panel-title">历史预警分布</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 等级筛选 -->
|
|
|
- <div class="panel-section">
|
|
|
- <div class="section-label">等级筛选</div>
|
|
|
- <div class="layer-list">
|
|
|
- <div class="layer-item">
|
|
|
- <el-checkbox v-model="levelChecked.severe" @change="onLevelChange">
|
|
|
- <span class="level-dot" style="background:#f56c6c"></span> 严重
|
|
|
- </el-checkbox>
|
|
|
- </div>
|
|
|
- <div class="layer-item">
|
|
|
- <el-checkbox v-model="levelChecked.important" @change="onLevelChange">
|
|
|
- <span class="level-dot" style="background:#e6a23c"></span> 重要
|
|
|
- </el-checkbox>
|
|
|
- </div>
|
|
|
- <div class="layer-item">
|
|
|
- <el-checkbox v-model="levelChecked.normal" @change="onLevelChange">
|
|
|
- <span class="level-dot" style="background:#409eff"></span> 一般
|
|
|
- </el-checkbox>
|
|
|
- </div>
|
|
|
+ <div class="panel-title">历史预警分布</div>
|
|
|
+ <div class="panel-sub">
|
|
|
+ 共 <b>{{ warningList.length }}</b> 条 · 已上图 <b>{{ mapPoints.length }}</b> 条
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 统计数据 -->
|
|
|
+ <!-- 概览(点击卡片按等级筛选) -->
|
|
|
<div class="panel-section">
|
|
|
- <div class="section-label">预警统计</div>
|
|
|
- <div class="stats-row">
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-value">{{ warningList.length }}</div>
|
|
|
- <div class="stat-text">总数</div>
|
|
|
+ <div class="section-label">
|
|
|
+ 预警概览
|
|
|
+ <span class="section-hint">点击卡片筛选等级</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-grid">
|
|
|
+ <div class="stat-card" :class="{ active: levelFilter === 'all' }" @click="setLevel('all')">
|
|
|
+ <div class="stat-value">{{ rawList.length }}</div>
|
|
|
+ <div class="stat-text">全部</div>
|
|
|
</div>
|
|
|
- <div class="stat-card stat-severe">
|
|
|
+ <div class="stat-card stat-severe" :class="{ active: levelFilter === 'severe' }" @click="setLevel('severe')">
|
|
|
<div class="stat-value">{{ severeCount }}</div>
|
|
|
<div class="stat-text">严重</div>
|
|
|
</div>
|
|
|
- <div class="stat-card stat-important">
|
|
|
+ <div class="stat-card stat-important" :class="{ active: levelFilter === 'important' }"
|
|
|
+ @click="setLevel('important')">
|
|
|
<div class="stat-value">{{ importantCount }}</div>
|
|
|
<div class="stat-text">重要</div>
|
|
|
</div>
|
|
|
- <div class="stat-card stat-normal">
|
|
|
+ <div class="stat-card stat-normal" :class="{ active: levelFilter === 'normal' }" @click="setLevel('normal')">
|
|
|
<div class="stat-value">{{ normalCount }}</div>
|
|
|
<div class="stat-text">一般</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 搜索框 -->
|
|
|
+ <!-- 筛选 -->
|
|
|
<div class="panel-section">
|
|
|
- <div class="section-label">快速定位</div>
|
|
|
- <el-input
|
|
|
- v-model="searchKey"
|
|
|
- placeholder="输入设备名称搜索"
|
|
|
- clearable
|
|
|
- prefix-icon="Search"
|
|
|
- @input="onSearch"
|
|
|
- />
|
|
|
- <div class="search-results" v-if="searchResults.length > 0">
|
|
|
- <div
|
|
|
- class="search-item"
|
|
|
- v-for="item in searchResults"
|
|
|
- :key="item.id"
|
|
|
- @click="locatePoint(item)"
|
|
|
- >
|
|
|
- <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
|
|
|
- <span class="search-name">{{ item.name }}</span>
|
|
|
- <span class="search-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
|
|
|
- </div>
|
|
|
+ <div class="section-label">
|
|
|
+ 筛选条件
|
|
|
+ <span class="link-btn" v-if="typeFilter || statusFilter || levelFilter !== 'all'" @click="resetFilter">重置</span>
|
|
|
+ </div>
|
|
|
+ <div class="filter-row">
|
|
|
+ <el-select v-model="typeFilter" placeholder="全部类型" clearable filterable size="small" style="flex:1">
|
|
|
+ <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="statusFilter" placeholder="全部状态" clearable size="small"
|
|
|
+ style="width:106px;flex-shrink:0">
|
|
|
+ <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 地图容器 -->
|
|
|
- <div id="lsyjGISMap" class="map-container"></div>
|
|
|
-
|
|
|
- <!-- 详情弹窗 -->
|
|
|
- <el-dialog
|
|
|
- v-model="detailVisible"
|
|
|
- :title="currentItem.name"
|
|
|
- width="480px"
|
|
|
- :close-on-click-modal="false"
|
|
|
- class="detail-dialog"
|
|
|
- >
|
|
|
- <div class="detail-content">
|
|
|
- <div class="detail-header">
|
|
|
- <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">
|
|
|
- {{ currentItem.level }}
|
|
|
- </el-tag>
|
|
|
- <span class="detail-name">{{ currentItem.name }}</span>
|
|
|
+ <!-- 预警清单 -->
|
|
|
+ <div class="panel-section list-section">
|
|
|
+ <div class="section-label">
|
|
|
+ 归档清单
|
|
|
+ <span class="section-hint">{{ displayList.length }} 条</span>
|
|
|
</div>
|
|
|
- <div class="detail-info">
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">预警编号</span>
|
|
|
- <span class="info-value">{{ currentItem.warningNo }}</span>
|
|
|
- </div>
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">预警级别</span>
|
|
|
- <span class="info-value">
|
|
|
- <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">{{ currentItem.level }}</el-tag>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">预警时间</span>
|
|
|
- <span class="info-value">{{ currentItem.createTime }}</span>
|
|
|
- </div>
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">位置</span>
|
|
|
- <span class="info-value">{{ currentItem.location }}</span>
|
|
|
- </div>
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">处理状态</span>
|
|
|
- <span class="info-value">
|
|
|
- <el-tag :type="getStatusTagType(currentItem.statusKey)" size="small">{{ currentItem.statusText }}</el-tag>
|
|
|
- </span>
|
|
|
+ <el-input v-model="searchKey" placeholder="搜索名称 / 位置" clearable size="small" :prefix-icon="Search" />
|
|
|
+ <div class="warning-list">
|
|
|
+ <div class="warning-item" v-for="item in displayList" :key="item.id" :class="{ 'no-coord': !item.lng }"
|
|
|
+ @click="locate(item)">
|
|
|
+ <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
|
|
|
+ <div class="wi-main">
|
|
|
+ <div class="wi-name" :title="item.name">{{ item.name }}</div>
|
|
|
+ <div class="wi-sub">归档于 {{ formatTime(item.archiveTime) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="wi-right">
|
|
|
+ <span class="wi-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
|
|
|
+ <span class="wi-status" :class="`st-${item.statusKey}`">{{ item.statusText }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <div class="list-empty" v-if="!displayList.length">暂无符合条件的历史预警</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </el-dialog>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <!-- ==================== 地图容器 ==================== -->
|
|
|
+ <div id="lsyjGISMap" class="map-container"></div>
|
|
|
+
|
|
|
+ <!-- 地图工具条 -->
|
|
|
+ <div class="map-tools">
|
|
|
+ <el-tooltip content="回到预警范围" placement="left">
|
|
|
+ <el-button circle @click="fitView"><el-icon><Aim /></el-icon></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ==================== 详情抽屉 ==================== -->
|
|
|
+ <WarningGisDetail v-model="detailVisible" :record="currentRecord" mode="history" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="LsyjglGIS">
|
|
|
-import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
|
|
-import { Search } from '@element-plus/icons-vue'
|
|
|
+import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
|
|
+import { Search, Aim } from '@element-plus/icons-vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import locationIcon from '@/assets/images/location.png'
|
|
|
import { getWarningHistoryPage } from '@/api/drainage'
|
|
|
+import { listWarningTypes } from '@/api/warning/list'
|
|
|
+import WarningGisDetail from './components/WarningGisDetail.vue'
|
|
|
|
|
|
-// ==================== 等级筛选 ====================
|
|
|
-const levelChecked = reactive({ severe: true, important: true, normal: true })
|
|
|
-
|
|
|
-// ==================== 历史预警数据(从API加载)====================
|
|
|
+// ==================== 数据 ====================
|
|
|
+const rawList = ref([])
|
|
|
+/** 归一化后的全部预警(含无坐标的,清单里照样能看详情) */
|
|
|
const warningList = ref([])
|
|
|
|
|
|
-// 预警级别/状态数字→文字映射
|
|
|
+/** 能上图的点位 */
|
|
|
+const mapPoints = computed(() => warningList.value.filter((p) => p.lng && p.lat))
|
|
|
+
|
|
|
const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
|
|
|
-const statusMap = { HANDLED: '已处置', CLOSED: '已解除', DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中' }
|
|
|
+const levelKeyMap = { 1: 'severe', 2: 'important', 3: 'normal' }
|
|
|
+const statusMap = {
|
|
|
+ DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收',
|
|
|
+ PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除'
|
|
|
+}
|
|
|
+
|
|
|
+const typeOptions = ref([])
|
|
|
+const statusOptions = ref([])
|
|
|
+const warningTypeMap = ref(new Map())
|
|
|
+
|
|
|
+async function loadWarningTypes() {
|
|
|
+ try {
|
|
|
+ const res = await listWarningTypes()
|
|
|
+ const list = res?.data || res?.rows || []
|
|
|
+ const map = new Map()
|
|
|
+ if (Array.isArray(list)) {
|
|
|
+ list.forEach((item) => {
|
|
|
+ if (item?.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ warningTypeMap.value = map
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('加载预警类型配置失败', error)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-// 加载历史预警数据
|
|
|
async function loadData() {
|
|
|
try {
|
|
|
- const res = await getWarningHistoryPage(1, 1000, {
|
|
|
- historyOnly: true,
|
|
|
- equipmentType: 'drainage'
|
|
|
- })
|
|
|
+ const [res] = await Promise.all([
|
|
|
+ getWarningHistoryPage(1, 1000, { historyOnly: true, equipmentType: 'drainage' }),
|
|
|
+ loadWarningTypes()
|
|
|
+ ])
|
|
|
const pageData = res.code !== undefined ? res.data : res
|
|
|
const records = pageData.records || []
|
|
|
- const hasCoordCount = records.filter(w => w.longitude && w.latitude).length
|
|
|
- console.log('[历史预警GIS] API返回记录数:', records.length, '有坐标记录数:', hasCoordCount)
|
|
|
- if (records.length > 0 && hasCoordCount === 0) {
|
|
|
- console.warn('[历史预警GIS] 所有预警均无经纬度坐标,请检查发布预警时设备是否有定位数据')
|
|
|
- }
|
|
|
- const noCoordRecords = records.filter(w => !w.longitude || !w.latitude)
|
|
|
- if (noCoordRecords.length > 0) {
|
|
|
- console.warn('[历史预警GIS] 无坐标预警(将被过滤):', noCoordRecords.map(w => ({ id: w.id, name: w.equipmentName, longitude: w.longitude, latitude: w.latitude })))
|
|
|
+ rawList.value = records
|
|
|
+ warningList.value = records.map(normalize)
|
|
|
+
|
|
|
+ // 类型下拉:按记录里实际出现的类型构建
|
|
|
+ const types = [...new Set(records.map((r) => r.warningType).filter(Boolean))]
|
|
|
+ typeOptions.value = types.map((val) => ({
|
|
|
+ value: String(val),
|
|
|
+ label: warningTypeMap.value.get(String(val)) || String(val)
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 状态下拉
|
|
|
+ const statuses = [...new Set(records.map((r) => r.status).filter(Boolean))]
|
|
|
+ statusOptions.value = statuses.map((val) => ({ value: val, label: statusMap[val] || val }))
|
|
|
+
|
|
|
+ const noCoord = records.length - warningList.value.filter((p) => p.lng && p.lat).length
|
|
|
+ if (noCoord > 0) {
|
|
|
+ console.warn(`[历史预警GIS] ${noCoord} 条预警缺少经纬度,未上图(清单中仍可查看详情)`)
|
|
|
}
|
|
|
- warningList.value = records.map(w => ({
|
|
|
- id: w.id,
|
|
|
- name: w.equipmentName || '-',
|
|
|
- warningNo: w.warningNo || '-',
|
|
|
- deviceCode: w.deviceCode || w.warningType || '',
|
|
|
- level: levelMap[w.warningLevel] || '一般',
|
|
|
- createTime: w.createTime || '-',
|
|
|
- location: w.location || '-',
|
|
|
- statusKey: w.status || '',
|
|
|
- statusText: statusMap[w.status] || '-',
|
|
|
- lng: parseFloat(w.longitude),
|
|
|
- lat: parseFloat(w.latitude)
|
|
|
- })).filter(w => w.lng && w.lat)
|
|
|
} catch (error) {
|
|
|
console.error('加载历史预警数据失败', error)
|
|
|
ElMessage.error('加载历史预警数据失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// ==================== 统计计算 ====================
|
|
|
-const severeCount = computed(() => warningList.value.filter(v => v.level === '严重').length)
|
|
|
-const importantCount = computed(() => warningList.value.filter(v => v.level === '重要').length)
|
|
|
-const normalCount = computed(() => warningList.value.filter(v => v.level === '一般').length)
|
|
|
+function normalize(w) {
|
|
|
+ return {
|
|
|
+ id: w.id,
|
|
|
+ name: w.equipmentName || '-',
|
|
|
+ level: levelMap[w.warningLevel] || '一般',
|
|
|
+ levelKey: levelKeyMap[w.warningLevel] || 'normal',
|
|
|
+ typeValue: w.warningType ? String(w.warningType) : '',
|
|
|
+ typeText: w.warningType ? (warningTypeMap.value.get(String(w.warningType)) || String(w.warningType)) : '未分类',
|
|
|
+ statusValue: w.status || '',
|
|
|
+ statusKey: String(w.status || 'unknown').toLowerCase(),
|
|
|
+ statusText: statusMap[w.status] || '-',
|
|
|
+ createTime: w.createTime || '',
|
|
|
+ archiveTime: w.updateTime || w.publishTime || w.createTime || '',
|
|
|
+ location: w.location || '',
|
|
|
+ raw: w,
|
|
|
+ lng: parseFloat(w.longitude),
|
|
|
+ lat: parseFloat(w.latitude)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 统计 ====================
|
|
|
+const severeCount = computed(() => rawList.value.filter((v) => v.warningLevel === 1).length)
|
|
|
+const importantCount = computed(() => rawList.value.filter((v) => v.warningLevel === 2).length)
|
|
|
+const normalCount = computed(() => rawList.value.filter((v) => v.warningLevel === 3).length)
|
|
|
+
|
|
|
+// ==================== 筛选 ====================
|
|
|
+const levelFilter = ref('all')
|
|
|
+const typeFilter = ref('')
|
|
|
+const statusFilter = ref('')
|
|
|
+const searchKey = ref('')
|
|
|
+
|
|
|
+const displayList = computed(() => {
|
|
|
+ const key = searchKey.value.trim().toLowerCase()
|
|
|
+ return warningList.value.filter((item) => {
|
|
|
+ if (levelFilter.value !== 'all' && item.levelKey !== levelFilter.value) return false
|
|
|
+ if (typeFilter.value && item.typeValue !== typeFilter.value) return false
|
|
|
+ if (statusFilter.value && item.statusValue !== statusFilter.value) return false
|
|
|
+ if (key && !(item.name.toLowerCase().includes(key) || item.location.toLowerCase().includes(key))) return false
|
|
|
+ return true
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+function setLevel(key) {
|
|
|
+ levelFilter.value = levelFilter.value === key ? 'all' : key
|
|
|
+}
|
|
|
+
|
|
|
+function resetFilter() {
|
|
|
+ levelFilter.value = 'all'
|
|
|
+ typeFilter.value = ''
|
|
|
+ statusFilter.value = ''
|
|
|
+ searchKey.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+watch(displayList, () => {
|
|
|
+ const visibleIds = new Set(displayList.value.map((item) => item.id))
|
|
|
+ warningList.value.forEach((point) => {
|
|
|
+ const label = markerMap.get(point.id)
|
|
|
+ if (!label) return
|
|
|
+ if (visibleIds.has(point.id)) label.show()
|
|
|
+ else label.hide()
|
|
|
+ })
|
|
|
+})
|
|
|
|
|
|
// ==================== 工具方法 ====================
|
|
|
function getLevelColor(level) {
|
|
|
@@ -195,46 +243,12 @@ function getLevelClass(level) {
|
|
|
return map[level] || ''
|
|
|
}
|
|
|
|
|
|
-function getTagType(level) {
|
|
|
- const map = { '严重': 'danger', '重要': 'warning', '一般': '' }
|
|
|
- return map[level] || ''
|
|
|
+function formatTime(value) {
|
|
|
+ if (!value) return '-'
|
|
|
+ return String(value).replace('T', ' ').slice(5, 16)
|
|
|
}
|
|
|
|
|
|
-function getLevelKey(level) {
|
|
|
- const map = { '严重': 'severe', '重要': 'important', '一般': 'normal' }
|
|
|
- return map[level] || 'normal'
|
|
|
-}
|
|
|
-
|
|
|
-function getStatusTagType(status) {
|
|
|
- const map = { HANDLED: 'success', CLOSED: '', DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning' }
|
|
|
- return map[status] || 'info'
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 搜索功能 ====================
|
|
|
-const searchKey = ref('')
|
|
|
-const searchResults = ref([])
|
|
|
-
|
|
|
-function onSearch() {
|
|
|
- if (!searchKey.value.trim()) {
|
|
|
- searchResults.value = []
|
|
|
- return
|
|
|
- }
|
|
|
- const key = searchKey.value.trim().toLowerCase()
|
|
|
- searchResults.value = warningList.value.filter(v =>
|
|
|
- v.name.toLowerCase().includes(key) || v.location.toLowerCase().includes(key)
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function locatePoint(item) {
|
|
|
- if (mapInstance) {
|
|
|
- mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
|
|
|
- openDetail(item)
|
|
|
- }
|
|
|
- searchResults.value = []
|
|
|
- searchKey.value = ''
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 地图相关 ====================
|
|
|
+// ==================== 地图 ====================
|
|
|
let mapInstance = null
|
|
|
const markerMap = new Map()
|
|
|
|
|
|
@@ -244,22 +258,13 @@ function initMap() {
|
|
|
console.warn('BMapGL 未加载或容器不存在')
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
try {
|
|
|
mapInstance = new BMapGL.Map('lsyjGISMap')
|
|
|
- const centerPoint = new BMapGL.Point(110.393, 28.452)
|
|
|
- mapInstance.centerAndZoom(centerPoint, 15)
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 15)
|
|
|
mapInstance.enableScrollWheelZoom(true)
|
|
|
|
|
|
- warningList.value.forEach(point => {
|
|
|
- addMapMarker(point)
|
|
|
- })
|
|
|
-
|
|
|
- // 默认定位到第一个预警点位
|
|
|
- if (warningList.value.length > 0) {
|
|
|
- const firstPoint = warningList.value[0]
|
|
|
- mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
|
|
|
- }
|
|
|
+ mapPoints.value.forEach(addMapMarker)
|
|
|
+ fitView()
|
|
|
} catch (e) {
|
|
|
console.error('百度地图初始化失败:', e)
|
|
|
}
|
|
|
@@ -268,7 +273,6 @@ function initMap() {
|
|
|
function addMapMarker(point) {
|
|
|
const color = getLevelColor(point.level)
|
|
|
const bPoint = new BMapGL.Point(point.lng, point.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);">
|
|
|
${point.name}
|
|
|
@@ -280,49 +284,48 @@ function addMapMarker(point) {
|
|
|
position: bPoint,
|
|
|
offset: new BMapGL.Size(-30, -50)
|
|
|
})
|
|
|
- label.setStyle({
|
|
|
- border: 'none',
|
|
|
- background: 'transparent',
|
|
|
- padding: '0',
|
|
|
- zIndex: '10'
|
|
|
- })
|
|
|
+ label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
|
|
|
mapInstance.addOverlay(label)
|
|
|
markerMap.set(point.id, label)
|
|
|
|
|
|
- label.addEventListener('click', function () {
|
|
|
- openDetail(point)
|
|
|
- })
|
|
|
+ label.addEventListener('click', () => locate(point))
|
|
|
}
|
|
|
|
|
|
-// ==================== 等级筛选 ====================
|
|
|
-function onLevelChange() {
|
|
|
- warningList.value.forEach(point => {
|
|
|
- const label = markerMap.get(point.id)
|
|
|
- if (!label) return
|
|
|
- const key = getLevelKey(point.level)
|
|
|
- const visible = levelChecked[key]
|
|
|
- label.show()
|
|
|
- if (!visible) {
|
|
|
- label.hide()
|
|
|
- }
|
|
|
- })
|
|
|
+/** 回到能看全所有点位的视野 */
|
|
|
+function fitView() {
|
|
|
+ if (!mapInstance || !mapPoints.value.length) return
|
|
|
+ const points = mapPoints.value.map((p) => new BMapGL.Point(p.lng, p.lat))
|
|
|
+ if (points.length === 1) {
|
|
|
+ mapInstance.centerAndZoom(points[0], 16)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ mapInstance.setViewport(points)
|
|
|
}
|
|
|
|
|
|
-// ==================== 详情弹窗 ====================
|
|
|
+function locate(item) {
|
|
|
+ if (!item) return
|
|
|
+ if (mapInstance && item.lng && item.lat) {
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
|
|
|
+ }
|
|
|
+ if (!item.lng || !item.lat) {
|
|
|
+ ElMessage.warning('该预警缺少经纬度坐标,无法在地图上定位')
|
|
|
+ }
|
|
|
+ openDetail(item)
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 详情 ====================
|
|
|
const detailVisible = ref(false)
|
|
|
-const currentItem = ref({})
|
|
|
+const currentRecord = ref(null)
|
|
|
|
|
|
-function openDetail(point) {
|
|
|
- currentItem.value = { ...point }
|
|
|
+function openDetail(item) {
|
|
|
+ currentRecord.value = item.raw || null
|
|
|
detailVisible.value = true
|
|
|
}
|
|
|
|
|
|
// ==================== 生命周期 ====================
|
|
|
onMounted(async () => {
|
|
|
await loadData()
|
|
|
- nextTick(() => {
|
|
|
- initMap()
|
|
|
- })
|
|
|
+ nextTick(() => initMap())
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
@@ -344,28 +347,23 @@ onMounted(async () => {
|
|
|
position: absolute;
|
|
|
top: 16px;
|
|
|
left: 16px;
|
|
|
- width: 280px;
|
|
|
- background: rgba(6, 30, 65, 0.85);
|
|
|
- border-radius: 8px;
|
|
|
+ bottom: 16px;
|
|
|
+ width: 300px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: rgba(6, 30, 65, 0.88);
|
|
|
+ border: 1px solid rgba(90, 160, 255, 0.18);
|
|
|
+ border-radius: 10px;
|
|
|
z-index: 100;
|
|
|
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
- padding: 0;
|
|
|
- max-height: calc(100% - 32px);
|
|
|
- overflow-y: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.side-panel::-webkit-scrollbar {
|
|
|
- width: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.side-panel::-webkit-scrollbar-thumb {
|
|
|
- background: rgba(255, 255, 255, 0.2);
|
|
|
- border-radius: 2px;
|
|
|
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
|
|
+ backdrop-filter: blur(6px);
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.panel-header {
|
|
|
padding: 16px 18px 12px;
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.panel-title {
|
|
|
@@ -375,88 +373,103 @@ onMounted(async () => {
|
|
|
letter-spacing: 1px;
|
|
|
}
|
|
|
|
|
|
+.panel-sub {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.55);
|
|
|
+}
|
|
|
+
|
|
|
+.panel-sub b {
|
|
|
+ color: #61c0ff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
.panel-section {
|
|
|
- padding: 14px 18px;
|
|
|
+ padding: 12px 18px;
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
-.panel-section:last-child {
|
|
|
+.panel-section.list-section {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
border-bottom: none;
|
|
|
+ padding-bottom: 14px;
|
|
|
}
|
|
|
|
|
|
.section-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
font-size: 12px;
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
+ color: rgba(255, 255, 255, 0.55);
|
|
|
margin-bottom: 10px;
|
|
|
- text-transform: uppercase;
|
|
|
letter-spacing: 1px;
|
|
|
}
|
|
|
|
|
|
-/* 等级列表 */
|
|
|
-.layer-list {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 6px;
|
|
|
-}
|
|
|
-
|
|
|
-.layer-item :deep(.el-checkbox) {
|
|
|
- color: rgba(255, 255, 255, 0.85);
|
|
|
- height: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.layer-item :deep(.el-checkbox__label) {
|
|
|
- color: rgba(255, 255, 255, 0.85);
|
|
|
- font-size: 13px;
|
|
|
- display: inline-flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
+.section-hint {
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.32);
|
|
|
+ letter-spacing: 0;
|
|
|
}
|
|
|
|
|
|
-.layer-item :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
|
|
- background: #409eff;
|
|
|
- border-color: #409eff;
|
|
|
+.link-btn {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #61c0ff;
|
|
|
+ cursor: pointer;
|
|
|
+ letter-spacing: 0;
|
|
|
}
|
|
|
|
|
|
-.level-dot {
|
|
|
- display: inline-block;
|
|
|
- width: 10px;
|
|
|
- height: 10px;
|
|
|
- border-radius: 50%;
|
|
|
- margin-right: 2px;
|
|
|
- vertical-align: middle;
|
|
|
+.link-btn:hover {
|
|
|
+ text-decoration: underline;
|
|
|
}
|
|
|
|
|
|
/* 统计卡片 */
|
|
|
-.stats-row {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
+.stat-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 6px;
|
|
|
}
|
|
|
|
|
|
.stat-card {
|
|
|
- flex: 1;
|
|
|
- background: rgba(255, 255, 255, 0.08);
|
|
|
+ background: rgba(255, 255, 255, 0.07);
|
|
|
+ border: 1px solid transparent;
|
|
|
border-radius: 6px;
|
|
|
- padding: 10px 4px;
|
|
|
+ padding: 9px 2px;
|
|
|
text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.18s;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.13);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.active {
|
|
|
+ border-color: rgba(97, 192, 255, 0.85);
|
|
|
+ background: rgba(97, 192, 255, 0.16);
|
|
|
}
|
|
|
|
|
|
.stat-card.stat-severe {
|
|
|
- background: rgba(245, 108, 108, 0.15);
|
|
|
+ background: rgba(245, 108, 108, 0.14);
|
|
|
}
|
|
|
|
|
|
.stat-card.stat-important {
|
|
|
- background: rgba(230, 162, 60, 0.15);
|
|
|
+ background: rgba(230, 162, 60, 0.14);
|
|
|
}
|
|
|
|
|
|
.stat-card.stat-normal {
|
|
|
- background: rgba(64, 158, 255, 0.15);
|
|
|
+ background: rgba(64, 158, 255, 0.14);
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
- font-size: 20px;
|
|
|
+ font-size: 19px;
|
|
|
font-weight: 700;
|
|
|
color: #fff;
|
|
|
line-height: 1.2;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
}
|
|
|
|
|
|
.stat-severe .stat-value {
|
|
|
@@ -474,17 +487,25 @@ onMounted(async () => {
|
|
|
.stat-text {
|
|
|
font-size: 11px;
|
|
|
color: rgba(255, 255, 255, 0.5);
|
|
|
- margin-top: 4px;
|
|
|
+ margin-top: 3px;
|
|
|
}
|
|
|
|
|
|
-/* 搜索 */
|
|
|
-.side-panel :deep(.el-input__wrapper) {
|
|
|
+/* 筛选行 */
|
|
|
+.filter-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 深色面板下的输入控件 */
|
|
|
+.side-panel :deep(.el-input__wrapper),
|
|
|
+.side-panel :deep(.el-select__wrapper) {
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
box-shadow: none;
|
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
}
|
|
|
|
|
|
-.side-panel :deep(.el-input__inner) {
|
|
|
+.side-panel :deep(.el-input__inner),
|
|
|
+.side-panel :deep(.el-select__placeholder) {
|
|
|
color: #fff;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
@@ -493,144 +514,150 @@ onMounted(async () => {
|
|
|
color: rgba(255, 255, 255, 0.35);
|
|
|
}
|
|
|
|
|
|
-.side-panel :deep(.el-input__prefix .el-icon) {
|
|
|
- color: rgba(255, 255, 255, 0.4);
|
|
|
+.side-panel :deep(.el-input__prefix .el-icon),
|
|
|
+.side-panel :deep(.el-select__caret) {
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
}
|
|
|
|
|
|
-.search-results {
|
|
|
+/* 预警清单 */
|
|
|
+.warning-list {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
margin-top: 8px;
|
|
|
- max-height: 200px;
|
|
|
overflow-y: auto;
|
|
|
- border-radius: 4px;
|
|
|
+ margin-right: -6px;
|
|
|
+ padding-right: 4px;
|
|
|
}
|
|
|
|
|
|
-.search-results::-webkit-scrollbar {
|
|
|
- width: 3px;
|
|
|
+.warning-list::-webkit-scrollbar {
|
|
|
+ width: 4px;
|
|
|
}
|
|
|
|
|
|
-.search-results::-webkit-scrollbar-thumb {
|
|
|
- background: rgba(255, 255, 255, 0.15);
|
|
|
+.warning-list::-webkit-scrollbar-thumb {
|
|
|
+ background: rgba(255, 255, 255, 0.18);
|
|
|
border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
-.search-item {
|
|
|
+.warning-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- padding: 7px 10px;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 8px;
|
|
|
+ border-radius: 6px;
|
|
|
cursor: pointer;
|
|
|
- border-radius: 4px;
|
|
|
transition: background 0.15s;
|
|
|
}
|
|
|
|
|
|
-.search-item:hover {
|
|
|
+.warning-item:hover {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
-.search-name {
|
|
|
- flex: 1;
|
|
|
- font-size: 12px;
|
|
|
- color: rgba(255, 255, 255, 0.85);
|
|
|
- margin-left: 6px;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
+/* 无坐标的记录仍可查看详情,视觉上弱化 */
|
|
|
+.warning-item.no-coord .wi-name,
|
|
|
+.warning-item.no-coord .wi-sub {
|
|
|
+ color: rgba(255, 255, 255, 0.42);
|
|
|
}
|
|
|
|
|
|
-.search-level {
|
|
|
- font-size: 11px;
|
|
|
- padding: 1px 6px;
|
|
|
- border-radius: 3px;
|
|
|
+.warning-item.no-coord .level-dot {
|
|
|
+ opacity: 0.4;
|
|
|
}
|
|
|
|
|
|
-.search-level.level-severe {
|
|
|
- color: #f56c6c;
|
|
|
- background: rgba(245, 108, 108, 0.15);
|
|
|
-}
|
|
|
-
|
|
|
-.search-level.level-important {
|
|
|
- color: #e6a23c;
|
|
|
- background: rgba(230, 162, 60, 0.15);
|
|
|
+.wi-main {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.search-level.level-normal {
|
|
|
- color: #409eff;
|
|
|
- background: rgba(64, 158, 255, 0.15);
|
|
|
+.wi-name {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-/* ==================== 详情弹窗样式 ==================== */
|
|
|
-.detail-dialog :deep(.el-dialog) {
|
|
|
- border-radius: 10px;
|
|
|
+.wi-sub {
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.42);
|
|
|
+ margin-top: 2px;
|
|
|
overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.detail-dialog :deep(.el-dialog__header) {
|
|
|
- background: #1a1a2e;
|
|
|
- padding: 14px 20px;
|
|
|
- margin: 0;
|
|
|
+.wi-right {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 3px;
|
|
|
}
|
|
|
|
|
|
-.detail-dialog :deep(.el-dialog__title) {
|
|
|
- color: #fff;
|
|
|
- font-size: 15px;
|
|
|
+.wi-level {
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
}
|
|
|
|
|
|
-.detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
|
|
|
- color: rgba(255, 255, 255, 0.6);
|
|
|
+.wi-status {
|
|
|
+ font-size: 10px;
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
}
|
|
|
|
|
|
-.detail-dialog :deep(.el-dialog__body) {
|
|
|
- padding: 0;
|
|
|
+.wi-status.st-handled {
|
|
|
+ color: #67c23a;
|
|
|
}
|
|
|
|
|
|
-.detail-content {
|
|
|
- padding: 20px;
|
|
|
+.wi-status.st-closed {
|
|
|
+ color: #61c0ff;
|
|
|
}
|
|
|
|
|
|
-.detail-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 16px;
|
|
|
- padding-bottom: 12px;
|
|
|
- border-bottom: 1px solid #f0f0f0;
|
|
|
+.list-empty {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.32);
|
|
|
+ padding: 24px 0;
|
|
|
}
|
|
|
|
|
|
-.detail-name {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
+.level-dot {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: inline-block;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
}
|
|
|
|
|
|
-.detail-info {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
+.wi-level.level-severe {
|
|
|
+ color: #f56c6c;
|
|
|
+ background: rgba(245, 108, 108, 0.15);
|
|
|
}
|
|
|
|
|
|
-.info-row {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- padding: 8px 0;
|
|
|
- border-bottom: 1px solid #f5f5f5;
|
|
|
+.wi-level.level-important {
|
|
|
+ color: #e6a23c;
|
|
|
+ background: rgba(230, 162, 60, 0.15);
|
|
|
}
|
|
|
|
|
|
-.info-row:last-child {
|
|
|
- border-bottom: none;
|
|
|
+.wi-level.level-normal {
|
|
|
+ color: #409eff;
|
|
|
+ background: rgba(64, 158, 255, 0.15);
|
|
|
}
|
|
|
|
|
|
-.info-label {
|
|
|
- width: 80px;
|
|
|
- flex-shrink: 0;
|
|
|
- font-size: 13px;
|
|
|
- color: #909399;
|
|
|
+/* ==================== 地图工具条 ==================== */
|
|
|
+.map-tools {
|
|
|
+ position: absolute;
|
|
|
+ top: 16px;
|
|
|
+ right: 16px;
|
|
|
+ z-index: 100;
|
|
|
}
|
|
|
|
|
|
-.info-value {
|
|
|
- font-size: 13px;
|
|
|
- color: #303133;
|
|
|
+.map-tools :deep(.el-button) {
|
|
|
+ background: rgba(6, 30, 65, 0.85);
|
|
|
+ border-color: rgba(90, 160, 255, 0.25);
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
|
|
|
-.info-value.highlight {
|
|
|
- color: #f56c6c;
|
|
|
- font-weight: 600;
|
|
|
+.map-tools :deep(.el-button:hover) {
|
|
|
+ background: rgba(30, 70, 130, 0.95);
|
|
|
+ border-color: rgba(90, 160, 255, 0.5);
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
</style>
|