【类型】:feat

【原因】:要把 ADS-B 的飞行器数据实时显示在地图上
【过程】:监听后台推送的 ADS-B 数据,拿到经纬度后在地图上添加飞行器图标,并定时更新图标位置;支持多个飞行器同时显示,还加了去重和更新逻辑,避免重复创建标记
【影响】:
This commit is contained in:
air 2025-06-20 15:03:24 +08:00
parent fc4a3d3029
commit f55ec12808
5 changed files with 134 additions and 22 deletions

View File

@ -5,8 +5,11 @@
@open="openCallback">
<!-- 起飞设置弹出框 -->
<template v-if="dialogItem == 'takeoffBox'">
<el-slider class="w-100" v-model="takeoffValue" :show-tooltip="false" show-input :min="1" :max="100">
</el-slider>
<div class="flex mse mac">
<el-slider class="w-100" v-model="takeoffValue" :show-tooltip="false" show-input :min="1" :max="100">
</el-slider>
<font class="m-l-5"></font>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="medium" @click="dialogVisible = false">关闭</el-button>
<el-button size="medium" type="primary"
@ -214,7 +217,7 @@
<div class="m-t-5">加锁</div>
</el-button>
<el-button size="medium" type="primary" class="flex1 butIcon"
@click="dialogVisible = true; dialogTitle = '高度设置(米)'; dialogItem = 'takeoffBox'; speakText('设置起飞高度')">
@click="dialogVisible = true; dialogTitle = '高度设置'; dialogItem = 'takeoffBox'; speakText('设置起飞高度')">
<i class="iconfont icon-yangshi_icon_tongyong_departure f-s-24"></i>
<div class="m-t-5">起飞</div>
</el-button>

View File

@ -8,6 +8,7 @@
import mapboxgl from 'mapbox-gl'
import { MapboxStyleSwitcherControl, FollowControl, CustomFullscreenControl, NoFlyControl, RestrictflyControl, SaveToFileControl, PolygonToggleControl } from '@/utils/mapboxgl_plugs'
import planeIcon from '@/assets/svg/plane.svg'
import civilIcon from '@/assets/svg/civil.svg'
export default {
name: 'MapBox',
@ -15,6 +16,7 @@ export default {
return {
token: 'pk.eyJ1Ijoic3pkb3QiLCJhIjoiY2xhN2pkMWFoMHJ4eTN3cXp6bmlzaHZ0NCJ9.3hH-EAUr0wQCaLvIM2lBMQ',
map: null,
ADSBMarkers: [], // ADSB
planes: [], //
lonLats: [], // 线
wayLonLats: [], // 线
@ -252,8 +254,8 @@ export default {
const dy = currentPoint.y - startPoint.y
if (Math.sqrt(dx * dx + dy * dy) <= 5 && isLongPress) { //
const lonLat = {
lon: this.map.unproject(currentPoint).lng,
lat: this.map.unproject(currentPoint).lat
lon: Number(this.map.unproject(currentPoint).lng.toFixed(7)),
lat: Number(this.map.unproject(currentPoint).lat.toFixed(7))
}
this.$emit('longPress', lonLat) //
this.makeGuidedMarker(lonLat) //
@ -864,6 +866,54 @@ export default {
})
this.planes = []
},
/**
* @description: 创建 ADSB 飞机图标并添加到地
* @param {Array} adsbList - ADSB 飞机数据列表
*/
makeADSBPlanes (adsbList = []) {
//
if (this.ADSBMarkers && this.ADSBMarkers.length) {
this.ADSBMarkers.forEach(marker => marker.remove())
}
this.ADSBMarkers = []
adsbList.forEach((plane) => {
//
const iconWrapper = document.createElement('div')
iconWrapper.className = 'adsb-wrapper'
iconWrapper.style.width = '36px'
iconWrapper.style.height = '36px'
// transiton
const planeIcon = document.createElement('div')
planeIcon.className = 'adsb-icon'
planeIcon.style.backgroundImage = `url(${civilIcon})`
planeIcon.style.width = '100%'
planeIcon.style.height = '100%'
planeIcon.style.backgroundSize = 'contain'
planeIcon.style.backgroundRepeat = 'no-repeat'
planeIcon.style.transform = `rotate(${plane.heading}deg)`
planeIcon.style.transition = 'transform 0.3s linear'
//
iconWrapper.appendChild(planeIcon)
const popupContent = `
<h3>ADSB 飞机</h3>
<hr>
<p>ICAO: ${plane.icao}</p>
<p>高度: ${plane.alt} m</p>
<p>航向: ${plane.heading}°</p>
`
const marker = new mapboxgl.Marker(iconWrapper)
.setLngLat([plane.lon, plane.lat])
.setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(popupContent))
.addTo(this.map)
this.ADSBMarkers.push(marker)
})
},
/**
* @description: 实时更新经纬度
* @param {obj} lonLat lon经度 lat纬度
@ -950,4 +1000,14 @@ export default {
cursor: pointer;
}
.adsb-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.adsb-icon {
will-change: transform;
}
</style>

View File

@ -25,7 +25,8 @@ const store = new Vuex.Store({
paidOrderList: [], // 已付款 和已退款但是发货状态为 已发货 订单列表
logList: [], // 操作日志列表
messageList: [], // 管理员公告列表
crosFrequency: null// 对频macadd
crosFrequency: null, // 对频macadd
ADSBList: [] // 存放当前活跃的 ADSB 飞机数据
},
mutations: {
/**
@ -130,6 +131,27 @@ const store = new Vuex.Store({
*/
setCrosFrequency (state, macAdd) {
state.crosFrequency = macAdd
},
/**
* @description: 设置当前活跃的 ADSB 飞机数据
* @param {*} ADSB
*/
setADSBList (state, ADSB) {
const index = state.ADSBList.findIndex(i => i.icao === ADSB.icao)
if (index > -1) {
// 更新已有
state.ADSBList[index] = { ...ADSB, timestamp: Date.now() }
} else {
// 新增
state.ADSBList.push({ ...ADSB, timestamp: Date.now() })
}
},
/**
* @description: 清除过期的 ADSB 数据
*/
cleanExpiredADSBList (state) {
const now = Date.now()
state.ADSBList = state.ADSBList.filter(i => now - i.timestamp < 20000)
}
},
actions: {

View File

@ -15,10 +15,17 @@
<!-- 点飞设置弹出框 -->
<template v-if="dialogItem == 'guidedBox'">
<el-form label-position="left">
<el-form-item label="纬度" label-width="80px">
<el-input v-model="guidedLonLat.lat" placeholder="请输维度" label="纬度"></el-input>
</el-form-item>
<el-form-item label="经度" label-width="80px">
<el-input v-model="guidedLonLat.lon" placeholder="请输经度" label="经度"></el-input>
</el-form-item>
<el-form-item label="高度设置" label-width="80px">
<el-input-number v-model="guidedAlt" label="高度设置"></el-input-number>
<font class="m-l-5"></font>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button size="medium" @click="dialogVisible = false">关闭</el-button>
@ -47,8 +54,6 @@ export default {
guidedLonLat: {}, //
guidedAlt: '', //
isReserveGuidedMaker: false, //
showMapbox: true, //
mapboxKey: 0, // map-box
planesId: this.$route.params.id,
localCount: 0 //
}
@ -80,6 +85,9 @@ export default {
noflyData () {
return this.$store.state.noflyData
},
ADSBList () {
return this.$store.state.ADSBList
},
/**
* @description: 侧边栏显隐
*/
@ -185,6 +193,15 @@ export default {
},
deep: true
},
ADSBList: {
handler (newList) {
if (this.$refs.mapbox && typeof this.$refs.mapbox.makeADSBPlanes === 'function') {
this.$refs.mapbox.makeADSBPlanes(newList)
}
},
immediate: true,
deep: true
},
/**
* @description: 侧边栏缩进有变化时 地图重新自适应
*/

View File

@ -101,22 +101,31 @@ export default {
}
},
mounted () {
// ADSB
setInterval(() => {
if (this.$store.state.ADSBList.length > 0) {
this.$store.commit('cleanExpiredADSBList')
}
}, 5000)
},
created () {
/* init */
this.$store.commit('app/setIsMobile')//
this.$store.dispatch('fetchPlaneClassList')//
this.$store.dispatch('fetchAirList')//
this.$store.dispatch('fetchShopList')//
this.$store.dispatch('fetchAdminList')//
this.$store.dispatch('fetchSiteList')//
this.$store.dispatch('fetchRouteList')// 线
this.$store.dispatch('fetchNoflyData', this.shop_id)// shopid
this.$store.dispatch('fetchCategoryList')//
this.$store.dispatch('fetchSpuList')// spu
this.$store.dispatch('fetchSkuList')// sku
this.$store.dispatch('fetchPaidOrderList')//
this.$store.dispatch('fetchMessageList')//
this.$store.commit('app/setIsMobile')
Promise.all([
this.$store.dispatch('fetchPlaneClassList'),
this.$store.dispatch('fetchAirList'),
this.$store.dispatch('fetchShopList'),
this.$store.dispatch('fetchAdminList'),
this.$store.dispatch('fetchSiteList'),
this.$store.dispatch('fetchRouteList'),
this.$store.dispatch('fetchCategoryList'),
this.$store.dispatch('fetchSpuList'),
this.$store.dispatch('fetchSkuList'),
this.$store.dispatch('fetchPaidOrderList'),
this.$store.dispatch('fetchMessageList')
]).then(() => {
return this.$store.dispatch('fetchNoflyData', this.shop_id)
})
},
watch: {
/**
@ -211,7 +220,8 @@ export default {
// ADSB
mqtt.doSubscribe('ADSB/+', (res) => {
if (res.topic.indexOf('ADSB') > -1) {
const data = JSON.parse(res.msg)
this.$store.commit('setADSBList', data)
}
})
//