【类 型】:feat

【原  因】:
【过  程】:1.优化飞机状态组件 在飞将列表里面加入 在线字段 解锁字段 2.增加概况多架飞机 状态组件
【影  响】:

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

View File

@ -8,6 +8,7 @@
import mapboxgl from 'mapbox-gl' import mapboxgl from 'mapbox-gl'
import { MapboxStyleSwitcherControl, FollowControl, CustomFullscreenControl, NoFlyControl, RestrictflyControl, SaveToFileControl, PolygonToggleControl } from '@/utils/mapboxgl_plugs' import { MapboxStyleSwitcherControl, FollowControl, CustomFullscreenControl, NoFlyControl, RestrictflyControl, SaveToFileControl, PolygonToggleControl } from '@/utils/mapboxgl_plugs'
import planeIcon from '@/assets/svg/plane.svg' import planeIcon from '@/assets/svg/plane.svg'
// import unlineIcon from '@/assets/svg/plane_unline.svg'
import civilIcon from '@/assets/svg/civil.svg' import civilIcon from '@/assets/svg/civil.svg'
export default { export default {

View File

@ -1,15 +1,16 @@
<template> <template>
<div class="mainBox flex column no-select"> <div class="mainBox flex column no-select">
<!-- 心跳 --> <!-- 心跳 ps:黑色只网络通 绿色飞控有效心跳-->
<div class="flex"> <div class="flex">
<div class="tag flex mac mc iconfont" <div class="tag flex mac mc iconfont" :class="[
:class="online ? heartAnimation ? 'icon-heart online' : 'icon-heart1 online' : 'icon-xinsui offline'"> online ? (heartAnimation ? 'icon-heart online' : 'icon-heart1 online') : 'icon-xinsui offline',
plane.heartBeat ? 'useful-heart' : ''
]">
</div> </div>
</div> </div>
<!-- 状态 --> <!-- 锁状态 -->
<div class="flex"> <div class="flex">
<div class="tag flex mac mc iconfont" :class="isLockState ? 'icon-suoding' : 'icon-jiesuo'"> <div class="tag flex mac mc iconfont" :class="isUnlock ? 'icon-jiesuo' : 'icon-suoding'"></div>
</div>
</div> </div>
<!-- 飞机模式 --> <!-- 飞机模式 -->
<div class="flex"> <div class="flex">
@ -62,7 +63,9 @@
<!-- 飞机载重 钩子状态 --> <!-- 飞机载重 钩子状态 -->
<div class="flex"> <div class="flex">
<div v-if="loadweight" class="plane-mode p-l-5 p-r-5 mc mac"> <div v-if="loadweight" class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">{{hookstatus}} {{ loadweight }}</font> <font class="plane-mode-text" v-if="hookstatus || loadweight">
{{ hookstatus || '' }} {{ loadweight ? loadweight + '克' : '' }}
</font>
</div> </div>
<div class="tag flex mac mc iconfont icon-mianxingdiaogou"> <div class="tag flex mac mc iconfont icon-mianxingdiaogou">
</div> </div>
@ -78,9 +81,7 @@ export default {
data () { data () {
return { return {
/* 心跳 */ /* 心跳 */
heartAnimation: false, // heartAnimation: false //
online: false,
isOnlineSetTimeout: null
} }
}, },
props: { props: {
@ -92,6 +93,10 @@ export default {
components: { components: {
}, },
computed: { computed: {
// 线
online () {
return this.plane?.online ?? false
},
// //
heartRandom () { heartRandom () {
if (this.plane && this.plane.planeState) { if (this.plane && this.plane.planeState) {
@ -99,14 +104,9 @@ export default {
} }
return null return null
}, },
// //
isLockState () { isUnlock () {
if (this.plane && this.plane.planeState) { return this.plane?.planeState?.isUnlock ?? false
if (Number(this.plane.planeState.heartBeat) & 128) {
return false
}
}
return true
}, },
// //
getPlaneMode () { getPlaneMode () {
@ -180,20 +180,11 @@ export default {
watch: { watch: {
heartRandom: { heartRandom: {
handler () { handler () {
console.log('心跳:', this.plane.heartBeat)
// //
this.heartAnimation = true this.heartAnimation = true
setTimeout(() => { setTimeout(() => {
this.heartAnimation = false this.heartAnimation = false
}, 500) }, 500)
// 线
if (this.isOnlineSetTimeout) { // 线
clearInterval(this.isOnlineSetTimeout)
}
this.online = true
this.isOnlineSetTimeout = setTimeout(() => { // 10 线
this.online = false
}, 10000)
} }
} }
}, },
@ -203,9 +194,6 @@ export default {
created () { created () {
}, },
destroyed () { destroyed () {
if (this.isOnlineSetTimeout) {
clearInterval(this.isOnlineSetTimeout)
}
} }
} }
@ -249,6 +237,11 @@ export default {
.plane-mode-text { .plane-mode-text {
display: inline-block; display: inline-block;
white-space: nowrap; /* 防止内容换行 */ white-space: nowrap;
/* 防止内容换行 */
}
.useful-heart {
color: $success-color;
} }
</style> </style>

View File

@ -1,17 +1,44 @@
<template> <template>
<div class="mainBox flex column no-select"> <div class="mainBox flex column no-select">
<!-- 心跳 --> <div class="flex stat-row">
<!-- <div class="flex"> <div class="plane-mode p-l-5 p-r-5 mc mac">
<div class="tag flex mac mc iconfont" <font class="plane-mode-text">总架数{{ totalCount }}</font>
:class="online ? heartAnimation ? 'icon-heart online' : 'icon-heart1 online' : 'icon-xinsui offline'">
</div> </div>
</div> --> <div class="tag flex mac mc iconfont icon-zongshu"></div>
</div> </div>
<div class="flex stat-row">
<div class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">在线数{{ onlineCount }}</font>
</div>
<div class="tag flex mac mc iconfont icon-zaixian1"></div>
</div>
<div class="flex stat-row">
<div class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">总作业架数{{ unlockedCount }}</font>
</div>
<div class="tag flex mac mc iconfont icon-wurenjijiesuo"></div>
</div>
<div class="flex stat-row">
<div class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">总作业时长{{ formattedDuration }}</font>
</div>
<div class="tag flex mac mc iconfont icon-shichang"></div>
</div>
<div class="flex stat-row">
<div class="plane-mode p-l-5 p-r-5 mc mac">
<font class="plane-mode-text">作业总数{{ totalWorkingDistance.toFixed(1) }} </font>
</div>
<div class="tag flex mac mc iconfont icon-pin-distance-line"></div>
</div>
</div>
</template> </template>
<script> <script>
import geodist from 'geodist'
export default { export default {
name: 'Statistics', name: 'Statistics',
@ -20,7 +47,7 @@ export default {
} }
}, },
props: { props: {
plane: { planes: {
type: Object, type: Object,
deep: true deep: true
} }
@ -28,6 +55,52 @@ export default {
components: { components: {
}, },
computed: { computed: {
totalCount () {
return Object.keys(this.planes || {}).length
},
onlineCount () {
return Object.values(this.planes || {}).filter(p => p.planeState?.online).length
},
totalWorkingDuration () {
// - startTime
const now = Math.floor(Date.now() / 1000)
return Object.values(this.planes || {}).reduce((total, p) => {
const s = p.planeState?.flyDataSave?.startTime
const isUnlock = p.planeState?.isUnlock
if (isUnlock && s) {
return total + (now - s)
}
return total
}, 0)
},
totalWorkingDistance () {
let totalDistance = 0
Object.values(this.planes || {}).forEach(p => {
const path = p.planeState?.flyDataSave?.path || []
for (let i = 1; i < path.length; i++) {
const prev = path[i - 1]
const curr = path[i]
if (prev && curr) {
totalDistance += geodist(
{ lat: prev[1], lon: prev[0] },
{ lat: curr[1], lon: curr[0] },
{ exact: true, unit: 'meters' }
)
}
}
})
return totalDistance
},
unlockedCount () {
return Object.values(this.planes || {}).filter(p => p.planeState?.isUnlock).length
},
formattedDuration () {
const sec = this.totalWorkingDuration
const h = Math.floor(sec / 3600).toString().padStart(2, '0')
const m = Math.floor((sec % 3600) / 60).toString().padStart(2, '0')
const s = (sec % 60).toString().padStart(2, '0')
return `${h}:${m}:${s}`
}
}, },
watch: { watch: {
@ -83,4 +156,11 @@ export default {
display: inline-block; display: inline-block;
white-space: nowrap; /* 防止内容换行 */ white-space: nowrap; /* 防止内容换行 */
} }
.item {
padding: 0 6px;
height: 29px;
line-height: 29px;
font-size: 13px;
}
</style> </style>

View File

@ -65,7 +65,7 @@ const routes = [
redirect: '/model/index', redirect: '/model/index',
meta: { meta: {
title: '机型管理', title: '机型管理',
icon: 'el-icon-edit-outline', icon: 'iconfont icon-chuiqigudingyi',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
}, },

View File

@ -348,7 +348,7 @@ const store = new Vuex.Store({
const res = await api.get('getAirList') const res = await api.get('getAirList')
res.data.airList.forEach(plane => { res.data.airList.forEach(plane => {
plane.planeState = { // 飞机状态初始化字段 plane.planeState = { // 飞机状态初始化字段
flyDataMark: false, // 飞机解锁标记成真 isUnlock: false, // 飞机解锁标记成真
flyDataSave: { // 飞机加锁截至 待上传飞行数据之后 再清空此值 flyDataSave: { // 飞机加锁截至 待上传飞行数据之后 再清空此值
startTime: null, // 解锁时的时间戳(秒) startTime: null, // 解锁时的时间戳(秒)
endTime: null, // 加锁时的时间戳(秒) endTime: null, // 加锁时的时间戳(秒)
@ -358,6 +358,8 @@ const store = new Vuex.Store({
}, },
heartBeat: null, // 心跳 heartBeat: null, // 心跳
heartRandom: null, // 每次接收到心跳创建一个随机数 用于watch监听 heartRandom: null, // 每次接收到心跳创建一个随机数 用于watch监听
online: false, // 是否在线
onlineTimeout: null, // 存放定时器引用
voltagBattery: null, // 电压信息 voltagBattery: null, // 电压信息
currentBattery: null, // 电流信息 currentBattery: null, // 电流信息
batteryRemaining: null, // 电池电量 batteryRemaining: null, // 电池电量

View File

@ -1 +1 @@
@import 'https://at.alicdn.com/t/c/font_3703467_cqwk36imkj.css'; //iconfont阿里巴巴 @import 'https://at.alicdn.com/t/c/font_3703467_17bw22w7wt3g.css'; //iconfont阿里巴巴

View File

@ -3,7 +3,7 @@
<map-box ref="mapbox" @map-ready="onMapReady"> <map-box ref="mapbox" @map-ready="onMapReady">
<template #content> <template #content>
<div v-show="mapReady"> <div v-show="mapReady">
<!-- <Statistics :plane="plane" /> --> <Statistics :planes="airList" />
</div> </div>
</template> </template>
</map-box> </map-box>
@ -13,7 +13,7 @@
<script> <script>
import MapBox from '@/components/MapBox' import MapBox from '@/components/MapBox'
import { waitForMapCanvasReady } from '@/utils' import { waitForMapCanvasReady } from '@/utils'
// import Statistics from '@/components/Statistics' import Statistics from '@/components/Statistics'
export default { export default {
name: 'Home', name: 'Home',
@ -23,8 +23,8 @@ export default {
} }
}, },
components: { components: {
MapBox MapBox,
// Statistics Statistics
}, },
computed: { computed: {
airList () { airList () {

View File

@ -6,7 +6,7 @@
<template #content> <template #content>
<div v-show="mapReady"> <div v-show="mapReady">
<!-- <SwarmStatus :planes="planeList" /> --> <!-- <SwarmStatus :planes="planeList" /> -->
<SwarmControllerTabs :planes="planeList" @mapXOffset="mapXOffset" @makeRoute="makeRoute" @clearRoute="clearRoute" /> <SwarmControllerTabs :planes="planeList"/>
</div> </div>
</template> </template>
</map-box> </map-box>

View File

@ -151,13 +151,26 @@ export default {
plane.planeState.heartRandom = Math.random() plane.planeState.heartRandom = Math.random()
plane.planeState.heartBeat = jsonData.heartBeat plane.planeState.heartBeat = jsonData.heartBeat
const oldMark = plane.planeState.flyDataMark /* 判断飞机是否在线 */
// 线
plane.planeState.online = true
// 线
if (plane.planeState.onlineTimeout) {
clearTimeout(plane.planeState.onlineTimeout)
}
// 线10线
plane.planeState.onlineTimeout = setTimeout(() => {
plane.planeState.online = false
}, 10000)
/* 飞行数据上传 */
const oldMark = plane.planeState.isUnlock
const newMark = (Number(jsonData.heartBeat) & 128) !== 0 const newMark = (Number(jsonData.heartBeat) & 128) !== 0
// //
if (!oldMark && newMark) { if (!oldMark && newMark) {
// //
plane.planeState.flyDataMark = true plane.planeState.isUnlock = true
plane.planeState.flyDataSave = { plane.planeState.flyDataSave = {
startTime: Math.floor(Date.now() / 1000), // startTime: Math.floor(Date.now() / 1000), //
startBattery: plane.planeState.batteryRemaining, startBattery: plane.planeState.batteryRemaining,
@ -166,7 +179,7 @@ export default {
path: [] path: []
} }
} else if (oldMark && !newMark) { } else if (oldMark && !newMark) {
plane.planeState.flyDataMark = false plane.planeState.isUnlock = false
this.handleFlightDataUpload(plane) // this.handleFlightDataUpload(plane) //
@ -192,7 +205,7 @@ export default {
plane.planeState.position.shift() // plane.planeState.position.shift() //
} }
// //
if (plane.planeState.flyDataMark && plane.planeState.flyDataSave?.path) { if (plane.planeState.isUnlock && plane.planeState.flyDataSave?.path) {
plane.planeState.flyDataSave.path.push([ plane.planeState.flyDataSave.path.push([
position.lon / 10e6, position.lon / 10e6,
position.lat / 10e6, position.lat / 10e6,