【类 型】:feat

【原  因】:飞机在poprp弹出框 实时显示飞机状态  包括集群控制  和  概念页面
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
This commit is contained in:
air 2025-06-23 21:13:50 +08:00
parent 8d06b53183
commit 356ea21847
9 changed files with 191 additions and 286 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/dist
/package-lock.json
/src/components/statistics.vue
/src/components/SwarmStatus.vue

View File

@ -882,19 +882,30 @@ export default {
*/
makePlane (plane, index = 0) {
const customIcon = document.createElement('div')
customIcon.className = 'custom-marker' //
customIcon.style.backgroundImage = `url(${planeIcon})` // 使 SVG
customIcon.style.width = '64px' //
customIcon.style.height = '64px' //
// marker
this.planes[index] = new mapboxgl.Marker(customIcon)
customIcon.className = 'custom-marker'
customIcon.style.backgroundImage = `url(${planeIcon})`
customIcon.style.width = '64px'
customIcon.style.height = '64px'
const marker = new mapboxgl.Marker(customIcon)
.setLngLat([plane.lon, plane.lat])
.setPopup(
new mapboxgl.Popup({ offset: 25 }).setHTML(
'<h3>' + plane.name + '</h3><hr><p>macID:' + plane.macadd + '</p>'
)
) //
.addTo(this.map)
// popupmarker
const popup = new mapboxgl.Popup({
offset: 25,
closeButton: true,
closeOnClick: true
}).setHTML(`<h3>${plane.name}</h3>`)
marker.setPopup(popup)
// popuppopup
customIcon.addEventListener('click', (e) => {
e.stopPropagation() // click
popup.setLngLat([plane.lon, plane.lat]).addTo(this.map)
})
this.planes[index] = marker // marker
},
/**
* @description: 移除页面上的所有飞机
@ -979,6 +990,70 @@ export default {
})
}
},
/**
* @description 实时更新地图上指定飞机图标的弹窗内容Popup
* @param {Object} stateObj 飞机状态对象包含飞机的状态信息
* @param {number} index 飞机对象索引 ps:用这个索引 取本组件组件共有变量planes对象组 指定对象
*/
updatePopupContent (stateObj, index = 0) {
const plane = this.planes[index]
if (!plane) return
//
const lastPos = stateObj.position?.[stateObj.position.length - 1] ?? []
//
const heartClass = stateObj.online
? 'heart-icon heart-online'
: 'heart-icon heart-offline'
// HTML
const popupContent = `
<style>
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.3); }
}
.heart-icon {
display: inline-block;
width: 12px;
height: 12px;
margin-left: 5px;
border-radius: 50%;
}
.heart-online {
background-color: green;
animation: heartbeat 1s infinite;
}
.heart-offline {
background-color: gray;
}
</style>
<h3>${stateObj.name}</h3>
<hr>
<p><strong>心跳:</strong> <span class="${heartClass}"></span></p>
<p><strong>解锁状态:</strong>
<span style="color:${stateObj.isUnlock ? 'green' : ''}">
${stateObj.isUnlock ? '已解锁' : '未解锁'}
</span>
</p>
<p><strong>模式:</strong> ${stateObj.getPlaneMode ?? '未知'}</p>
<p><strong>定位状态:</strong> ${stateObj.fixType ?? '--'}</p>
<p><strong>卫星颗数:</strong> ${stateObj.satCount ?? '--'}</p>
<p><strong>电压:</strong> ${stateObj.voltagBattery ?? '--'} V</p>
<p><strong>电流:</strong> ${stateObj.currentBattery ?? '--'} A</p>
<p><strong>高度:</strong> ${lastPos[2] ?? '--'} </p>
<p><strong>纬度:</strong> ${lastPos[0] ?? '--'}°</p>
<p><strong>经度:</strong> ${lastPos[1] ?? '--'}°</p>
<p><strong>对地速度:</strong> ${stateObj.groundSpeed ?? '--'} /</p>
`
const popup = plane.getPopup()
if (popup) {
popup.setHTML(popupContent)
}
},
/**
* @description: 镜头跳转
* @param {obj} lonLat {lon:lon,lat:lat} 经纬度
@ -1047,4 +1122,27 @@ export default {
.adsb-icon {
will-change: transform;
}
/*飞机popup 心跳图标样式*/
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.3); }
}
.heart-icon {
display: inline-block;
width: 16px;
height: 16px;
margin-left: 5px;
border-radius: 50%;
}
.heart-online {
background-color: green;
animation: heartbeat 1s infinite;
}
.heart-offline {
background-color: gray;
}
</style>

View File

@ -2,11 +2,7 @@
<div class="mainBox flex column no-select">
<!-- 心跳 ps:黑色只网络通 绿色飞控有效心跳-->
<div class="flex">
<div class="tag flex mac mc iconfont" :class="[
online ? (heartAnimation ? 'icon-heart online' : 'icon-heart1 online') : 'icon-xinsui offline',
plane.heartBeat ? 'useful-heart' : ''
]">
</div>
<div class="tag flex mac mc iconfont" :class="[heartIconClass, usefulHeartClass]"></div>
</div>
<!-- 解锁状态 -->
<div class="flex">
@ -95,7 +91,10 @@ export default {
computed: {
// 线
online () {
return this.plane?.online ?? false
if (this.plane && this.plane.planeState) {
return this.plane.planeState.online
}
return false
},
//
heartRandom () {
@ -104,9 +103,21 @@ export default {
}
return null
},
heartIconClass () {
if (!this.online) {
return 'icon-xinsui offline'
}
return this.heartAnimation ? 'icon-heart online' : 'icon-heart1 online'
},
usefulHeartClass () {
return this.online && this.plane.planeState.heartBeat ? 'useful-heart' : ''
},
//
isUnlock () {
return this.plane?.planeState?.isUnlock ?? false
if (this.plane && this.plane.planeState) {
return this.plane.planeState.isUnlock
}
return false
},
//
getPlaneMode () {

View File

@ -48,7 +48,7 @@ export default {
},
props: {
planes: {
type: Object,
type: Array,
deep: true
}
},

View File

@ -1,254 +0,0 @@
<template>
<div class="mainBox flex column no-select">
<!-- 心跳 -->
<div class="flex">
<div class="tag flex mac mc iconfont"
:class="online ? heartAnimation ? 'icon-heart online' : 'icon-heart1 online' : 'icon-xinsui offline'">
</div>
</div>
<!-- 锁定状态 -->
<div class="flex">
<div class="tag flex mac mc iconfont" :class="isLockState ? 'icon-suoding' : 'icon-jiesuo'">
</div>
</div>
<!-- 飞机模式 -->
<div class="flex">
<div v-if="getPlaneMode" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ getPlaneMode }}</font>
</div>
<div class="tag flex mac mc iconfont icon-moshixuanze">
</div>
</div>
<!-- 卫星 -->
<div class="flex">
<div v-if="satCount" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ fixType }} {{ satCount }}</font>
</div>
<div class="tag flex mac mc iconfont icon-weixing">
</div>
</div>
<!-- 电池电压 -->
<div class="flex">
<div v-if="voltagBattery" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ voltagBattery }}V</font>
</div>
<div class="tag flex mac mc iconfont icon-dianya1">
</div>
</div>
<!-- 电池电流 -->
<div class="flex">
<div v-if="currentBattery" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ currentBattery }}A</font>
</div>
<div class="tag flex mac mc iconfont icon-dianliu">
</div>
</div>
<!-- 飞机高度 -->
<div class="flex">
<div v-if="positionAlt" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ positionAlt }}</font>
</div>
<div class="tag flex mac mc iconfont icon-gaodu">
</div>
</div>
<!-- 飞机对地速度 -->
<div class="flex">
<div v-if="groundSpeed" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{ groundSpeed }}/</font>
</div>
<div class="tag flex mac mc iconfont icon-sudu">
</div>
</div>
<!-- 飞机载重 钩子状态 -->
<div class="flex">
<div v-if="loadweight" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{hookstatus}} {{ loadweight }}</font>
</div>
<div class="tag flex mac mc iconfont icon-mianxingdiaogou">
</div>
</div>
</div>
</template>
<script>
export default {
name: 'PlaneStatus',
data () {
return {
/* 心跳 */
heartAnimation: false, //
online: false,
isOnlineSetTimeout: null
}
},
props: {
plane: {
type: Object,
deep: true
}
},
components: {
},
computed: {
//
heartRandom () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.heartRandom
}
return null
},
//
isLockState () {
if (this.plane && this.plane.planeState) {
if (Number(this.plane.planeState.heartBeat) & 128) {
return false
}
}
return true
},
//
getPlaneMode () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.getPlaneMode
}
return null
},
//
satCount () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.satCount
}
return null
},
//
fixType () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.fixType
}
return null
},
//
voltagBattery () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.voltagBattery
}
return null
},
//
currentBattery () {
if (this.plane && this.plane.planeState) {
if (Number(this.plane.planeState.currentBattery) > 0) {
return this.plane.planeState.currentBattery
} else {
return 0
}
}
return null
},
//
positionAlt () {
if (this.plane && this.plane.planeState && this.plane.planeState.position.length > 0) {
const posLen = this.plane.planeState.position.length
return this.plane.planeState.position[posLen - 1][2]
}
return null
},
//
groundSpeed () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.groundSpeed
}
return null
},
//
hookstatus () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.hookstatus
}
return null
},
//
loadweight () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState.loadweight
}
return null
}
},
watch: {
heartRandom: {
handler () {
console.log('心跳:', this.plane.heartBeat)
//
this.heartAnimation = true
setTimeout(() => {
this.heartAnimation = false
}, 500)
// 线
if (this.isOnlineSetTimeout) { // 线
clearInterval(this.isOnlineSetTimeout)
}
this.online = true
this.isOnlineSetTimeout = setTimeout(() => { // 10 线
this.online = false
}, 10000)
}
}
},
methods: {
},
created () {
},
destroyed () {
if (this.isOnlineSetTimeout) {
clearInterval(this.isOnlineSetTimeout)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.mainBox {
position: absolute;
width: 29px;
top: 40px;
left: 10px;
z-index: 90;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
background-color: white;
border-radius: 4px;
}
.mainBox .flex:not(:first-child) {
border-top: 1px solid #ddd;
}
.tag {
height: 29px;
min-width: 29px;
cursor: pointer;
border: 0;
font-size: 22px;
}
.plane-mode {
position: absolute;
left: 29px;
height: 29px;
display: flex;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
background-color: rgba(255, 255, 255, 0.5);
}
.plane-mode-text {
display: inline-block;
white-space: nowrap; /* 防止内容换行 */
}
</style>

View File

@ -30,6 +30,10 @@ export default {
airList () {
return this.$store.state.airList
},
// s
planeStatus () {
return this.airList.map(plane => plane.planeState)
},
/**
* @description: 侧边栏显隐
*/
@ -82,6 +86,16 @@ export default {
},
immediate: true
},
//
planeStatus: {
handler (val) {
val.forEach((stateObj, index) => {
stateObj.name = this.airList[index].name //
this.$refs.mapbox.updatePopupContent(stateObj, index)
})
},
deep: true
},
/**
* @description: 侧边栏显隐
*/

View File

@ -48,16 +48,18 @@ export default {
currentLang: this.$store.state.settings.language || 'zh-CN',
moduleOptions: [
{ value: 'home', label: '概况' },
{ value: 'model', label: '机型管理' },
{ value: 'register', label: '飞机管理' },
{ value: 'model', label: '机型列表' },
{ value: 'register', label: '飞机列表' },
{ value: 'nofly', label: '飞行限制' },
{ value: 'route', label: '航线管理' },
{ value: 'site', label: '站点管理' },
{ value: 'planes', label: '无人机' },
{ value: 'shop', label: '商铺管理' },
{ value: 'admin', label: '管理员管理' },
{ value: 'admin', label: '账户列表' },
{ value: 'category', label: '分类管理' },
{ value: 'product', label: '商品管理' }
{ value: 'product', label: '商品管理' },
{ value: 'broadcast', label: '广告管理' },
{ value: 'order', label: '订单与统计' }
],
selectedModules: []
}

View File

@ -80,6 +80,13 @@ export default {
}
return null
},
planeState () {
if (this.plane && this.plane.planeState) {
return this.plane.planeState
} else {
return {}
}
},
position () {
if (this.plane) {
if (this.plane.planeState.position.length > 0) {
@ -211,6 +218,7 @@ export default {
mounted () {
},
watch: {
// deep:true
plane: {
async handler () {
try {
@ -218,12 +226,24 @@ export default {
await waitForMapCanvasReady(this.$refs.mapbox.map)
//
this.onMapReady()
console.warn('飞机数据更新', this.plane)
} catch (err) {
console.debug('等待地图画布准备超时', err)
}
},
immediate: true
},
//
planeState: {
handler (val) {
if (val) {
val.name = this.plane.name //
// popup
this.$refs.mapbox.updatePopupContent(val)
}
},
deep: true
},
/**
* @description: 更新飞机位置 并画出轨迹 跟随飞机
*/

View File

@ -1,11 +1,10 @@
<template>
<div class="h-100">
<!-- 地图组件 -->
<map-box ref="mapbox" :enableShowNofly="true" :enableGuided="true" :enblueScale="!$store.state.app.isWideScreen"
<map-box ref="mapbox" v-if="swarmReady" :enableShowNofly="true" :enableGuided="true" :enblueScale="!$store.state.app.isWideScreen"
@longPress="handleLongPress" @map-ready="onMapReady">
<template #content>
<div v-show="mapReady">
<!-- <SwarmStatus :planes="planeList" /> -->
<SwarmControllerTabs :planes="planeList"/>
</div>
</template>
@ -38,7 +37,6 @@
<script>
import MapBox from '@/components/MapBox'
// import SwarmStatus from '@/components/SwarmStatus.vue'
import SwarmControllerTabs from '@/components/SwarmControllerTabs.vue'
import { waitForMapCanvasReady } from '@/utils'
@ -52,12 +50,12 @@ export default {
guidedLonLat: {}, //
guidedAlt: '', //
isReserveGuidedMaker: false, //
mapReady: false //
mapReady: false, //
swarmReady: false//
}
},
components: {
MapBox,
// SwarmStatus,
SwarmControllerTabs
},
computed: {
@ -67,6 +65,10 @@ export default {
const idArr = this.$store.state.app.swarmIdArr
return allPlanes.filter(plane => idArr.includes(plane.id))
},
// s
planeStatus () {
return this.planeList.map(plane => plane.planeState)
},
/**
* @description: 侧边栏显隐
*/
@ -75,9 +77,11 @@ export default {
}
},
created () {
//
// 2
if (this.$store.state.app.swarmIdArr.length < 2) {
this.$router.replace('/register/index')
this.$router.replace('/register/index')// 2
} else {
this.swarmReady = true // <map-box>
}
},
methods: {
@ -93,7 +97,6 @@ export default {
openCallback () {
},
//
//
handleLongPress (lonLat) {
this.isReserveGuidedMaker = false
this.dialogTitle = '集群指点'
@ -165,7 +168,7 @@ export default {
this.isReserveGuidedMaker = true
this.dialogVisible = false
},
//
//
getSwarmCenter () {
const positions = this.planeList.map(p => {
const pos = p?.planeState?.position
@ -231,6 +234,16 @@ export default {
},
immediate: true
},
//
planeStatus: {
handler (val) {
val.forEach((stateObj, index) => {
stateObj.name = this.planeList[index].name //
this.$refs.mapbox.updatePopupContent(stateObj, index)
})
},
deep: true
},
/**
* @description: 侧边栏显隐
*/