一、需求分析
最近做了一个vue3+高德地图的工作,可视化监控,实时展示全国/地区范围内的设备告警情况,包括:
-
风险等级筛选(多选)
-
地区选择(省市区三级联动)
-
动态统计(告警设备总数、告警城市数等)
-
地图交互:
- 不同缩放层级显示对应行政级别的数据(省→市→区)
- 点击标记点查看详情或下钻
- 鹰眼导航辅助定位
核心挑战
- 数据动态加载:避免频繁请求,需缓存不同层级数据
- 性能优化:大量标记点(Marker)渲染时的流畅性
- 交互一致性:搜索条件变化时,地图视角与数据的同步

二、 开发流程
技术选型
技术栈 | 用途 |
---|---|
Vue3 + ElementUI | 前端框架 & UI组件库 |
高德地图JS API | 地图渲染、标记点、信息窗口 |
Pinia | 状态管理(缓存层级数据) |
因为我的需求是省市县/区的展示,不需要具体到更细一级的设备,并且hover上去需要展示众多数据,也不需要前端用indexCluster来聚合,需求上就直接用markers实现了。
关键实现步骤
-
地图初始化
- 加载高德地图,设置默认缩放级别和中心点
- 添加行政区域图层 (
AMap.DistrictLayer
)和鹰眼控件
-
数据分层管理
- 通过
Pinia
缓存省/市/区三级数据,避免重复请求 - 根据当前缩放级别动态切换数据源
- 通过
-
标记点动态渲染
- 根据告警比例动态设置标记点颜色(红/绿)
- 使用
InfoWindow
展示详情,内嵌 Vue 组件
-
搜索条件联动
- 风险等级或地区变化时,重新请求数据并更新视图
- 无数据时自动回退到默认视图
三、数据交互分析
后端返回的数据结构
css
{
"code": "000000",
"content": {
"alterCityTotal": 1,
"alterDeviceTotal": 43,
"cityTotal": 1,
"deviceTotal": 43,
"regionAlterList": [
{
"addressId": "1001,1001",
"addressName": "北京市",
"alterDeviceTotal": 125,
"city": "北京市",
"deviceTotal": 341,
"lat": 39.88125,
"layer": "city",
"lng": 116.42488,
"lnglat": [
116.42488,
39.88125
],
"province": "北京市",
"rate": "36.66"
},
{
"addressId": "1001",
"addressName": "北京市",
"alterDeviceTotal": 125,
"deviceTotal": 341,
"lat": 39.88125,
"layer": "province",
"lng": 116.42488,
"lnglat": [
116.42488,
39.88125
],
"province": "北京市",
"rate": "36.66"
},
{
"addressId": "1001,1001,4895",
"addressName": "朝阳区",
"alterDeviceTotal": 43,
"city": "北京市",
"deviceTotal": 43,
"lat": 39.93123,
"layer": "district",
"lng": 116.46234,
"lnglat": [
116.46234,
39.93123
],
"province": "北京市",
"rate": "100",
"region": "朝阳区"
}
]
},
"errors": [],
"message": "",
"metadata": {},
"status": "success"
}
前端使用
(1) 数据存储与分层管理
-
使用 Pinia 缓存不同层级的
regionAlterList
,避免重复请求。 -
通过
layer
字段区分数据层级,例如:zoom=5
(省级) → 仅渲染layer="province"
的数据zoom=10
(市级) → 仅渲染layer="city"
的数据
(2) 动态渲染策略
- 标记点生成 :遍历
regionAlterList
,根据lnglat
创建地图标记点(Marker)。 - 样式计算 :通过
rate
字段判断告警比例,动态设置标记点颜色:
(3) 统计信息联动
- 顶部统计栏(如"告警城市数")直接绑定
alterCityTotal
和cityTotal
。 - 搜索条件变化时,重新请求数据并更新统计数值。
四、 代码分析
1. 核心模块概览
代码主要分为以下几个功能模块:
- 地图初始化与配置
- 数据获取与状态管理
- 标记点(Marker)渲染与交互
- 搜索条件处理
- 组件生命周期管理
2. 逐函数解析
2.1 地图初始化相关函数
onMounted
钩子函数
php
onMounted(() => {
AMapLoader.load({
key: 'xxxxxxxx',// 去高德地图申请,建议使用企业账号
version: '2.0',
plugins: ['AMap.DistrictLayer', 'AMap.HawkEye']
})
.then((AMap) => {
// 创建地图实例
map = new AMap.Map('container', {
viewMode: '2D',
zoom: 4.7,
zooms: [4, 16],
center: [104.937478, 35.439575],
mapStyle: 'amap://styles/noramal'
});
// 添加行政区划图层
const districtLayer = new AMap.DistrictLayer.Country({
SOC: 'CHN',
depth: 3,
zIndex: 90,
styles: DISTRICT_LAYER_DEFAULT_STYLE
});
// 添加鹰眼控件
var hawkEye = new AMap.HawkEye({
isOpen: true,
position: 'RB',
mapStyle: 'amap://styles/normal'
});
map.add(districtLayer);
map.addControl(hawkEye);
initData(); // 初始化数据
setMapListener(); // 设置事件监听
})
.catch((e) => {
console.error('地图加载失败:', e);
});
});
功能:
- 异步加载高德地图JS API
- 初始化地图实例并设置默认视图(中国全境)
- 添加行政区划图层和鹰眼控件
- 调用
initData()
初始化数据 - 调用
setMapListener()
设置事件监听
关键点:
- 使用
AMapLoader
确保API加载完成后再初始化 - 地图初始中心点设置为中国地理中心(104.937478, 35.439575)
- 添加了两个重要地图组件:行政区划图层和鹰眼控件
setMapListener
函数
ini
const setMapListener = () => {
// 监听地图缩放事件
map.on('zoomend', () => {
const zoom = map.getZoom();
updateMapData(zoom);
});
};
功能:
- 监听地图缩放事件
- 缩放结束后获取当前缩放级别并调用
updateMapData
2.2 数据管理相关函数
initData
函数
ini
const initData = async () => {
await getMapData();
mapDataStore.updateData(aMapDataList.value);
// 从store获取省级数据
const provinceData = mapDataStore.getDataByLayer(AREA_LEVEL_PROVINCE);
if (provinceData) {
updateMarkers(provinceData.regionAlterList);
// 设置初始层级
currentLayer.value = AREA_LEVEL_PROVINCE;
}
};
功能:
- 初始化加载地图数据
- 将数据存入Pinia store
- 渲染省级标记点
- 设置当前层级为省级
执行流程:
- 调用
getMapData()
获取数据 - 更新store中的数据
- 从store获取省级数据
- 调用
updateMarkers
渲染标记点
getMapData
函数
ini
const getMapData = async () => {
const params = {
riskLevel: searchForm.riskLevel?.length ? searchForm.riskLevel.join(',') : null,
addressId: searchForm.addressId?.length ? searchForm.addressId.join(',') : null
};
const res = await api.checkItem.getAreaSummaryList(params);
if (res && res.content) {
// 更新 store 中的全量数据
mapDataStore.updateData(res.content);
aMapDataList.value = res.content;
// 获取当前层级的数据并更新标记
const currentZoom = map.getZoom();
const currentLayer = getAreaLevelByZoom(currentZoom);
const data = mapDataStore.getDataByLayer(currentLayer);
if (data) {
updateMarkers(data.regionAlterList);
}
return res;
}
return null;
};
功能:
- 根据搜索条件获取地图数据
- 更新store和本地数据
- 根据当前缩放级别渲染对应层级的标记点
参数处理:
- 将数组类型的
riskLevel
和addressId
转换为字符串参数 - 空数组处理为
null
避免无效参数
数据流:
- 发起API请求
- 成功返回后更新store
- 更新组件内数据
aMapDataList
- 根据当前层级渲染标记点
updateMapData
函数
ini
const updateMapData = async (zoom) => {
const newLayer = getAreaLevelByZoom(zoom);
if (!newLayer) return;
// 如果层级没有变化,不重新请求数据
if (newLayer === currentLayer.value) return;
// 更新当前层级
currentLayer.value = newLayer;
if (infoWindow) {
infoWindow.close();
infoWindow = null;
}
// 从 store 获取数据
const data = mapDataStore.getDataByLayer(newLayer);
if (data) {
updateMarkers(data.regionAlterList);
} else {
updateMarkers([]); // 清空标记点
}
};
功能:
- 根据缩放级别更新显示的数据层级
- 避免重复渲染相同层级
- 关闭已打开的信息窗口
- 从store获取对应层级数据并更新标记点
优化点:
- 层级变化判断避免不必要渲染
- 及时清理已打开的信息窗口
2.3 标记点相关函数
getStyle
函数
ini
const getStyle = (data) => {
const size = 70;
// 根据告警比例决定颜色
const rate = parseFloat(data.rate) || 0;
const isHighAlert = rate >= 20; // 告警比例大于等于20%使用红色,否则使用绿色
const bgColor = isHighAlert ? 'rgba(255, 77, 79, 0.7)' : 'rgb(82.4, 155.2, 46.4,0.7)';
const borderColor = isHighAlert ? 'rgba(255, 77, 79, 1)' : 'rgb(82.4, 155.2, 46.4,0.7)';
return {
bgColor,
borderColor,
size,
color: '#ffffff',
textAlign: 'center',
boxShadow: '0px 0px 5px rgba(0,0,0,0.8)'
};
};
功能:
- 根据数据生成标记点样式
- 告警比例≥20%显示红色,否则显示绿色
- 返回包含各种样式属性的对象
createInfoWindowContent
函数
ini
const createInfoWindowContent = (data) => {
// 创建容器
const container = document.createElement('div');
// 创建应用实例
const app = createApp(InfoWindowTable, {
detail: data,
onViewDevices: () => {
state.alertDeviceInfoDialogShow = true;
AlertDeviceInfoDialog({
show: state.alertDeviceInfoDialogShow,
riskLevel: searchForm.riskLevel,
detail: data,
close: () => {
state.alertDeviceInfoDialogShow = false;
}
});
}
});
// 挂载组件
app.mount(container);
return container;
};
功能:
- 动态创建信息窗口内容
- 使用Vue的
createApp
挂载自定义组件InfoWindowTable
- 传递点击事件处理函数
技术亮点:
- 在原生地图API中嵌入Vue组件
- 实现组件化信息窗口内容
- 支持自定义事件传递
updateMarkers
函数
ini
const updateMarkers = (data) => {
// 清除现有标记
markers.forEach((marker) => marker.setMap(null));
markers = [];
// 关闭已打开的 infoWindow
if (infoWindow) {
infoWindow.close();
infoWindow = null;
}
if (!data || data.length === 0) return;
// 获取当前地图的缩放级别
const currentZoom = map.getZoom();
const currentLayer = getAreaLevelByZoom(currentZoom);
// 根据当前层级过滤数据
const filteredData = data.filter((item) => {
// 确保经纬度是有效的数组
if (!Array.isArray(item.lnglat) || item.lnglat.length !== 2 || !item.lnglat[0] || !item.lnglat[1]) {
return false;
}
const [lng, lat] = item.lnglat;
// 确保经纬度是有效的数字
if (typeof lng !== 'number' || typeof lat !== 'number' || isNaN(lng) || isNaN(lat)) {
return false;
}
// 只显示当前层级的数据
return item.layer === currentLayer;
});
if (filteredData.length === 0) return;
// 创建标记点
filteredData.forEach((item) => {
const [lng, lat] = item.lnglat;
const styleObj = getStyle(item);
// 创建标记点内容
const content = document.createElement('div');
content.className = 'amap-cluster';
content.style.backgroundColor = styleObj.bgColor;
content.style.width = styleObj.size + 'px';
content.style.height = styleObj.size + 'px';
content.style.border = `solid 1px ${styleObj.borderColor}`;
content.style.borderRadius = '50%';
content.innerHTML = `
<span class="showText">${item.addressName}</span>
<span class="showText">${item.alterDeviceTotal}</span>
`;
content.style.color = styleObj.color;
content.style.textAlign = styleObj.textAlign;
content.style.boxShadow = styleObj.boxShadow;
// 创建标记点
const marker = new AMap.Marker({
position: new AMap.LngLat(lng, lat),
content: content,
offset: new AMap.Pixel(-30, -30),
zIndex: 100,
extData: item
});
// 添加事件监听
marker.on('mouseover', () => {
infoWindow = new AMap.InfoWindow({
position: new AMap.LngLat(lng, lat),
offset: new AMap.Pixel(0, -80),
content: createInfoWindowContent(item),
closeWhenClickMap: true
});
infoWindow.open(map);
});
marker.on('click', () => {
const curZoom = map.getZoom();
if (curZoom < 20) {
map.setZoomAndCenter(curZoom + 1, new AMap.LngLat(lng, lat));
}
});
// 将标记点添加到地图
marker.setMap(map);
markers.push(marker);
});
};
功能:
- 全量更新地图标记点
- 清理旧标记点和信息窗口
- 过滤无效数据
- 根据当前层级筛选数据
- 创建新标记点并添加交互事件
执行流程:
- 清理现有标记
- 关闭已打开的信息窗口
- 数据有效性校验
- 层级过滤
- 为每个数据项创建标记点
- 添加鼠标悬停和点击事件
交互细节:
- 鼠标悬停显示信息窗口
- 点击标记点放大一级并居中
- 使用自定义HTML内容作为标记点
2.4 搜索与视图控制
onSearchFormChange
函数
ini
const onSearchFormChange = async (val, triggeringField) => {
if (triggeringField === 'riskLevel') {
// 风险等级改变
if (searchForm.addressId?.length) {
// 如果已选择地区,清空 store 并获取该地区的数据
mapDataStore.clearData();
const res = await getMapData();
const currentAddressIdStr = searchForm.addressId.join(',');
await handleRegionData(res, currentAddressIdStr);
} else {
// 如果未选择地区,检查是否需要清空 store
if (hasSearchConditions()) {
mapDataStore.clearData();
}
await resetMapView();
}
} else if (triggeringField === 'addressId') {
// 地区改变
searchForm.addressId = val || [];
if (searchForm.addressId.length) {
// 如果选择了地区,清空 store
mapDataStore.clearData();
const res = await getMapData();
const currentAddressIdStr = searchForm.addressId.join(',');
await handleRegionData(res, currentAddressIdStr);
} else {
// 如果清除了地区选择,检查是否需要清空 store
if (hasSearchConditions()) {
mapDataStore.clearData();
}
await resetMapView();
}
}
};
功能:
- 处理搜索条件变化
- 区分风险等级和地区两种变化类型
- 根据条件组合采取不同的数据加载策略
处理逻辑:
-
风险等级变化:
- 如果有地区选择:重新加载该地区数据
- 无地区选择:重置为全国视图
-
地区选择变化:
- 选择地区:加载该地区数据并定位
- 清除地区:重置为全国视图
handleRegionData
函数
ini
const handleRegionData = async (res, currentAddressIdStr) => {
if (!res?.content) return;
// 从返回数据中找到目标地区的数据
const targetItem = res.content.regionAlterList?.find((item) => item.addressId === currentAddressIdStr);
if (targetItem?.lnglat) {
let newZoom;
// 根据返回数据中的 layer 判断缩放级别
if (targetItem.layer === AREA_LEVEL_PROVINCE) newZoom = 7;
else if (targetItem.layer === AREA_LEVEL_CITY) newZoom = 10;
else if (targetItem.layer === AREA_LEVEL_DISTRICT) newZoom = 12;
map.setZoomAndCenter(newZoom, new AMap.LngLat(targetItem.lnglat[0], targetItem.lnglat[1]));
updateMarkers(res.content.regionAlterList);
} else {
// 如果找不到目标地区,清空 store 并回到省级视图
mapDataStore.clearData();
map.setZoom(4.7);
map.setCenter([104.937478, 35.439575]);
updateMarkers([]);
// 给出提示
ElMessage({
message: '该地区无正在告警设备',
type: 'warning'
});
}
};
功能:
- 处理地区数据定位
- 根据地区层级设置合适的缩放级别
- 找不到数据时回退到默认视图并提示
层级与缩放级别对应关系:
- 省级:zoom=7
- 市级:zoom=10
- 区级:zoom=12
resetMapView
函数
ini
const resetMapView = async () => {
map.setZoom(4.7);
map.setCenter([104.937478, 35.439575]);
const res = await getMapData();
if (res?.content) {
updateMarkers(res.content.regionAlterList);
}
};
功能:
- 重置地图视图到初始状态
- 重新加载数据并更新标记点
2.5 工具函数
getAreaLevelByZoom
函数
kotlin
const getAreaLevelByZoom = (zoom) => {
if (zoom >= MAP_ZOOM_PROVINCE_MIN && zoom <= MAP_ZOOM_PROVINCE_MAX) {
return AREA_LEVEL_PROVINCE;
} else if (zoom >= MAP_ZOOM_CITY_MIN && zoom <= MAP_ZOOM_CITY_MAX) {
return AREA_LEVEL_CITY;
} else if (zoom >= MAP_ZOOM_DISTRICT_MIN && zoom <= MAP_ZOOM_DISTRICT_MAX) {
return AREA_LEVEL_DISTRICT;
}
return null;
};
功能:
- 根据缩放级别判断当前显示的区域层级
- 使用预定义的常量范围进行比较
常量定义:
ini
export const MAP_ZOOM_PROVINCE_MIN = 2;
export const MAP_ZOOM_PROVINCE_MAX = 8;
export const MAP_ZOOM_CITY_MIN = 8;
export const MAP_ZOOM_CITY_MAX = 11;
export const MAP_ZOOM_DISTRICT_MIN = 11;
export const MAP_ZOOM_DISTRICT_MAX = 20;
hasSearchConditions
函数
ini
const hasSearchConditions = () => {
return (
(searchForm.riskLevel && searchForm.riskLevel.length > 0) ||
(searchForm.addressId && searchForm.addressId.length > 0)
);
};
功能:
- 判断当前是否有搜索条件
- 用于决定是否需要清空store
3. 组件生命周期
scss
onUnmounted(() => {
// 清除所有标记点
markers.forEach((marker) => marker.setMap(null));
markers = [];
map?.destroy();
});
五、pinia仓库代码
javascript
/*
* @Descripttion: 地图数据管理store
* @version: 1.0.0
* @Author:
* @Date: 2025-06-13 14:30:00
* @LastEditors:
* @LastEditTime: 2025-06-17 16:20:49
*/
import { defineStore } from 'pinia';
import { AREA_LEVEL_PROVINCE, AREA_LEVEL_CITY, AREA_LEVEL_DISTRICT } from '@/constant/modules/checkItem';
export const useMapDataStore = defineStore({
id: 'mapData',
state: () => ({
fullData: null // 存储完整的全量数据
}),
getters: {
// 获取所有省级数据
provinceData: (state) => {
if (!state.fullData?.regionAlterList) return null;
const provinceList = state.fullData.regionAlterList.filter((item) => item.layer === AREA_LEVEL_PROVINCE);
return {
...state.fullData,
regionAlterList: provinceList
};
},
// 获取所有市级数据
cityData: (state) => {
if (!state.fullData?.regionAlterList) return null;
const cityList = state.fullData.regionAlterList.filter((item) => item.layer === AREA_LEVEL_CITY);
return {
...state.fullData,
regionAlterList: cityList
};
},
// 获取所有区级数据
districtData: (state) => {
if (!state.fullData?.regionAlterList) return null;
const districtList = state.fullData.regionAlterList.filter((item) => item.layer === AREA_LEVEL_DISTRICT);
return {
...state.fullData,
regionAlterList: districtList
};
}
},
actions: {
// 更新数据
updateData(data) {
this.fullData = data;
},
// 清除数据
clearData() {
this.fullData = null;
},
// 根据层级获取数据(用于地图初始化和缩放)
getDataByLayer(layer) {
switch (layer) {
case AREA_LEVEL_PROVINCE:
return this.provinceData;
case AREA_LEVEL_CITY:
return this.cityData;
case AREA_LEVEL_DISTRICT:
return this.districtData;
default:
return null;
}
}
}
});
总结
通过分层数据管理、动态渲染优化和精准的状态控制,实现了高效的地图监控系统。关键经验:
- 分层数据管理 :通过
layer
字段和缩放级别实现智能数据加载 - 状态共享:使用Pinia store缓存数据,避免重复请求
- 性能优化:标记点全量更新而非增量更新,确保一致性
- 交互体验:动态样式、信息窗口、点击下钻等多维交互设计
补充
因为上述代码比较冗余,现通过cursor生成一个本地能直接使用的case,mock了一份数据且把常量移过来了~把高德的key换成你自己的,就可以看demo啦~。
ini
<!--
* @Descripttion:
* @version:
* @Author:
* @Date: 2025-06-10 14:16:03
* @LastEditors:
* @LastEditTime: 2025-06-20 14:37:48
-->
<template>
<BaseCard>
<div class="monitor-header">
<el-form ref="formRef" :inline="true" :model="searchForm" class="monitor-form">
<div class="form-left">
<el-form-item label="风险等级:" prop="riskLevel">
<el-checkbox-group v-model="searchForm.riskLevel" @change="(val) => onSearchFormChange(val, 'riskLevel')">
<el-checkbox v-for="item in riskLevelOptions" :key="item.value" :label="item.value"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="地区:" prop="addressId">
<AddressCascader
style="width: 410px"
:value="searchForm.addressId"
:teleported="false"
@change="(val) => onSearchFormChange(val, 'addressId')"
></AddressCascader>
</el-form-item>
</div>
<div class="form-right">
<el-form-item label="正在告警总设备数:" class="stat-item">
<p>
<span class="value-red">{{ aMapDataList.alterDeviceTotal }}个</span>
<span class="separator">/</span>
<span>{{ aMapDataList.deviceTotal }}个</span>
</p>
</el-form-item>
<el-form-item label="告警城市:" class="stat-item">
<p>
<span class="value-red">{{ aMapDataList.alterCityTotal }}个</span>
<span class="separator">/</span>
<span>{{ aMapDataList.cityTotal }}个</span>
</p>
</el-form-item>
</div>
</el-form>
</div>
<div id="container"></div>
</BaseCard>
</template>
<script setup>
// ====== checkItem.js常量mock在本文件 ======
const RISK_LEVEL_HIGH = 'high';
const RISK_LEVEL_CRIT = 'crit';
const RISK_LEVEL_WARN = 'warn';
const RISK_LEVEL_NOTICE = 'notice';
const riskLevelOptions = [
{ label: RISK_LEVEL_CRIT, value: RISK_LEVEL_CRIT },
{ label: RISK_LEVEL_HIGH, value: RISK_LEVEL_HIGH },
{ label: RISK_LEVEL_WARN, value: RISK_LEVEL_WARN },
{ label: RISK_LEVEL_NOTICE, value: RISK_LEVEL_NOTICE }
];
const MAP_ZOOM_PROVINCE_MIN = 2;
const MAP_ZOOM_PROVINCE_MAX = 8;
const MAP_ZOOM_CITY_MIN = 8;
const MAP_ZOOM_CITY_MAX = 11;
const MAP_ZOOM_DISTRICT_MIN = 11;
const MAP_ZOOM_DISTRICT_MAX = 20;
const DISTRICT_LAYER_DEFAULT_STYLE = {
'stroke-width': 1,
'coastline-stroke': '#DC143C',
'nation-stroke': '#DC143C',
'province-stroke': '#FF4500',
'city-stroke': 'rgba(100, 149, 237, 0.5)',
fill: (e) => ''
};
const AREA_LEVEL_PROVINCE = 'province';
const AREA_LEVEL_CITY = 'city';
const AREA_LEVEL_DISTRICT = 'district';
// ====== end checkItem.js常量mock ======
defineOptions({
name: 'AmapMonitor'
});
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import AlertDeviceInfoDialog from './alertDeviceInfoDialog';
// import constant from '@/constant';
import AMapLoader from '@amap/amap-jsapi-loader';
import BaseCard from '@/components/common/baseCard.vue';
import InfoWindowTable from './InfoWindowTable.vue';
import AddressCascader from '@/components/common/selectAddressCascader.vue';
import { createApp } from 'vue';
// import api from '@/api';
// import { useMapDataStore } from '@/store/modules/mapDataStore';
import { ElMessage } from 'element-plus';
// ====== mock数据 ======
const mockRegionAlterList = [
{
addressId: '1001',
addressName: '北京市',
layer: AREA_LEVEL_PROVINCE,
lnglat: [116.407396, 39.9042],
alterDeviceTotal: 5,
deviceTotal: 20,
rate: 25
},
{
addressId: '2001',
addressName: '上海市',
layer: AREA_LEVEL_PROVINCE,
lnglat: [121.473701, 31.230416],
alterDeviceTotal: 2,
deviceTotal: 15,
rate: 13.3
},
{
addressId: '3001',
addressName: '广州市',
layer: AREA_LEVEL_PROVINCE,
lnglat: [113.264385, 23.129112],
alterDeviceTotal: 1,
deviceTotal: 10,
rate: 10
}
];
const mockAMapDataList = {
cityTotal: 3,
alterCityTotal: 2,
deviceTotal: 45,
alterDeviceTotal: 8,
regionAlterList: mockRegionAlterList
};
// ====== end mock数据 ======
const searchForm = reactive({
riskLevel: [],
addressId: []
});
const state = reactive({
alertDeviceInfoDialogShow: false
});
let map = null;
let markers = []; // 存储所有标记点
let infoWindow = null;
const aMapDataList = ref({ ...mockAMapDataList });
// 添加一个变量来记录当前层级,控制有搜索条件时,只有跨层才发请求更新数据
const currentLayer = ref(null);
// const mapDataStore = useMapDataStore();
// 获取样式
const getStyle = (data) => {
const size = 70;
// 根据告警比例决定颜色
const rate = parseFloat(data.rate) || 0;
const isHighAlert = rate >= 20; // 告警比例大于等于20%使用红色,否则使用绿色
const bgColor = isHighAlert ? 'rgba(255, 77, 79, 0.7)' : 'rgb(82.4, 155.2, 46.4,0.7)';
const borderColor = isHighAlert ? 'rgba(255, 77, 79, 1)' : 'rgb(82.4, 155.2, 46.4,0.7)';
return {
bgColor,
borderColor,
size,
color: '#ffffff',
textAlign: 'center',
boxShadow: '0px 0px 5px rgba(0,0,0,0.8)'
};
};
// 创建信息窗口内容
const createInfoWindowContent = (data) => {
// 创建容器
const container = document.createElement('div');
// 创建应用实例
const app = createApp(InfoWindowTable, {
detail: data,
onViewDevices: () => {
state.alertDeviceInfoDialogShow = true;
AlertDeviceInfoDialog({
show: state.alertDeviceInfoDialogShow,
riskLevel: searchForm.riskLevel,
detail: data,
close: () => {
state.alertDeviceInfoDialogShow = false;
}
});
}
});
// 挂载组件
app.mount(container);
return container;
};
// 更新标记点数据
const updateMarkers = (data) => {
// 清除现有标记
markers.forEach((marker) => marker.setMap(null));
markers = [];
// 关闭已打开的 infoWindow
if (infoWindow) {
infoWindow.close();
infoWindow = null;
}
if (!data || data.length === 0) return;
// 获取当前地图的缩放级别
const currentZoom = map.getZoom();
const currentLayer = getAreaLevelByZoom(currentZoom);
// 根据当前层级过滤数据
const filteredData = data.filter((item) => {
// 确保经纬度是有效的数组
if (!Array.isArray(item.lnglat) || item.lnglat.length !== 2 || !item.lnglat[0] || !item.lnglat[1]) {
return false;
}
const [lng, lat] = item.lnglat;
// 确保经纬度是有效的数字
if (typeof lng !== 'number' || typeof lat !== 'number' || isNaN(lng) || isNaN(lat)) {
return false;
}
// 只显示当前层级的数据
return item.layer === currentLayer;
});
if (filteredData.length === 0) return;
// 创建标记点
filteredData.forEach((item) => {
const [lng, lat] = item.lnglat;
const styleObj = getStyle(item);
// 创建标记点内容
const content = document.createElement('div');
content.className = 'amap-cluster';
content.style.backgroundColor = styleObj.bgColor;
content.style.width = styleObj.size + 'px';
content.style.height = styleObj.size + 'px';
content.style.border = `solid 1px ${styleObj.borderColor}`;
content.style.borderRadius = '50%';
content.innerHTML = `
<span class="showText">${item.addressName}</span>
<span class="showText">${item.alterDeviceTotal}</span>
`;
content.style.color = styleObj.color;
content.style.textAlign = styleObj.textAlign;
content.style.boxShadow = styleObj.boxShadow;
// 创建标记点
const marker = new AMap.Marker({
position: new AMap.LngLat(lng, lat),
content: content,
offset: new AMap.Pixel(-30, -30),
zIndex: 100,
extData: item
});
// 添加事件监听
marker.on('mouseover', () => {
infoWindow = new AMap.InfoWindow({
position: new AMap.LngLat(lng, lat),
offset: new AMap.Pixel(0, -80),
content: createInfoWindowContent(item),
closeWhenClickMap: true
});
infoWindow.open(map);
});
marker.on('click', () => {
const curZoom = map.getZoom();
if (curZoom < 20) {
map.setZoomAndCenter(curZoom + 1, new AMap.LngLat(lng, lat));
}
});
// 将标记点添加到地图
marker.setMap(map);
markers.push(marker);
});
};
// 根据缩放级别获取对应的区域级别
const getAreaLevelByZoom = (zoom) => {
if (zoom >= MAP_ZOOM_PROVINCE_MIN && zoom <= MAP_ZOOM_PROVINCE_MAX) {
return AREA_LEVEL_PROVINCE;
} else if (zoom >= MAP_ZOOM_CITY_MIN && zoom <= MAP_ZOOM_CITY_MAX) {
return AREA_LEVEL_CITY;
} else if (zoom >= MAP_ZOOM_DISTRICT_MIN && zoom <= MAP_ZOOM_DISTRICT_MAX) {
return AREA_LEVEL_DISTRICT;
}
return null;
};
// 判断是否有搜索条件
const hasSearchConditions = () => {
return (
(searchForm.riskLevel && searchForm.riskLevel.length > 0) ||
(searchForm.addressId && searchForm.addressId.length > 0)
);
};
// 缩放时获取对应级别的数据
const updateMapData = async (zoom) => {
const newLayer = getAreaLevelByZoom(zoom);
if (!newLayer) return;
// 如果层级没有变化,不重新请求数据
if (newLayer === currentLayer.value) return;
// 更新当前层级
currentLayer.value = newLayer;
if (infoWindow) {
infoWindow.close();
infoWindow = null;
}
// 直接用mock数据
updateMarkers(aMapDataList.value.regionAlterList);
};
// 获取地图数据
const getMapData = async () => {
// const params = {
// riskLevel: searchForm.riskLevel?.length ? searchForm.riskLevel.join(',') : null,
// addressId: searchForm.addressId?.length ? searchForm.addressId.join(',') : null
// };
// const res = await api.checkItem.getAreaSummaryList(params);
// if (res && res.content) {
// aMapDataList.value = res.content;
// // ...
// return res;
// }
// return null;
// 直接用mock数据
aMapDataList.value = { ...mockAMapDataList };
return { content: mockAMapDataList };
};
// 初始化时获取省级数据
const initData = async () => {
await getMapData();
// const provinceData = mapDataStore.getDataByLayer(AREA_LEVEL_PROVINCE);
// if (provinceData) {
// updateMarkers(provinceData.regionAlterList);
// currentLayer.value = AREA_LEVEL_PROVINCE;
// }
updateMarkers(aMapDataList.value.regionAlterList);
currentLayer.value = AREA_LEVEL_PROVINCE;
};
const setMapListener = () => {
// 监听地图缩放事件
map.on('zoomend', () => {
const zoom = map.getZoom();
updateMapData(zoom);
});
};
onMounted(() => {
AMapLoader.load({
key: 'xxxxxxxx',// 去高德地图申请,建议使用企业账号
version: '2.0',
plugins: ['AMap.DistrictLayer', 'AMap.HawkEye']
})
.then((AMap) => {
map = new AMap.Map('container', {
viewMode: '2D',
zoom: 4.7,
zooms: [4, 16],
center: [104.937478, 35.439575],
mapStyle: 'amap://styles/noramal'
});
// 创建国家行政区图层
const districtLayer = new AMap.DistrictLayer.Country({
SOC: 'CHN',
depth: 3,
zIndex: 90,
styles: DISTRICT_LAYER_DEFAULT_STYLE
});
// 创建鹰眼控件
var hawkEye = new AMap.HawkEye({
isOpen: true, // 确保鹰眼控件默认打开
position: 'RB', // 设置鹰眼控件在右下角
mapStyle: 'amap://styles/normal'
});
map.add(districtLayer);
map.addControl(hawkEye);
initData(); // 初始化数据
setMapListener(); // 设置地图监听
})
.catch((e) => {
console.error('地图加载失败:', e);
});
});
// 重置地图视图并获取数据
const resetMapView = async () => {
map.setZoom(4.7);
map.setCenter([104.937478, 35.439575]);
// const res = await getMapData();
// if (res?.content) {
// updateMarkers(res.content.regionAlterList);
// }
aMapDataList.value = { ...mockAMapDataList };
updateMarkers(aMapDataList.value.regionAlterList);
};
// 处理地区数据
const handleRegionData = async (res, currentAddressIdStr) => {
if (!res?.content) return;
// 从返回数据中找到目标地区的数据
const targetItem = res.content.regionAlterList?.find((item) => item.addressId === currentAddressIdStr);
if (targetItem?.lnglat) {
let newZoom;
// 根据返回数据中的 layer 判断缩放级别
if (targetItem.layer === AREA_LEVEL_PROVINCE) newZoom = 7;
else if (targetItem.layer === AREA_LEVEL_CITY) newZoom = 10;
else if (targetItem.layer === AREA_LEVEL_DISTRICT) newZoom = 12;
map.setZoomAndCenter(newZoom, new AMap.LngLat(targetItem.lnglat[0], targetItem.lnglat[1]));
updateMarkers(res.content.regionAlterList);
} else {
map.setZoom(4.7);
map.setCenter([104.937478, 35.439575]);
updateMarkers([]);
ElMessage({
message: '该地区无正在告警设备',
type: 'warning'
});
}
};
const onSearchFormChange = async (val, triggeringField) => {
if (triggeringField === 'riskLevel') {
if (searchForm.addressId?.length) {
const res = await getMapData();
const currentAddressIdStr = searchForm.addressId.join(',');
await handleRegionData(res, currentAddressIdStr);
} else {
await resetMapView();
}
} else if (triggeringField === 'addressId') {
searchForm.addressId = val || [];
if (searchForm.addressId.length) {
const res = await getMapData();
const currentAddressIdStr = searchForm.addressId.join(',');
await handleRegionData(res, currentAddressIdStr);
} else {
await resetMapView();
}
}
};
onUnmounted(() => {
markers.forEach((marker) => marker.setMap(null));
markers = [];
map?.destroy();
});
</script>
<style lang="scss" scoped>
.monitor-header {
display: flex;
justify-content: flex-start;
align-items: flex-start;
padding: 0;
position: relative;
margin-bottom: 16px;
&::after {
content: '';
height: 1px;
position: absolute;
left: -17px;
right: -17px;
bottom: -8px;
background: #ccc;
}
.monitor-form {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
width: 100%;
}
.form-left {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.form-right {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.stat-item {
:deep(.el-form-item__label) {
margin-right: -10px;
}
.value-red {
color: #ff4d4f;
font-size: 20px;
font-weight: bold;
}
.separator {
margin: 0 4px;
color: #999;
font-weight: 700;
}
}
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 0;
}
#container {
width: 100%;
height: 80vh;
position: relative;
margin-top: 16px;
}
:deep(.amap-cluster) {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
font-size: 12px;
padding: 4px;
}
:deep(.showText) {
font-size: 14px;
font-weight: bold;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
text-align: center;
line-height: 1.2;
color: #f2f3f5;
}
</style>