@@ -0,0 +1,759 @@
< template >
< div class = "map-test-container" >
< div id = "mapContainer" class = "map-container" v-loading = "loading" element-loading-text="加载设备数据中..." > < / div >
< div class = "info-panel" >
< h3 > 地图信息 < / h3 >
< p > 总点位数 : { { points . length } } < / p >
< p > 当前缩放级别 : { { currentZoom } } < / p >
< div class = "legend" >
< h4 > 图例 < / h4 >
< div class = "legend-item" >
< span class = "dot" > < / span >
< span > 设备点位 < / span >
< / div >
< / div >
< / div >
<!-- 隐藏的SVG定义 -- >
< svg width = "0" height = "0" style = "position: absolute;" >
< defs >
< filter id = "glow" >
< feGaussianBlur stdDeviation = "3" result = "coloredBlur" / >
< feMerge >
< feMergeNode in = "coloredBlur" / >
< feMergeNode in = "SourceGraphic" / >
< / feMerge >
< / filter >
< / defs >
< / svg >
<!-- 点位信息弹出框 -- >
< el-dialog
v-model = "dialogVisible"
title = "位置详情"
width = "400px"
:close-on-click-modal = "true"
>
< div class = "point-info" >
< div class = "info-row" >
< span class = "label" > 编号 : < / span >
< span class = "value" > { { selectedPoint . id } } < / span >
< / div >
< div class = "info-row" >
< span class = "label" > 上报时间 : < / span >
< span class = "value" > { { selectedPoint . reportTime } } < / span >
< / div >
< div class = "info-row" >
< span class = "label" > 经度 : < / span >
< span class = "value" > { { selectedPoint . longitude } } < / span >
< / div >
< div class = "info-row" >
< span class = "label" > 纬度 : < / span >
< span class = "value" > { { selectedPoint . latitude } } < / span >
< / div >
< / div >
< / el-dialog >
< / div >
< / template >
< script setup name = "MapTest" >
import { ref , onMounted , onBeforeUnmount } from 'vue'
import { initMap , initLayer , addImage } from '@/utils/map'
import { mapConfig , layerUrl , imageArr } from '@/config/map'
import { PointLayer , Marker } from '@antv/l7'
import { getLastData } from '@/api/td/tdEngine'
import { ElMessage } from 'element-plus'
const scene = ref ( null )
const pointLayer = ref ( null )
const currentZoom = ref ( 13 )
const points = ref ( [ ] )
const dialogVisible = ref ( false )
const selectedPoint = ref ( {
id : '' ,
reportTime : '' ,
longitude : '' ,
latitude : ''
} )
const loading = ref ( false )
let refreshTimer = null // 定时刷新定时器
// 从接口获取设备数据
const fetchDeviceData = async ( ) => {
loading . value = true
try {
const response = await getLastData ( )
// 转换后端数据格式为前端需要的格式
const devicePoints = response . map ( ( item , index ) => {
return {
id : item . deviceId || ` PT ${ String ( index + 1 ) . padStart ( 4 , '0' ) } ` ,
name : ` 设备 ${ item . deviceId || index + 1 } ` ,
longitude : item . longitude ,
latitude : item . latitude ,
reportTime : item . createTime || item . time || new Date ( ) . toLocaleString ( 'zh-CN' , {
year : 'numeric' ,
month : '2-digit' ,
day : '2-digit' ,
hour : '2-digit' ,
minute : '2-digit' ,
second : '2-digit' ,
hour12 : false
} ) ,
status : 'online'
}
} )
points . value = devicePoints
console . log ( '从接口获取到' , points . value . length , '个设备点位' )
// 如果已经有图层,更新图层数据;否则创建新图层
if ( scene . value ) {
updatePointLayer ( )
}
return devicePoints
} catch ( error ) {
console . error ( '获取设备数据失败:' , error )
ElMessage . error ( '获取设备数据失败,请稍后重试' )
return [ ]
} finally {
loading . value = false
}
}
// 初始化地图
const initializeMap = async ( ) => {
try {
// 从接口获取设备数据
await fetchDeviceData ( )
// 如果有数据,使用第一个设备的经纬度作为地图中心
let mapCenter = mapConfig . center
if ( points . value . length > 0 ) {
const firstPoint = points . value [ 0 ]
mapCenter = [ firstPoint . longitude , firstPoint . latitude ]
console . log ( '地图中心设置为:' , mapCenter )
}
// 创建地图场景,使用动态中心点
const mapScene = await initMap ( 'mapContainer' , {
... mapConfig ,
center : mapCenter
} )
scene . value = mapScene
// 添加图层
initLayer ( mapScene , layerUrl )
// 添加图片资源
addImage ( mapScene , imageArr )
// 创建点图层
if ( points . value . length > 0 ) {
createPointLayer ( )
// 启动定时刷新
startAutoRefresh ( )
} else {
ElMessage . warning ( '暂无设备数据' )
}
// 监听缩放事件
mapScene . on ( 'zoom' , ( ) => {
currentZoom . value = Math . round ( mapScene . getZoom ( ) )
} )
console . log ( '地图初始化成功,已添加' , points . value . length , '个点位' )
} catch ( error ) {
console . error ( '地图初始化失败:' , error )
ElMessage . error ( '地图初始化失败' )
}
}
// 创建3D立体标记图标HTML
const create3DMarkerElement = ( point ) => {
const element = document . createElement ( 'div' )
element . className = 'marker-3d'
element . innerHTML = `
<div class="marker-container">
<div class="pulse-ring"></div>
<div class="outer-glow"></div>
<div class="marker-pin">
<div class="marker-glow"></div>
<div class="inner-shine"></div>
<svg class="marker-icon" viewBox="0 0 24 24" fill="white">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</div>
<div class="marker-shadow"></div>
<div class="status-ring"></div>
</div>
`
// 添加点击事件
element . addEventListener ( 'click' , ( ) => {
selectedPoint . value = {
id : point . id ,
reportTime : point . reportTime ,
longitude : point . longitude . toFixed ( 6 ) ,
latitude : point . latitude . toFixed ( 6 )
}
dialogVisible . value = true
} )
return element
}
// 创建点图层(使用Marker实现3D效果)
const createPointLayer = ( ) => {
if ( ! scene . value || points . value . length === 0 ) return
const markers = [ ]
// 为每个点创建3D Marker
points . value . forEach ( point => {
const element = create3DMarkerElement ( point )
const marker = new Marker ( {
element : element ,
anchor : 'bottom'
} ) . setLnglat ( [ point . longitude , point . latitude ] )
scene . value . addMarker ( marker )
markers . push ( marker )
} )
// 脉冲动画底层光圈
const pulseLayer = new PointLayer ( {
name : 'pulsePoints' ,
zIndex : 100
} )
. source ( points . value , {
parser : {
type : 'json' ,
x : 'longitude' ,
y : 'latitude'
}
} )
. shape ( 'circle' )
. size ( 40 )
. color ( '#5B8FF9' )
. style ( {
opacity : 0.15
} )
. animate ( {
enable : true ,
type : 'breathe' ,
duration : 2000 ,
interval : 0.5 ,
trailLength : 0.1
} )
scene . value . addLayer ( pulseLayer )
pointLayer . value = { markers , pulseLayer }
}
// 更新点图层数据
const updatePointLayer = ( ) => {
if ( ! scene . value || points . value . length === 0 ) return
// 只更新脉冲图层的数据源,不重建整个图层
if ( pointLayer . value && pointLayer . value . pulseLayer ) {
pointLayer . value . pulseLayer . setData ( points . value )
}
// 更新现有markers的位置和数据
if ( pointLayer . value && pointLayer . value . markers ) {
const oldMarkers = pointLayer . value . markers
const newPoints = points . value
// 如果点位数量相同,只更新位置
if ( oldMarkers . length === newPoints . length ) {
oldMarkers . forEach ( ( marker , index ) => {
const newPoint = newPoints [ index ]
if ( newPoint ) {
// 更新位置
marker . setLnglat ( [ newPoint . longitude , newPoint . latitude ] )
// 更新点击事件数据
const element = marker . getElement ( )
element . onclick = ( ) => {
selectedPoint . value = {
id : newPoint . id ,
reportTime : newPoint . reportTime ,
longitude : newPoint . longitude . toFixed ( 6 ) ,
latitude : newPoint . latitude . toFixed ( 6 )
}
dialogVisible . value = true
}
}
} )
console . log ( '已无感更新' , newPoints . length , '个点位位置' )
} else {
// 点位数量变化时才完全重建
rebuildPointLayer ( )
}
} else {
// 首次创建或图层不存在时
createPointLayer ( )
}
}
// 完全重建点图层(仅在点位数量变化时使用)
const rebuildPointLayer = ( ) => {
if ( ! scene . value ) return
// 清除旧的markers和图层
if ( pointLayer . value ) {
if ( pointLayer . value . markers ) {
pointLayer . value . markers . forEach ( marker => {
marker . remove ( )
} )
}
if ( pointLayer . value . pulseLayer ) {
scene . value . removeLayer ( pointLayer . value . pulseLayer )
}
}
// 重新创建图层
createPointLayer ( )
console . log ( '点位数量变化,已重建图层' )
}
// 启动定时刷新
const startAutoRefresh = ( ) => {
// 清除已存在的定时器
if ( refreshTimer ) {
clearInterval ( refreshTimer )
}
// 每10秒刷新一次数据
refreshTimer = setInterval ( ( ) => {
console . log ( '定时刷新设备数据...' )
fetchDeviceData ( )
} , 10000 )
console . log ( '已启动自动刷新, 每10秒更新一次' )
}
// 停止定时刷新
const stopAutoRefresh = ( ) => {
if ( refreshTimer ) {
clearInterval ( refreshTimer )
refreshTimer = null
console . log ( '已停止自动刷新' )
}
}
onMounted ( ( ) => {
initializeMap ( )
} )
onBeforeUnmount ( ( ) => {
// 停止定时刷新
stopAutoRefresh ( )
if ( pointLayer . value ) {
// 清理markers
if ( pointLayer . value . markers ) {
pointLayer . value . markers . forEach ( marker => {
marker . remove ( ) // 使用marker.remove()方法移除标记
} )
}
// 清理图层
if ( pointLayer . value . pulseLayer ) {
scene . value ? . removeLayer ( pointLayer . value . pulseLayer )
}
}
if ( scene . value ) {
scene . value . destroy ( )
}
} )
< / script >
< style scoped lang = "scss" >
. map - test - container {
width : 100 % ;
height : 100 vh ;
position : relative ;
. map - container {
width : 100 % ;
height : 100 % ;
}
. info - panel {
position : absolute ;
top : 20 px ;
right : 20 px ;
background : rgba ( 255 , 255 , 255 , 0.95 ) ;
padding : 20 px ;
border - radius : 8 px ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.15 ) ;
min - width : 220 px ;
backdrop - filter : blur ( 10 px ) ;
h3 {
margin : 0 0 15 px 0 ;
font - size : 16 px ;
color : # 333 ;
border - bottom : 2 px solid # 00 D9FF ;
padding - bottom : 10 px ;
}
p {
margin : 8 px 0 ;
font - size : 14 px ;
color : # 666 ;
& : first - of - type {
font - weight : bold ;
color : # 00 D9FF ;
font - size : 16 px ;
}
}
. legend {
margin - top : 20 px ;
padding - top : 15 px ;
border - top : 1 px solid # e8e8e8 ;
h4 {
margin : 0 0 12 px 0 ;
font - size : 14 px ;
color : # 666 ;
}
. legend - item {
display : flex ;
align - items : center ;
margin - bottom : 10 px ;
font - size : 13 px ;
color : # 666 ;
. dot {
width : 12 px ;
height : 12 px ;
border - radius : 50 % ;
margin - right : 8 px ;
border : 2 px solid # fff ;
background - color : # 5 B8FF9 ;
box - shadow : 0 0 6 px rgba ( 91 , 143 , 249 , 0.5 ) ;
animation : pulse 2 s ease - in - out infinite ;
}
}
}
}
@ keyframes pulse {
0 % , 100 % {
transform : scale ( 1 ) ;
opacity : 1 ;
}
50 % {
transform : scale ( 1.1 ) ;
opacity : 0.8 ;
}
}
}
// 3D标记样式
: deep ( . marker - 3 d ) {
cursor : pointer ;
transform - origin : center bottom ;
. marker - container {
position : relative ;
width : 40 px ;
height : 48 px ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. pulse - ring {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) ;
width : 50 px ;
height : 50 px ;
border : 2 px solid # 5 B8FF9 ;
border - radius : 50 % ;
opacity : 0 ;
animation : pulse - ring 2 s cubic - bezier ( 0.4 , 0 , 0.6 , 1 ) infinite ;
}
. outer - glow {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) ;
width : 42 px ;
height : 42 px ;
border - radius : 50 % ;
background : radial - gradient ( circle , rgba ( 91 , 143 , 249 , 0.4 ) 0 % , transparent 70 % ) ;
animation : glow - pulse 2 s ease - in - out infinite ;
}
. marker - pin {
position : relative ;
width : 28 px ;
height : 28 px ;
border - radius : 50 % 50 % 50 % 0 ;
transform : rotate ( - 45 deg ) ;
background : linear - gradient ( 135 deg , # 5 B8FF9 0 % , # 7 BA3FF 100 % ) ;
border : 2 px solid rgba ( 255 , 255 , 255 , 0.95 ) ;
box - shadow :
0 3 px 6 px rgba ( 0 , 0 , 0 , 0.3 ) ,
0 6 px 12 px rgba ( 0 , 0 , 0 , 0.2 ) ,
0 0 0 1 px rgba ( 255 , 255 , 255 , 0.1 ) ,
inset 0 - 2 px 3 px rgba ( 0 , 0 , 0 , 0.2 ) ,
inset 0 2 px 3 px rgba ( 255 , 255 , 255 , 0.3 ) ;
transition : all 0.3 s ease ;
z - index : 2 ;
}
. marker - glow {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) ;
width : 120 % ;
height : 120 % ;
border - radius : 50 % ;
box - shadow : 0 0 20 px rgba ( 91 , 143 , 249 , 0.5 ) , 0 0 35 px rgba ( 91 , 143 , 249 , 0.3 ) ;
animation : glow - pulse 2 s ease - in - out infinite ;
}
. inner - shine {
position : absolute ;
top : 20 % ;
left : 20 % ;
width : 40 % ;
height : 40 % ;
background : radial - gradient ( circle , rgba ( 255 , 255 , 255 , 0.6 ) 0 % , transparent 70 % ) ;
border - radius : 50 % ;
transform : rotate ( 45 deg ) ;
}
. marker - icon {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) rotate ( 45 deg ) ;
width : 16 px ;
height : 16 px ;
filter : drop - shadow ( 0 1 px 2 px rgba ( 0 , 0 , 0 , 0.3 ) ) ;
}
. marker - shadow {
position : absolute ;
bottom : - 5 px ;
left : 50 % ;
transform : translateX ( - 50 % ) ;
width : 20 px ;
height : 8 px ;
background : radial - gradient ( ellipse at center , rgba ( 0 , 0 , 0 , 0.35 ) 0 % , transparent 70 % ) ;
border - radius : 50 % ;
animation : shadow - pulse 3 s ease - in - out infinite ;
}
. status - ring {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) ;
width : 38 px ;
height : 38 px ;
border : 2 px solid # 5 B8FF9 ;
border - radius : 50 % ;
opacity : 0.4 ;
animation : status - ring - pulse 2 s ease - in - out infinite ;
}
& : hover {
. marker - pin {
transform : rotate ( - 45 deg ) scale ( 1.1 ) ;
box - shadow :
0 4 px 8 px rgba ( 0 , 0 , 0 , 0.4 ) ,
0 8 px 16 px rgba ( 0 , 0 , 0 , 0.3 ) ,
0 0 0 2 px rgba ( 255 , 255 , 255 , 0.2 ) ,
inset 0 - 2 px 3 px rgba ( 0 , 0 , 0 , 0.2 ) ,
inset 0 2 px 3 px rgba ( 255 , 255 , 255 , 0.4 ) ;
}
. status - ring {
animation : status - ring - hover 0.6 s ease - in - out infinite ;
}
}
@ keyframes pulse - ring {
0 % {
transform : translate ( - 50 % , - 50 % ) scale ( 0.75 ) ;
opacity : 0.8 ;
}
50 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1.2 ) ;
opacity : 0.4 ;
}
100 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1.6 ) ;
opacity : 0 ;
}
}
@ keyframes glow - pulse {
0 % , 100 % {
opacity : 0.6 ;
transform : translate ( - 50 % , - 50 % ) scale ( 1 ) ;
}
50 % {
opacity : 0.9 ;
transform : translate ( - 50 % , - 50 % ) scale ( 1.08 ) ;
}
}
@ keyframes shadow - pulse {
0 % , 100 % {
transform : translateX ( - 50 % ) scale ( 1 ) ;
opacity : 0.4 ;
}
50 % {
transform : translateX ( - 50 % ) scale ( 0.88 ) ;
opacity : 0.25 ;
}
}
@ keyframes status - ring - pulse {
0 % , 100 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1 ) ;
opacity : 0.5 ;
}
50 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1.12 ) ;
opacity : 0.25 ;
}
}
@ keyframes status - ring - hover {
0 % , 100 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1.08 ) ;
opacity : 0.7 ;
}
50 % {
transform : translate ( - 50 % , - 50 % ) scale ( 1.2 ) ;
opacity : 0.35 ;
}
}
}
// 点位信息弹出框样式
: deep ( . el - dialog ) {
border - radius : 12 px ;
overflow : hidden ;
box - shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.12 ) ;
. el - dialog _ _header {
background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 100 % ) ;
padding : 24 px 30 px ;
margin : 0 ;
border - bottom : none ;
. el - dialog _ _title {
color : # fff ;
font - size : 18 px ;
font - weight : 600 ;
letter - spacing : 0.5 px ;
}
. el - dialog _ _headerbtn {
top : 24 px ;
right : 30 px ;
width : 32 px ;
height : 32 px ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border - radius : 50 % ;
transition : all 0.3 s ease ;
& : hover {
background : rgba ( 255 , 255 , 255 , 0.3 ) ;
transform : rotate ( 90 deg ) ;
}
. el - dialog _ _close {
color : # fff ;
font - size : 18 px ;
font - weight : bold ;
}
}
}
. el - dialog _ _body {
padding : 32 px 30 px ;
background : # fff ;
}
}
. point - info {
. info - row {
display : flex ;
align - items : center ;
margin - bottom : 18 px ;
padding : 16 px 18 px ;
background : linear - gradient ( 135 deg , # f8f9fa 0 % , # f1f3f5 100 % ) ;
border - radius : 8 px ;
border - left : 4 px solid # 667 eea ;
transition : all 0.3 s cubic - bezier ( 0.4 , 0 , 0.2 , 1 ) ;
position : relative ;
overflow : hidden ;
& : : before {
content : '' ;
position : absolute ;
left : 0 ;
top : 0 ;
height : 100 % ;
width : 0 ;
background : linear - gradient ( 90 deg , rgba ( 102 , 126 , 234 , 0.08 ) 0 % , transparent 100 % ) ;
transition : width 0.3 s ease ;
}
& : last - child {
margin - bottom : 0 ;
}
& : hover {
background : linear - gradient ( 135 deg , # fff 0 % , # f8f9fa 100 % ) ;
transform : translateX ( 4 px ) ;
box - shadow : 0 4 px 12 px rgba ( 102 , 126 , 234 , 0.15 ) ;
border - left - color : # 764 ba2 ;
& : : before {
width : 100 % ;
}
}
. label {
font - weight : 600 ;
color : # 495057 ;
min - width : 95 px ;
font - size : 14 px ;
display : flex ;
align - items : center ;
position : relative ;
& : : after {
content : ':' ;
margin - left : 4 px ;
color : # 667 eea ;
}
}
. value {
color : # 212529 ;
font - size : 14 px ;
flex : 1 ;
word - break : break - all ;
font - family : 'Consolas' , 'Monaco' , monospace ;
letter - spacing : 0.3 px ;
}
}
}
< / style >