可视化大屏系列(4):高德地图集成与动态热区渲染
在智慧工地可视化大屏中,地图模块是信息密度最高、交互频率最高的关键组件。本节将围绕“高德地图集成”与“动态热区渲染”展开,构建一套支持区域划分、实时定位、状态标注、交互控制的地图系统。
一、地图组件集成方案
1. 安装高德地图 Vue 插件
在 Vue3 项目中推荐使用 @vuemap/vue-amap
,它对高德地图进行了 Vue3 的适配封装,使用方便、功能全面。
✅ 安装插件
npm install @vuemap/vue-amap
✅ 引入并初始化(main.ts)
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import AMapLoader from '@vuemap/vue-amap'
import '@vuemap/vue-amap/dist/style.css' // 样式必须引入
const app = createApp(App)
app.use(AMapLoader, {
key: '你的高德Key', // 替换为你申请的高德 Key
version: '2.0',
plugins: [
'AMap.Marker',
'AMap.Polygon',
'AMap.InfoWindow',
'AMap.ToolBar',
'AMap.Scale'
]
})
app.mount('#app')
2. 封装地图基础容器组件 MapPanel.vue
地图组件作为容器,需要对地图大小、响应式处理、初始视图进行封装。
<!-- src/components/map/MapPanel.vue -->
<template>
<div class="map-container">
<el-amap
ref="amapRef"
:center="center"
:zoom="zoom"
class="w-full h-full"
>
<slot />
</el-amap>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue'
defineProps({
center: {
type: Array,
default: () => [116.397428, 39.90923] // 默认北京天安门
},
zoom: {
type: Number,
default: 17
}
})
const amapRef = ref()
</script>
<style scoped>
.map-container {
width: 100%;
height: 100%;
position: relative;
}
</style>
3. 动态创建地图 Polygon、Marker 的组件封装
为了模块化开发,可以将区域绘制、人员设备定位拆分成独立子组件。
示例:区域绘制组件 MapPolygon.vue
<!-- src/components/map/MapPolygon.vue -->
<template>
<el-amap-polygon
v-for="(area, index) in areas"
:key="index"
:path="area.path"
:stroke-color="getStrokeColor(area.status)"
:fill-color="getFillColor(area.status)"
:fill-opacity="0.4"
:stroke-weight="2"
@click="() => handleAreaClick(area)"
/>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
areas: Array
})
const emit = defineEmits(['areaClick'])
const getStrokeColor = (status) => {
return status === 'error' ? '#F56C6C' : '#409EFF'
}
const getFillColor = (status) => {
const map = {
normal: '#67C23A',
warning: '#E6A23C',
error: '#F56C6C',
off: '#909399'
}
return map[status] || '#a0cfff'
}
const handleAreaClick = (area) => {
emit('areaClick', area)
}
</script>
示例:设备定位组件 MapDevice.vue
<!-- src/components/map/MapDevice.vue -->
<template>
<el-amap-marker
v-for="(device, index) in devices"
:key="index"
:position="device.position"
:title="device.name"
:icon="deviceIcon"
/>
</template>
<script setup>
import { defineProps } from 'vue'
import deviceIconUrl from '@/assets/device-icon.png'
const props = defineProps({
devices: Array
})
const deviceIcon = {
image: deviceIconUrl,
size: [32, 32]
}
</script>
4. 实际使用示例
<template>
<MapPanel :center="[116.39, 39.91]" :zoom="17">
<MapPolygon :areas="areaList" @areaClick="handleAreaClick" />
<MapDevice :devices="deviceList" />
</MapPanel>
</template>
<script setup>
import MapPanel from '@/components/map/MapPanel.vue'
import MapPolygon from '@/components/map/MapPolygon.vue'
import MapDevice from '@/components/map/MapDevice.vue'
const areaList = [
{
name: '基坑区域',
path: [
[116.3969, 39.9083],
[116.3977, 39.9089],
[116.3970, 39.9094]
],
status: 'normal'
}
]
const deviceList = [
{
name: '塔吊1号',
position: [116.3974, 39.9092]
}
]
const handleAreaClick = (area) => {
console.log('点击区域:', area.name)
}
</script>
功能模块 | 技术栈 | 说明 |
---|---|---|
地图底图 | vue-amap | 基于 Vue3 组件化封装高德地图 |
区域热区 | AMap.Polygon | 区域状态动态绑定颜色 |
定位设备 | AMap.Marker | 支持实时坐标更新 |
容器封装 | MapPanel.vue | 自适应布局,统一中心与缩放 |
插件注册 | ToolBar , InfoWindow , Scale | 辅助工具组件 |
二、工地区域划分与热区绘制
我们将通过高德地图 Polygon
实现区域绘制,结合后台接口获取数据,并支持热区颜色状态动态变化、点击联动信息弹窗等功能。
1. 区域数据结构设计(后端接口返回)
从后端获取区域数据,每个区域包含名称、坐标点位、状态等信息:
[
{
"id": 1,
"name": "基坑作业区",
"points": [
[116.3969, 39.9083],
[116.3977, 39.9089],
[116.3970, 39.9094]
],
"status": "normal",
"info": "施工中,安全正常"
},
{
"id": 2,
"name": "材料堆放区",
"points": [
[116.3980, 39.9080],
[116.3985, 39.9088],
[116.3979, 39.9091]
],
"status": "warning",
"info": "临时堆放材料,请注意安全"
}
]
2. 热区绘制组件封装(MapPolygon.vue)
<!-- src/components/map/MapPolygon.vue -->
<template>
<el-amap-polygon
v-for="area in areas"
:key="area.id"
:path="area.points"
:stroke-color="getStrokeColor(area.status)"
:fill-color="getFillColor(area.status)"
:fill-opacity="0.5"
:stroke-weight="2"
:ext-data="area"
@click="handleClick(area)"
/>
</template>
<script setup>
const props = defineProps({
areas: Array
})
const emit = defineEmits(['areaClick'])
const getStrokeColor = (status) => {
switch (status) {
case 'normal': return '#67C23A'
case 'warning': return '#E6A23C'
case 'danger': return '#F56C6C'
default: return '#409EFF'
}
}
const getFillColor = (status) => {
switch (status) {
case 'normal': return '#DFF0D8'
case 'warning': return '#FCF8E3'
case 'danger': return '#F2DEDE'
default: return '#E6F7FF'
}
}
const handleClick = (area) => {
emit('areaClick', area)
}
</script>
3. 页面中使用区域组件 + 获取数据
<template>
<MapPanel :center="[116.3974, 39.9093]" :zoom="17">
<MapPolygon :areas="areaList" @areaClick="onAreaClick" />
</MapPanel>
<!-- 信息弹窗 -->
<el-dialog v-model="dialogVisible" title="区域信息" width="30%">
<div>
<p><strong>名称:</strong>{{ currentArea.name }}</p>
<p><strong>状态:</strong>{{ currentArea.status }}</p>
<p><strong>详情:</strong>{{ currentArea.info }}</p>
</div>
</el-dialog>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import MapPanel from '@/components/map/MapPanel.vue'
import MapPolygon from '@/components/map/MapPolygon.vue'
import { getRequest } from '@/utils/request'
const areaList = ref([])
const currentArea = ref({})
const dialogVisible = ref(false)
const fetchAreaData = async () => {
try {
const res = await getRequest('/api/map/areas') // 替换为真实接口
areaList.value = res.data
} catch (err) {
console.error('区域数据加载失败', err)
}
}
const onAreaClick = (area) => {
currentArea.value = area
dialogVisible.value = true
}
onMounted(fetchAreaData)
</script>
4. 拓展功能建议(可选)
功能模块 | 说明 |
---|---|
实时状态更新 | 每 5 秒轮询后端,动态更新热区颜色 |
多状态图例 | 在地图侧边增加状态图例,辅助理解 |
动态添加与删除区域 | 通过管理后台动态编辑工地区域 |
鼠标悬停显示 Tooltip | 使用 AMap.InfoWindow 或浮层展示简要信息 |
要点 | 技术 |
---|---|
区域绘制 | el-amap-polygon |
状态动态渲染 | 颜色映射函数 getFillColor() |
区域点击联动 | 事件回调 @click="handleClick" |
数据来源 | 后端接口 /api/map/areas |
UI 展示 | Element Plus 弹窗 |
三、设备/人员定位模块设计与实现
我们通过高德地图的 Marker
组件实现定位展示,结合 WebSocket 或定时轮询方式进行动态刷新。
1. 后端返回数据结构示例
[
{
"id": "dev001",
"type": "设备",
"name": "塔吊-01",
"position": [116.3963, 39.9086],
"status": "online",
"extraInfo": "运行正常"
},
{
"id": "person101",
"type": "人员",
"name": "张三",
"position": [116.3971, 39.9091],
"status": "working",
"extraInfo": "施工中"
}
]
2. Marker 组件封装(MapMarkers.vue)
<!-- src/components/map/MapMarkers.vue -->
<template>
<el-amap-marker
v-for="item in list"
:key="item.id"
:position="item.position"
:icon="getIcon(item)"
:ext-data="item"
:offset="[-10, -30]"
@click="handleClick(item)"
/>
</template>
<script setup>
const props = defineProps({
list: Array
})
const emit = defineEmits(['markerClick'])
const getIcon = (item) => {
if (item.type === '设备') {
return item.status === 'online'
? '/icons/device-online.png'
: '/icons/device-offline.png'
} else if (item.type === '人员') {
return '/icons/worker.png'
}
return '/icons/default.png'
}
const handleClick = (item) => {
emit('markerClick', item)
}
</script>
💡 建议图标使用 32x32 或 48x48 的 PNG 图,支持状态区分。
3. 页面集成组件 + 弹窗展示
<template>
<MapPanel :center="[116.3974, 39.9093]" :zoom="17">
<MapPolygon :areas="areaList" />
<MapMarkers :list="markerList" @markerClick="onMarkerClick" />
</MapPanel>
<el-dialog v-model="dialogVisible" title="位置详情" width="30%">
<div>
<p><strong>名称:</strong>{{ currentItem.name }}</p>
<p><strong>类型:</strong>{{ currentItem.type }}</p>
<p><strong>状态:</strong>{{ currentItem.status }}</p>
<p><strong>详情:</strong>{{ currentItem.extraInfo }}</p>
</div>
</el-dialog>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import MapPanel from '@/components/map/MapPanel.vue'
import MapPolygon from '@/components/map/MapPolygon.vue'
import MapMarkers from '@/components/map/MapMarkers.vue'
import { getRequest } from '@/utils/request'
const markerList = ref([])
const currentItem = ref({})
const dialogVisible = ref(false)
const fetchMarkers = async () => {
try {
const res = await getRequest('/api/map/positions') // 替换接口
markerList.value = res.data
} catch (err) {
console.error('位置数据加载失败', err)
}
}
const onMarkerClick = (item) => {
currentItem.value = item
dialogVisible.value = true
}
onMounted(() => {
fetchMarkers()
// 每30秒自动刷新
setInterval(fetchMarkers, 30000)
})
</script>
4. 可选拓展功能建议
功能 | 说明 |
---|---|
状态动画效果 | 使用带动画的 icon 或 CSS 动效提升可视化体验 |
Marker 聚合 | 对密集人员或设备使用点聚合(AMap.MarkerCluster) |
WebSocket 推送 | 使用 Socket 替代轮询,实现秒级刷新 |
点击后地图定位 | 点击人员定位可自动居中放大(map.setCenter() ) |
模块 | 技术实现 |
---|---|
实时定位 | 高德地图 el-amap-marker |
图标切换 | 根据类型/状态自定义 icon |
数据更新 | 轮询接口 / WebSocket |
联动弹窗 | Element Plus 弹窗 |
UI 提示 | 悬停 Tooltip、状态图例 |
四、热区自适应展示与高亮反馈
为了在不同分辨率、不同缩放层级下保持良好的视觉效果和交互体验,我们实现了 热区的自适应绘制与状态高亮反馈功能。
1. 热区数据结构与后端接口
[
{
"id": "zone001",
"name": "塔吊作业区",
"points": [
[116.3962, 39.9083],
[116.3965, 39.9085],
[116.3964, 39.9088],
[116.3961, 39.9086]
],
"status": "active"
},
{
"id": "zone002",
"name": "材料堆放区",
"points": [
[116.3970, 39.9082],
[116.3974, 39.9084],
[116.3972, 39.9087]
],
"status": "warning"
}
]
2. 热区绘制组件封装(MapHeatZones.vue
)
<template>
<el-amap-polygon
v-for="zone in zones"
:key="zone.id"
:path="zone.points"
:stroke-color="getStroke(zone.status)"
:fill-color="getFill(zone.status)"
:stroke-opacity="1"
:fill-opacity="0.4"
:stroke-weight="2"
@click="handleClick(zone)"
/>
</template>
<script setup>
const props = defineProps({
zones: Array
})
const emit = defineEmits(['zoneClick'])
const getStroke = (status) => {
return status === 'active' ? '#00bfa5' : status === 'warning' ? '#ff9800' : '#ccc'
}
const getFill = (status) => {
return status === 'active' ? '#b2dfdb' : status === 'warning' ? '#ffe082' : '#e0e0e0'
}
const handleClick = (zone) => {
emit('zoneClick', zone)
}
</script>
📝 可拓展状态色:normal(灰)、active(绿)、warning(黄)、danger(红)
3. 页面集成与状态反馈联动
<MapPanel :center="center" :zoom="17">
<MapHeatZones :zones="zoneList" @zoneClick="onZoneClick" />
</MapPanel>
<el-drawer v-model="drawerVisible" title="区域详情" direction="rtl" size="300px">
<p><strong>区域名称:</strong>{{ currentZone.name }}</p>
<p><strong>状态:</strong>{{ currentZone.status }}</p>
<!-- 可显示相关人员、事件、设备等 -->
</el-drawer>
const zoneList = ref([])
const currentZone = ref({})
const drawerVisible = ref(false)
const onZoneClick = (zone) => {
currentZone.value = zone
drawerVisible.value = true
}
// 获取热区数据
const fetchZones = async () => {
const res = await getRequest('/api/map/zones')
zoneList.value = res.data
}
onMounted(() => {
fetchZones()
})
4. 自适应与高亮优化技巧
优化项 | 实现方式 |
---|---|
响应式缩放 | 多边形 path 使用经纬度适配,无需额外缩放处理 |
高亮边框 | 点击或 hover 时动态改变 strokeWidth / strokeColor |
状态动效 | 可为 warning /danger 区域添加动画边框或闪烁 CSS 效果 |
图层控制 | 设置 zIndex 与图层顺序,避免与人员/设备标记冲突 |
提示气泡 | el-amap-info-window 支持点击或悬停时弹出说明 |
模块 | 技术实现 |
---|---|
热区绘制 | el-amap-polygon 多边形组件 |
自适应展示 | 根据经纬度坐标动态绘制 |
状态反馈 | 不同状态填充色与边框样式区分 |
信息联动 | 点击弹窗展示区域详情 |
动态刷新 | 定时更新热区数据或接入推送机制 |
五、地图图层控制与交互联动
实现图层的灵活控制与与其他模块(如图表、面板、列表等)的联动,以增强数据可视性与操作便捷性。
1. 图层模块划分
为了提升地图的模块化管理能力,我们将地图中的展示内容划分为多个“图层”,包括但不限于:
图层名称 | 显示内容 |
---|---|
热区图层 | 工地区域划分热区 |
人员定位图层 | 在线施工人员位置 |
设备图层 | 塔吊、升降机、摄像头等设备 |
事件图层 | 告警/巡检/签到记录点 |
2. 图层开关组件(MapLayerToggle.vue
)
<template>
<el-card shadow="hover" class="layer-panel">
<el-checkbox-group v-model="visibleLayers" @change="emitChange">
<el-checkbox label="heatzone">热区</el-checkbox>
<el-checkbox label="person">人员</el-checkbox>
<el-checkbox label="device">设备</el-checkbox>
<el-checkbox label="event">事件</el-checkbox>
</el-checkbox-group>
</el-card>
</template>
<script setup>
const visibleLayers = ref(['heatzone', 'person', 'device'])
const emit = defineEmits(['update'])
const emitChange = () => {
emit('update', visibleLayers.value)
}
</script>
<style scoped>
.layer-panel {
position: absolute;
top: 20px;
right: 20px;
z-index: 999;
width: 180px;
background: white;
}
</style>
3. 地图层级切换逻辑(父组件整合)
<MapLayerToggle @update="handleLayerUpdate" />
<el-amap>
<MapHeatZones v-if="layers.includes('heatzone')" :zones="zoneList" />
<MapPersons v-if="layers.includes('person')" :persons="personList" />
<MapDevices v-if="layers.includes('device')" :devices="deviceList" />
<MapEvents v-if="layers.includes('event')" :events="eventList" />
</el-amap>
const layers = ref(['heatzone', 'person', 'device'])
const handleLayerUpdate = (newLayers) => {
layers.value = newLayers
}
4. 地图与图表/面板联动展示
实现点击地图上的热区、设备或人员后,联动右侧或下方的信息面板或统计图表进行更新:
const currentPerson = ref({})
const currentDevice = ref({})
const handlePersonClick = (person) => {
currentPerson.value = person
showPersonDrawer.value = true
}
const handleDeviceClick = (device) => {
currentDevice.value = device
showDeviceDrawer.value = true
}
你可以通过组件间事件通信或使用 pinia
共享状态来完成跨模块联动,确保“图-表-列表”三位一体的统一交互体验。
5. 实战技巧总结
功能项 | 技术点 | 实现建议 |
---|---|---|
图层切换 | checkbox 控制显示 | 使用 v-if 控制图层组件 |
交互反馈 | 点击 marker 显示弹窗或面板 | 使用 @click + Drawer |
联动同步 | 地图和图表共享状态 | 使用 Pinia 管理全局 state |
数据更新 | 定时轮询 / WebSocket 实时刷新 | 配合 loading 动画防抖处理 |
UI 整洁性 | 悬浮图层控制面板 + 动画显示 | 保持右上角常驻入口 |
六、扩展建议与优化方向
功能方向 | 建议 |
---|---|
热区状态刷新 | 使用定时轮询或 WebSocket 推送 |
多图层叠加 | 支持环境监测/视频图层 |
设备状态动画提示 | 使用 Marker 动画属性或闪烁 |
多区域切换 | 支持工区选择与过滤视图 |
主题与夜间模式支持 | 配合 UI 主题同步切换 |
当然可以,以下是本篇《可视化大屏系列(4):高德地图集成与动态热区渲染》的结尾部分:
结语:构建真正“可感知”的智慧工地地图大屏
在本篇文章中,我们围绕“地图可视化”这一核心展开,从高德地图组件的集成、热区绘制、动态渲染,到地图图层控制与业务模块的交互联动,全方位搭建起一个真正“能看、能控、能联动”的智慧工地地图系统。
通过模块化封装、响应式布局、数据驱动渲染与高可扩展的事件系统,我们不仅实现了地图的展示,更打造了一个可随业务需求扩展的“空间信息平台”。
地图不仅仅是展示地理位置的载体,更是打通设备、人员、任务等数据的重要桥梁。在智慧工地建设过程中,未来我们还可以结合 GIS、BIM、IoT 等技术,进一步升级地图系统的智能化水平,实现从“看得见”到“看得懂”的跃迁。
下一篇,我们将进入《可视化大屏系列(5)》:设备状态面板与数据分析仪表盘,聚焦设备运行、工况统计、预警机制等方面的数据可视化实现。如果你对后续内容感兴趣,欢迎持续关注!
如需源码封装细节或实战项目部署,也欢迎留言或私信交流。🚀