Compare commits
8 Commits
bc6bf29da2
...
fd16bc4f63
| Author | SHA1 | Date | |
|---|---|---|---|
| fd16bc4f63 | |||
| 78c2d7de21 | |||
| e14aa9a975 | |||
| e7506728af | |||
| 9f48971c72 | |||
| 5f711afb1d | |||
| 3b4940ac4a | |||
| 6126b9d137 |
@ -207,12 +207,12 @@
|
||||
</div>
|
||||
<div class="butIconBox gap10 flex">
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="publishFun('{unlock:1}'); speakText('解锁飞机')">
|
||||
@click="confirmation('确认对飞机解锁?请确保飞行环境安全。', '解锁操作', '{unlock:1}'); speakText('解锁飞机')">
|
||||
<i class="iconfont icon-jiesuo f-s-24"></i>
|
||||
<div class="m-t-5">解锁</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="confirmation('飞机加锁,螺旋桨将停转,请谨慎操作!', '加锁操作', '{lock:1}'); speakText('加锁,请注意安全')">
|
||||
@click="confirmation('飞机加锁,螺旋桨将停转,请谨慎操作!', '加锁操作', '{lock:1}'); speakText('加锁飞机,请注意安全')">
|
||||
<i class=" iconfont icon-suoding f-s-24"></i>
|
||||
<div class="m-t-5">加锁</div>
|
||||
</el-button>
|
||||
|
||||
@ -123,8 +123,17 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onChange (val) {
|
||||
this.$emit('input', val)
|
||||
this.$emit('change', val)
|
||||
if (Array.isArray(val) && val.length === 2) {
|
||||
const start = val[0]
|
||||
const end = new Date(val[1])
|
||||
end.setHours(23, 59, 59, 999) // 把结束时间改为当天最后一毫秒
|
||||
this.internalValue = [start, end]
|
||||
this.$emit('input', [start, end])
|
||||
this.$emit('change', [start, end])
|
||||
} else {
|
||||
this.$emit('input', val)
|
||||
this.$emit('change', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 unlineIcon from '@/assets/svg/plane_unline.svg'
|
||||
import civilIcon from '@/assets/svg/civil.svg'
|
||||
|
||||
export default {
|
||||
@ -134,7 +135,7 @@ export default {
|
||||
|
||||
// 判断地图开启了点飞功能
|
||||
if (this.enableGuided) {
|
||||
// 长按事件 传longPress到组件外部调用
|
||||
// 长按事件 传longPress到组件外部调用
|
||||
let pressTimer = null
|
||||
let isLongPress = false // 标记是否为长按
|
||||
let startPoint = null // 记录按下时的位置
|
||||
@ -605,7 +606,146 @@ export default {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description: 创建飞机轨迹 ps:原理删除之前的轨迹 重新绘制
|
||||
* @description 清除历史轨迹所有轨迹线和轨迹点的图层与数据源
|
||||
* @param {Array<string>} lineLayerIds - 轨迹线图层ID数组
|
||||
* @param {Array<string>} pointLayerIds - 轨迹点图层ID数组
|
||||
*/
|
||||
clearHistoryPaths (lineLayerIds = [], pointLayerIds = []) {
|
||||
if (!this.map) return
|
||||
|
||||
// 清除轨迹线图层及对应数据源
|
||||
lineLayerIds.forEach(id => {
|
||||
if (this.map.getLayer(id)) {
|
||||
this.map.removeLayer(id)
|
||||
}
|
||||
if (this.map.getSource(id)) {
|
||||
this.map.removeSource(id)
|
||||
}
|
||||
})
|
||||
|
||||
// 清除轨迹点图层及对应数据源
|
||||
pointLayerIds.forEach(id => {
|
||||
if (this.map.getLayer(id)) {
|
||||
this.map.removeLayer(id)
|
||||
}
|
||||
if (this.map.getSource(id)) {
|
||||
this.map.removeSource(id)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 根据索引绘制一条历史轨迹的轨迹线和起点圆点
|
||||
* @param {Array<Array<number>>} pathArray - GPS坐标数组,格式为 [[lon, lat, ...], ...]
|
||||
* @param {number} index - 用于生成图层ID的索引,确保每条轨迹ID唯一
|
||||
*/
|
||||
drawHistoryPathByIndex (pathArray, index) {
|
||||
if (!this.map) return
|
||||
if (!pathArray || pathArray.length === 0) return
|
||||
|
||||
const zoomThreshold = 12 // 缩放等级阈值,决定显示轨迹线还是起点圆点
|
||||
const pointRadius = 6 // 起点圆点半径
|
||||
|
||||
// 轨迹线GeoJSON对象
|
||||
const lineGeojson = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: pathArray
|
||||
}
|
||||
}
|
||||
|
||||
// 起点圆点GeoJSON对象,取轨迹的第一个坐标点
|
||||
const pointGeojson = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: pathArray[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 图层ID,保证唯一
|
||||
const lineLayerId = `path${index}`
|
||||
const pointLayerId = `path${index}-point`
|
||||
|
||||
// 绘制轨迹线
|
||||
const drawLine = () => {
|
||||
if (this.map.getSource(lineLayerId)) return
|
||||
this.map.addSource(lineLayerId, {
|
||||
type: 'geojson',
|
||||
data: lineGeojson
|
||||
})
|
||||
this.map.addLayer({
|
||||
id: lineLayerId,
|
||||
type: 'line',
|
||||
source: lineLayerId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': '#3388ff',
|
||||
'line-width': 3
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制起点圆点
|
||||
const drawPoint = () => {
|
||||
if (this.map.getSource(pointLayerId)) return
|
||||
this.map.addSource(pointLayerId, {
|
||||
type: 'geojson',
|
||||
data: pointGeojson
|
||||
})
|
||||
this.map.addLayer({
|
||||
id: pointLayerId,
|
||||
type: 'circle',
|
||||
source: pointLayerId,
|
||||
paint: {
|
||||
'circle-radius': pointRadius,
|
||||
'circle-color': '#3388ff',
|
||||
'circle-stroke-color': '#fff',
|
||||
'circle-stroke-width': 2
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 根据缩放等级切换显示轨迹线还是起点圆点
|
||||
const updateDisplay = () => {
|
||||
const zoom = this.map.getZoom()
|
||||
|
||||
// 起点圆点一直显示,先确保点图层存在
|
||||
if (!this.map.getSource(pointLayerId)) {
|
||||
drawPoint()
|
||||
}
|
||||
|
||||
// 轨迹线根据缩放决定显示或隐藏
|
||||
if (zoom >= zoomThreshold) {
|
||||
// 放大,显示轨迹线
|
||||
if (!this.map.getSource(lineLayerId)) {
|
||||
drawLine()
|
||||
}
|
||||
} else {
|
||||
// 缩小时隐藏轨迹线
|
||||
if (this.map.getLayer(lineLayerId)) {
|
||||
this.map.removeLayer(lineLayerId)
|
||||
}
|
||||
if (this.map.getSource(lineLayerId)) {
|
||||
this.map.removeSource(lineLayerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 首次渲染轨迹显示
|
||||
updateDisplay()
|
||||
|
||||
// 监听缩放事件,动态切换显示
|
||||
this.map.on('zoom', () => {
|
||||
updateDisplay()
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @description: 创建飞机轨迹 ps:原理删除之前的轨迹 重新绘制 用于实时轨迹
|
||||
* @param {arr} coordinatesArray 飞机经纬高度数组
|
||||
*/
|
||||
createPathWithArray (coordinatesArray) {
|
||||
@ -842,6 +982,7 @@ export default {
|
||||
/**
|
||||
* @description: 镜头跳转
|
||||
* @param {obj} lonLat {lon:lon,lat:lat} 经纬度
|
||||
* @param {Number} zoom 地图放大率
|
||||
*/
|
||||
goto (lonLat, zoom = 18) {
|
||||
this.map.flyTo({
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<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'">
|
||||
<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>
|
||||
<!-- 锁定状态 -->
|
||||
<!-- 解锁状态 -->
|
||||
<div class="flex">
|
||||
<div class="tag flex mac mc iconfont" :class="isLockState ? 'icon-suoding' : 'icon-jiesuo'">
|
||||
</div>
|
||||
<div class="tag flex mac mc iconfont" :class="isUnlock ? 'icon-jiesuo' : 'icon-suoding'"></div>
|
||||
</div>
|
||||
<!-- 飞机模式 -->
|
||||
<div class="flex">
|
||||
@ -22,7 +23,7 @@
|
||||
<!-- 卫星 -->
|
||||
<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>
|
||||
<font class="plane-mode-text">{{ fixType }} {{ satCount }}颗</font>
|
||||
</div>
|
||||
<div class="tag flex mac mc iconfont icon-weixing">
|
||||
</div>
|
||||
@ -62,7 +63,9 @@
|
||||
<!-- 飞机载重 钩子状态 -->
|
||||
<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>
|
||||
<font class="plane-mode-text" v-if="hookstatus || loadweight">
|
||||
{{ hookstatus || '' }} {{ loadweight ? loadweight + '克' : '' }}
|
||||
</font>
|
||||
</div>
|
||||
<div class="tag flex mac mc iconfont icon-mianxingdiaogou">
|
||||
</div>
|
||||
@ -78,9 +81,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
/* 心跳 */
|
||||
heartAnimation: false, // 控制心跳动画图标
|
||||
online: false,
|
||||
isOnlineSetTimeout: null
|
||||
heartAnimation: false // 控制心跳动画图标
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@ -92,6 +93,10 @@ export default {
|
||||
components: {
|
||||
},
|
||||
computed: {
|
||||
// 飞机在线状态
|
||||
online () {
|
||||
return this.plane?.online ?? false
|
||||
},
|
||||
// 心跳随机数
|
||||
heartRandom () {
|
||||
if (this.plane && this.plane.planeState) {
|
||||
@ -99,14 +104,9 @@ export default {
|
||||
}
|
||||
return null
|
||||
},
|
||||
// 锁定状态
|
||||
isLockState () {
|
||||
if (this.plane && this.plane.planeState) {
|
||||
if (Number(this.plane.planeState.heartBeat) & 128) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
// 解锁状态
|
||||
isUnlock () {
|
||||
return this.plane?.planeState?.isUnlock ?? false
|
||||
},
|
||||
// 飞机模式
|
||||
getPlaneMode () {
|
||||
@ -180,20 +180,11 @@ export default {
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -203,9 +194,6 @@ export default {
|
||||
created () {
|
||||
},
|
||||
destroyed () {
|
||||
if (this.isOnlineSetTimeout) {
|
||||
clearInterval(this.isOnlineSetTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -249,6 +237,11 @@ export default {
|
||||
|
||||
.plane-mode-text {
|
||||
display: inline-block;
|
||||
white-space: nowrap; /* 防止内容换行 */
|
||||
white-space: nowrap;
|
||||
/* 防止内容换行 */
|
||||
}
|
||||
|
||||
.useful-heart {
|
||||
color: $success-color;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,17 +1,44 @@
|
||||
<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 class="flex stat-row">
|
||||
<div class="plane-mode p-l-5 p-r-5 mc mac">
|
||||
<font class="plane-mode-text">总架数:{{ totalCount }}</font>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="tag flex mac mc iconfont icon-zongshu"></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>
|
||||
|
||||
<script>
|
||||
import geodist from 'geodist'
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
@ -20,7 +47,7 @@ export default {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
plane: {
|
||||
planes: {
|
||||
type: Object,
|
||||
deep: true
|
||||
}
|
||||
@ -28,6 +55,52 @@ export default {
|
||||
components: {
|
||||
},
|
||||
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: {
|
||||
|
||||
@ -83,4 +156,11 @@ export default {
|
||||
display: inline-block;
|
||||
white-space: nowrap; /* 防止内容换行 */
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 0 6px;
|
||||
height: 29px;
|
||||
line-height: 29px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
291
src/components/SwarmControllerTabs.vue
Normal file
291
src/components/SwarmControllerTabs.vue
Normal file
@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="w-100 h-100 mainBox">
|
||||
<!-- 弹出框 -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="320px" top="30vh" @close="closeCallback"
|
||||
@open="openCallback">
|
||||
<!-- 起飞设置弹出框 -->
|
||||
<template v-if="dialogItem === 'takeoffBox'">
|
||||
<div class="flex mse mac">
|
||||
<el-slider class="w-100" v-model="takeoffValue" :show-tooltip="false" show-input :min="1" :max="100" />
|
||||
<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"
|
||||
@click="publishFun(`{guidedMode:{alt:${takeoffValue}}}`); speakText('确认起飞')">
|
||||
飞至
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- tab 控件组 -->
|
||||
<div class="flex column mr mac tabContainer p-l-10 p-r-10">
|
||||
<!-- 内容面板 -->
|
||||
<div class="tabContent" :class="{ active: activeIndex !== null }">
|
||||
<!-- 控制 -->
|
||||
<div v-if="activeIndex === 0" class="tabContentBox">
|
||||
<div class="clearB m-b-15 fb f-s-16 contentTit">
|
||||
<i class="iconfont icon-youxishoubing f-s-22 m-r-5"></i>
|
||||
<span>飞机控制</span>
|
||||
</div>
|
||||
<div class="butIconBox gap10 flex">
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="confirmation('确认对全部飞机解锁?请确保飞行环境安全。', '解锁操作', '{unlock:1}'); speakText('解锁全部飞机')">
|
||||
<i class="iconfont icon-jiesuo f-s-24"></i>
|
||||
<div class="m-t-5">解锁</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="confirmation('飞机全部加锁,螺旋桨将停转,请谨慎操作!', '加锁操作', '{lock:1}'); speakText('加锁全部飞机,请注意安全')">
|
||||
<i class="iconfont icon-suoding f-s-24"></i>
|
||||
<div class="m-t-5">加锁</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon" @click="showTakeoffDialog">
|
||||
<i class="iconfont icon-yangshi_icon_tongyong_departure f-s-24"></i>
|
||||
<div class="m-t-5">起飞</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="publishFun('{loiterMode:1}'); speakText('悬停')">
|
||||
<i class="iconfont icon-fengzheng1 f-s-24"></i>
|
||||
<div class="m-t-5">悬停</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="publishFun('{rtlMode:1}'); speakText('返航')">
|
||||
<i class="iconfont icon-yijianfanhang f-s-24"></i>
|
||||
<div class="m-t-5">返航</div>
|
||||
</el-button>
|
||||
<el-button size="medium" type="primary" class="flex1 butIcon"
|
||||
@click="publishFun('{landMode:1}'); speakText('降落')">
|
||||
<i class="iconfont icon-yangshi_icon_tongyong_arriving f-s-24"></i>
|
||||
<div class="m-t-5">降落</div>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 重选 -->
|
||||
<div v-else-if="activeIndex === 1" class="tabContentBox">
|
||||
<div class="clearB m-b-15 fb f-s-16 contentTit">
|
||||
<i class="iconfont icon-zhongxuanwenjian f-s-22 m-r-5"></i>
|
||||
<span>重新选择飞机</span>
|
||||
</div>
|
||||
<el-button type="danger" size="medium" class="m-t-10" @click="confirmReselect">
|
||||
确认重选
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tab 按钮组 -->
|
||||
<div class="flex gap10 m-b-10 taButGroup">
|
||||
<div class="flex1 h-100 taBut flex column mac mc animation" :class="activeIndex === index ? 'taButBG' : ''"
|
||||
v-for="(item, index) in controlItems" :key="index" @click="toggleContent(index, item.voice)">
|
||||
<i :class="item.icon" class="iconfont f-s-35 no-select"></i>
|
||||
<div class="m-t-15 fb f-s-18 no-select">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mqtt from '@/utils/mqtt'
|
||||
import { speakText } from '@/utils/index'
|
||||
|
||||
export default {
|
||||
name: 'TabController',
|
||||
props: {
|
||||
planes: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dialogTitle: '',
|
||||
dialogItem: '',
|
||||
dialogVisible: false,
|
||||
takeoffValue: 2,
|
||||
controlItems: [
|
||||
{ title: '控制', icon: 'icon-youxishoubing', voice: '控制飞机' },
|
||||
{ title: '重选', icon: 'icon-zhongxuanwenjian', voice: '重新选择飞机' }
|
||||
],
|
||||
activeIndex: null,
|
||||
tabIsOpen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobile () {
|
||||
return this.$store.state.app.isMobile
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
speakText,
|
||||
/** 关闭弹出框后的处理 */
|
||||
closeCallback () {
|
||||
if (this.dialogItem === 'compassBox') this.handlerCloseCompassBox()
|
||||
else if (this.dialogItem === 'acceBox') this.handlerCloseAcceBox()
|
||||
},
|
||||
/** 打开弹出框时处理 */
|
||||
openCallback () { },
|
||||
|
||||
/** 切换 tab 内容 */
|
||||
toggleContent (index, voice) {
|
||||
const wasActive = this.activeIndex === index
|
||||
this.activeIndex = wasActive ? null : index
|
||||
|
||||
if (this.tabIsOpen) {
|
||||
if (!wasActive) {
|
||||
this.tabIsOpen = false
|
||||
this.$emit('mapXOffset', 0, -200)
|
||||
} else {
|
||||
this.speakText(voice)
|
||||
}
|
||||
} else {
|
||||
this.tabIsOpen = true
|
||||
this.$emit('mapXOffset', 0, 200)
|
||||
this.speakText(voice)
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开发送起飞指令的对话框 */
|
||||
showTakeoffDialog () {
|
||||
this.dialogVisible = true
|
||||
this.dialogTitle = '高度设置'
|
||||
this.dialogItem = 'takeoffBox'
|
||||
this.speakText('设置起飞高度')
|
||||
},
|
||||
|
||||
/** 群发 MQTT 指令 */
|
||||
publishFun (jsonData) {
|
||||
if (!this.planes || this.planes.length === 0) {
|
||||
return this.$message.warning('没有连接的飞机')
|
||||
}
|
||||
this.planes.forEach(plane => {
|
||||
if (plane.macadd) {
|
||||
mqtt.publishFun(`cmd/${plane.macadd}`, jsonData)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 重选飞机 确定
|
||||
confirmReselect () {
|
||||
this.$confirm('将清空当前选中的所有飞机,是否确认重选?', '确认重选', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.state.app.swarmIdArr = []
|
||||
this.$router.replace('/register/index')
|
||||
}).catch(() => {
|
||||
this.$message.info('取消重选操作')
|
||||
})
|
||||
},
|
||||
|
||||
/** 弹窗确认发送指令 */
|
||||
confirmation (msg, title, instruct) {
|
||||
this.$confirm(msg, title, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
this.publishFun(instruct)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info('取消操作!')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/theme.scss";
|
||||
|
||||
.danger-color {
|
||||
color: $danger-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mainBox {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
z-index: 90;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 470px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
top: -50px;
|
||||
opacity: 0;
|
||||
transition: top 0.5s ease, opacity 1s ease;
|
||||
}
|
||||
|
||||
.tabContent.active {
|
||||
top: -10px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tabContentBox {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.contentTit i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.butIconBox {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: space-around;
|
||||
}
|
||||
|
||||
.butIcon {
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.taButGroup {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 470px;
|
||||
height: 105px;
|
||||
cursor: pointer;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.taButGroup {
|
||||
height: calc((100vw - 50px) / 4);
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.taBut {
|
||||
color: $maintext-color;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.taButBG {
|
||||
color: $graylight-color;
|
||||
background-color: $brand-color;
|
||||
}
|
||||
|
||||
.gap10 {
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
254
src/components/SwarmStatus.vue
Normal file
254
src/components/SwarmStatus.vue
Normal file
@ -0,0 +1,254 @@
|
||||
<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>
|
||||
@ -65,7 +65,7 @@ const routes = [
|
||||
redirect: '/model/index',
|
||||
meta: {
|
||||
title: '机型管理',
|
||||
icon: 'el-icon-edit-outline',
|
||||
icon: 'iconfont icon-chuiqigudingyi',
|
||||
roles: ['admin', 'editor'],
|
||||
tapName: 'plane'
|
||||
},
|
||||
@ -300,6 +300,16 @@ const routes = [
|
||||
tapName: 'plane'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/planes/swarm',
|
||||
component: () => import('@/views/layout/components/main/planes/swarm'),
|
||||
meta: {
|
||||
title: '集群控制',
|
||||
icon: 'iconfont icon-a-jiqunkongzhianniu_huaban1',
|
||||
roles: ['admin', 'editor'],
|
||||
tapName: 'plane'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/planes/index/:id/:name', // 动态加载路由时加ID参数
|
||||
component: () => import('@/views/layout/components/main/planes/index'),
|
||||
|
||||
@ -348,7 +348,7 @@ const store = new Vuex.Store({
|
||||
const res = await api.get('getAirList')
|
||||
res.data.airList.forEach(plane => {
|
||||
plane.planeState = { // 飞机状态初始化字段
|
||||
flyDataMark: false, // 飞机解锁标记成真
|
||||
isUnlock: false, // 飞机解锁标记成真
|
||||
flyDataSave: { // 飞机加锁截至 待上传飞行数据之后 再清空此值
|
||||
startTime: null, // 解锁时的时间戳(秒)
|
||||
endTime: null, // 加锁时的时间戳(秒)
|
||||
@ -358,6 +358,8 @@ const store = new Vuex.Store({
|
||||
},
|
||||
heartBeat: null, // 心跳
|
||||
heartRandom: null, // 每次接收到心跳创建一个随机数 用于watch监听
|
||||
online: false, // 是否在线
|
||||
onlineTimeout: null, // 存放定时器引用
|
||||
voltagBattery: null, // 电压信息
|
||||
currentBattery: null, // 电流信息
|
||||
batteryRemaining: null, // 电池电量
|
||||
|
||||
@ -1,3 +1,24 @@
|
||||
// 默认模块显示配置
|
||||
const defaultModuleVisibilityMap = {
|
||||
home: true,
|
||||
model: true,
|
||||
register: true,
|
||||
product: true,
|
||||
route: true,
|
||||
planes: true,
|
||||
site: true,
|
||||
shop: true,
|
||||
admin: true,
|
||||
category: true,
|
||||
broadcast: true,
|
||||
order: true,
|
||||
nofly: true
|
||||
}
|
||||
|
||||
// 从 localStorage 读取已有配置,合并覆盖默认配置
|
||||
const savedVisibility = JSON.parse(localStorage.getItem('moduleVisibilityMap') || '{}')
|
||||
const moduleVisibilityMap = { ...defaultModuleVisibilityMap, ...savedVisibility }
|
||||
|
||||
const state = {
|
||||
mqttState: false, // mqtt连接状态
|
||||
isCollapse: localStorage.getItem('isCollapse') ? !!+localStorage.getItem('isCollapse') : true, // 侧边导航栏 显隐
|
||||
@ -6,13 +27,25 @@ const state = {
|
||||
defaultLonLat: null, // 地图默认经纬度
|
||||
defaultZoom: null, // 地图默认缩放
|
||||
|
||||
moduleVisibilityMap, // 使用合并后的 moduleVisibilityMap 初始化
|
||||
|
||||
/* 页面参数 */
|
||||
orderSerch: null, // 订单列表页搜索条件
|
||||
toMessageIdArr: [], // 用户管理 发布公告页面 id临时传参
|
||||
toFlyDataIdArr: [] // 飞机飞行数据 临时传参
|
||||
toFlyDataIdArr: [], // 飞机飞行数据 临时传参
|
||||
swarmIdArr: []// 选中的 集群控制飞机ID组
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
// 设置 模块显示隐藏参数
|
||||
setModuleVisibility (state, { key, visible }) {
|
||||
state.moduleVisibilityMap[key] = visible
|
||||
localStorage.setItem('moduleVisibilityMap', JSON.stringify(state.moduleVisibilityMap))
|
||||
},
|
||||
setAllModuleVisibility (state, visibilityMap) {
|
||||
state.moduleVisibilityMap = visibilityMap
|
||||
localStorage.setItem('moduleVisibilityMap', JSON.stringify(state.moduleVisibilityMap))
|
||||
},
|
||||
// 导航栏 显隐
|
||||
setCollapse () {
|
||||
state.isCollapse = !state.isCollapse
|
||||
@ -64,7 +97,12 @@ const mutations = {
|
||||
// 飞机飞行数据 传递id数组
|
||||
setToFlyDataIdArr (state, idArr) {
|
||||
state.toFlyDataIdArr = idArr
|
||||
},
|
||||
// 设置 '选取的集群飞机'id组
|
||||
setSwarmIdArr (state, idArr) {
|
||||
state.swarmIdArr = idArr
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
@ -1 +1 @@
|
||||
@import 'https://at.alicdn.com/t/c/font_3703467_793cqnnxv0f.css'; //iconfont阿里巴巴
|
||||
@import 'https://at.alicdn.com/t/c/font_3703467_17bw22w7wt3g.css'; //iconfont阿里巴巴
|
||||
@ -11,7 +11,8 @@
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="border p-20 m-b-20">
|
||||
<el-form ref="form" :model="form" label-width="120px" :label-position="$store.state.app.isWideScreen?'top':'right'">
|
||||
<el-form ref="form" :model="form" label-width="120px"
|
||||
:label-position="$store.state.app.isWideScreen ? 'top' : 'right'">
|
||||
<el-form-item v-if="pageState === 'add'" label="所属商铺">
|
||||
<SelectionShopId v-model="form.shop_id" />
|
||||
</el-form-item>
|
||||
@ -33,6 +34,12 @@
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限设置">
|
||||
<el-radio-group v-model="form.role">
|
||||
<el-radio label="admin">管理员</el-radio>
|
||||
<el-radio label="editor">编辑</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="adminId == undefined ? '初始密码' : '新密码'">
|
||||
<el-input show-password v-model="form.pwd" :placeholder="adminId == undefined ? '密码' : '空置则为保持原密码'" />
|
||||
</el-form-item>
|
||||
@ -69,7 +76,8 @@ export default {
|
||||
uname: '',
|
||||
upFile: '',
|
||||
oldFile: '',
|
||||
pwd: ''
|
||||
pwd: '',
|
||||
role: 'admin'
|
||||
},
|
||||
adminId: this.$route.params.id, // get参数 获取管理员id 没有为添加页面
|
||||
pageState: 'add', // 页面状态
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<map-box ref="mapbox" @map-ready="onMapReady">
|
||||
<template #content>
|
||||
<div v-show="mapReady">
|
||||
<!-- <Statistics :plane="plane" /> -->
|
||||
<Statistics :planes="airList" />
|
||||
</div>
|
||||
</template>
|
||||
</map-box>
|
||||
@ -13,7 +13,7 @@
|
||||
<script>
|
||||
import MapBox from '@/components/MapBox'
|
||||
import { waitForMapCanvasReady } from '@/utils'
|
||||
// import Statistics from '@/components/Statistics'
|
||||
import Statistics from '@/components/Statistics'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
@ -23,8 +23,8 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MapBox
|
||||
// Statistics
|
||||
MapBox,
|
||||
Statistics
|
||||
},
|
||||
computed: {
|
||||
airList () {
|
||||
@ -73,7 +73,7 @@ export default {
|
||||
async handler () {
|
||||
try {
|
||||
// 等待地图画布准备好
|
||||
await waitForMapCanvasReady(this.map)
|
||||
await waitForMapCanvasReady(this.$refs.mapbox.map)
|
||||
// 画布准备好后执行你自己的逻辑
|
||||
this.onMapReady()
|
||||
} catch (err) {
|
||||
|
||||
@ -1,19 +1,94 @@
|
||||
<template>
|
||||
<div class="h-100">
|
||||
<div class="app-container">
|
||||
<el-row class="m-t-0">
|
||||
<el-col :span="24">
|
||||
<el-container>
|
||||
<el-header height="42px" class="l-h-42 p-l-10 p-r-10 border border-b-n">
|
||||
<div class="l">
|
||||
<i class="iconfont icon-shezhi f-s-20"></i>
|
||||
<font class="m-l-10 f-s-18 fb">设置</font>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="border p-20">
|
||||
<el-form label-width="120px">
|
||||
|
||||
<!-- 语言设置 -->
|
||||
<el-form-item label="语言设置">
|
||||
<el-radio-group v-model="currentLang" @change="changeLang">
|
||||
<el-radio label="zh-CN">简体中文</el-radio>
|
||||
<!-- 你可以继续添加更多语言 -->
|
||||
<!-- <el-radio label="en">English</el-radio> -->
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 添加删除系统模块 -->
|
||||
<el-form-item label="增删系统模块">
|
||||
<el-checkbox-group v-model="selectedModules" @change="handleChange">
|
||||
<el-checkbox v-for="item in moduleOptions" :key="item.value" :label="item.value">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Set',
|
||||
data () {
|
||||
return {
|
||||
currentLang: this.$store.state.settings.language || 'zh-CN',
|
||||
moduleOptions: [
|
||||
{ value: 'home', 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: 'category', label: '分类管理' },
|
||||
{ value: 'product', label: '商品管理' }
|
||||
],
|
||||
selectedModules: []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
computed: {
|
||||
...mapState('app', {
|
||||
moduleVisibilityMap: state => state.moduleVisibilityMap
|
||||
})
|
||||
},
|
||||
created () {
|
||||
// 根据 Vuex 中的 moduleVisibilityMap 初始化 selectedModules(取所有显示为 true 的模块)
|
||||
this.selectedModules = Object.keys(this.moduleVisibilityMap)
|
||||
.filter(key => this.moduleVisibilityMap[key])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('app', ['setModuleVisibility']),
|
||||
handleChange () {
|
||||
// 先把所有模块都设为 false
|
||||
Object.keys(this.moduleVisibilityMap).forEach(key => {
|
||||
this.setModuleVisibility({ key, visible: false })
|
||||
})
|
||||
// 再把选中的模块设为 true
|
||||
this.selectedModules.forEach(key => {
|
||||
this.setModuleVisibility({ key, visible: true })
|
||||
})
|
||||
},
|
||||
changeLang (lang) {
|
||||
this.$store.commit('settings/setLanguage', lang)
|
||||
this.$message.success(`语言已切换为:${lang === 'zh-CN' ? '简体中文' : lang}`)
|
||||
// 如果你用的是 vue-i18n,这里可能还需要调用 i18n.global.locale = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@ -23,15 +23,21 @@
|
||||
<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-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>
|
||||
<el-button size="medium" type="primary"
|
||||
@click="publishFun(`{guidedMode:{lon:${guidedLonLat.lon},lat:${guidedLonLat.lat},alt:${guidedAlt}}`); isReserveGuidedMaker = true; dialogVisible = false">飞至</el-button>
|
||||
<el-button size="medium" type="primary" @click="() => {
|
||||
if (flyToSinglePlane(guidedLonLat.lon, guidedLonLat.lat, guidedAlt)) {
|
||||
isReserveGuidedMaker = true;
|
||||
dialogVisible = false;
|
||||
}
|
||||
}">
|
||||
飞至
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -117,8 +123,35 @@ export default {
|
||||
this.dialogVisible = true
|
||||
this.dialogItem = 'guidedBox'
|
||||
this.guidedLonLat = lonLat // 设置点击的经纬度
|
||||
const posLen = this.plane.planeState.position.length
|
||||
this.guidedAlt = this.plane.planeState.position[posLen - 1][2]// 取出 点击时飞机的高度
|
||||
|
||||
// 安全获取飞机当前高度
|
||||
let height = 0
|
||||
if (this.plane && this.plane.planeState && Array.isArray(this.plane.planeState.position)) {
|
||||
const posLen = this.plane.planeState.position.length
|
||||
if (posLen > 0 && Array.isArray(this.plane.planeState.position[posLen - 1])) {
|
||||
height = this.plane.planeState.position[posLen - 1][2] || 0
|
||||
}
|
||||
}
|
||||
this.guidedAlt = height
|
||||
},
|
||||
// 单架飞机点飞指令,参数:lon, lat, alt
|
||||
flyToSinglePlane (lon, lat, alt) {
|
||||
const lonNum = Number(lon)
|
||||
const latNum = Number(lat)
|
||||
const altNum = Number(alt)
|
||||
|
||||
if (
|
||||
isNaN(lonNum) || isNaN(latNum) || isNaN(altNum) ||
|
||||
lonNum < -180 || lonNum > 180 ||
|
||||
latNum < -90 || latNum > 90
|
||||
) {
|
||||
this.$message.warning('请输入有效的经纬度(经度-180~180,纬度-90~90)和高度')
|
||||
return false
|
||||
}
|
||||
|
||||
const cmd = `{guidedMode:{lon:${lonNum.toFixed(7)},lat:${latNum.toFixed(7)},alt:${altNum.toFixed(1)}}}`
|
||||
this.publishFun(cmd)
|
||||
return true
|
||||
},
|
||||
// 地图组件回调地图加载完成后 执行
|
||||
onMapReady () {
|
||||
@ -182,7 +215,7 @@ export default {
|
||||
async handler () {
|
||||
try {
|
||||
// 等待地图画布准备好
|
||||
await waitForMapCanvasReady(this.map)
|
||||
await waitForMapCanvasReady(this.$refs.mapbox.map)
|
||||
// 画布准备好后执行你自己的逻辑
|
||||
this.onMapReady()
|
||||
} catch (err) {
|
||||
|
||||
244
src/views/layout/components/main/planes/swarm.vue
Normal file
244
src/views/layout/components/main/planes/swarm.vue
Normal file
@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="h-100">
|
||||
<!-- 地图组件 -->
|
||||
<map-box ref="mapbox" :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>
|
||||
</map-box>
|
||||
<!-- 弹出框 -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="320px" top="30vh" @close="closeCallback"
|
||||
@open="openCallback">
|
||||
<!-- 点飞设置弹出框 -->
|
||||
<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>
|
||||
<el-button size="medium" type="primary" @click="onSwarmFlyTo">飞至</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MapBox from '@/components/MapBox'
|
||||
// import SwarmStatus from '@/components/SwarmStatus.vue'
|
||||
import SwarmControllerTabs from '@/components/SwarmControllerTabs.vue'
|
||||
import { waitForMapCanvasReady } from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'Swarm',
|
||||
data () {
|
||||
return {
|
||||
dialogTitle: '', // 弹出框 标题
|
||||
dialogItem: '', // 弹出框 项目类型判断
|
||||
dialogVisible: false, // 弹出框 显隐
|
||||
guidedLonLat: {}, // 点飞 的经纬度
|
||||
guidedAlt: '', // 点飞的高度
|
||||
isReserveGuidedMaker: false, // 关闭指点飞行操作窗口时 标记是否删除图标
|
||||
mapReady: false // 地图加载完成后 回调时 设置此值 让地图组件插槽内容 滞后显示
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MapBox,
|
||||
// SwarmStatus,
|
||||
SwarmControllerTabs
|
||||
},
|
||||
computed: {
|
||||
// 选中的集群控制飞机s
|
||||
planeList () {
|
||||
const allPlanes = this.$store.state.airList
|
||||
const idArr = this.$store.state.app.swarmIdArr
|
||||
return allPlanes.filter(plane => idArr.includes(plane.id))
|
||||
},
|
||||
/**
|
||||
* @description: 侧边栏显隐
|
||||
*/
|
||||
isCollapse () {
|
||||
return this.$store.state.app.isCollapse
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// 没有参数 跳转到飞机列表页
|
||||
if (this.$store.state.app.swarmIdArr.length < 2) {
|
||||
this.$router.replace('/register/index')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 弹出框 关闭事件回调 */
|
||||
closeCallback () {
|
||||
if (this.dialogItem === 'guidedBox' && this.isReserveGuidedMaker === false) { // 关闭点飞窗口时
|
||||
this.$refs.mapbox.delGuidedMarker()// 删除所有点飞的地图标记
|
||||
}
|
||||
this.dialogVisible = false
|
||||
this.dialogItem = ''
|
||||
},
|
||||
/** 弹出框 打开事件回调 */
|
||||
openCallback () {
|
||||
},
|
||||
// 地图长按事件 记录地图经纬度
|
||||
// 地图长按事件 记录地图经纬度
|
||||
handleLongPress (lonLat) {
|
||||
this.isReserveGuidedMaker = false
|
||||
this.dialogTitle = '集群指点'
|
||||
this.dialogVisible = true
|
||||
this.dialogItem = 'guidedBox'
|
||||
this.guidedLonLat = lonLat // 点击的新位置中心点
|
||||
|
||||
// 获取所有飞机的高度并求平均
|
||||
const validHeights = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
||||
return Array.isArray(last) ? last[2] || 0 : 0
|
||||
}).filter(h => typeof h === 'number')
|
||||
|
||||
const avgAlt = validHeights.length
|
||||
? (validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length).toFixed(1)
|
||||
: 0
|
||||
|
||||
this.guidedAlt = Number(avgAlt)
|
||||
},
|
||||
// 集群指点飞行 保持所有飞机拓扑关系飞到指定位置
|
||||
onSwarmFlyTo () {
|
||||
const targetLon = Number(this.guidedLonLat.lon)
|
||||
const targetLat = Number(this.guidedLonLat.lat)
|
||||
const targetAlt = Number(this.guidedAlt)
|
||||
|
||||
if (
|
||||
isNaN(targetLon) || isNaN(targetLat) || isNaN(targetAlt) ||
|
||||
targetLon < -180 || targetLon > 180 ||
|
||||
targetLat < -90 || targetLat > 90
|
||||
) {
|
||||
this.$message.warning('请输入有效的经纬度(经度-180~180,纬度-90~90)和高度')
|
||||
return
|
||||
}
|
||||
|
||||
// 所有飞机当前中心点
|
||||
const currentCenter = this.getSwarmCenter()
|
||||
|
||||
if (!currentCenter) {
|
||||
this.$message.error('无法获取飞行器中心位置,检查飞机是否都已定位')
|
||||
return
|
||||
}
|
||||
|
||||
// 计算每架飞机相对于当前中心的偏移量
|
||||
const commands = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
||||
if (!Array.isArray(last)) return null
|
||||
|
||||
const offsetLon = last[0] - currentCenter.lon
|
||||
const offsetLat = last[1] - currentCenter.lat
|
||||
const offsetAlt = last[2] - currentCenter.alt
|
||||
|
||||
const newLon = targetLon + offsetLon
|
||||
const newLat = targetLat + offsetLat
|
||||
const newAlt = targetAlt + offsetAlt
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
cmd: `{guidedMode:{lon:${newLon.toFixed(7)},lat:${newLat.toFixed(7)},alt:${newAlt.toFixed(1)}}}`
|
||||
}
|
||||
}).filter(Boolean)
|
||||
|
||||
// 发送控制指令
|
||||
commands.forEach(({ id, cmd }) => {
|
||||
this.publishFun(cmd, id)
|
||||
})
|
||||
|
||||
this.isReserveGuidedMaker = true
|
||||
this.dialogVisible = false
|
||||
},
|
||||
// 获取集群中心点
|
||||
getSwarmCenter () {
|
||||
const positions = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
||||
return Array.isArray(last) ? last : null
|
||||
}).filter(Boolean)
|
||||
|
||||
if (!positions.length) return null
|
||||
|
||||
const sum = positions.reduce((acc, pos) => {
|
||||
acc.lon += pos[0]
|
||||
acc.lat += pos[1]
|
||||
acc.alt += pos[2] || 0
|
||||
return acc
|
||||
}, { lon: 0, lat: 0, alt: 0 })
|
||||
|
||||
const count = positions.length
|
||||
return {
|
||||
lon: sum.lon / count,
|
||||
lat: sum.lat / count,
|
||||
alt: sum.alt / count
|
||||
}
|
||||
},
|
||||
// 地图组件回调地图加载完成后 执行
|
||||
onMapReady () {
|
||||
this.mapReady = true // 标记地图加载完成
|
||||
this.makePlanes(this.planeList)
|
||||
},
|
||||
/**
|
||||
* @description: 创建飞机图标
|
||||
*/
|
||||
makePlanes (planes) {
|
||||
this.$refs.mapbox.removePlanes()// 先清除画布上现有的飞机
|
||||
planes.forEach((plane, index) => { // 创建所有飞机
|
||||
let planeDefaultLonLat
|
||||
if (localStorage.getItem(plane.name) !== null) { // 从本地缓存 拿飞机得初始位置
|
||||
planeDefaultLonLat = JSON.parse(localStorage.getItem(plane.name))
|
||||
plane.lon = planeDefaultLonLat.lon
|
||||
plane.lat = planeDefaultLonLat.lat
|
||||
} else {
|
||||
// 缺省 给经纬度 小数点后随机第五位 即有个10米左右的随机距离
|
||||
plane.lon = 100 + Number((Math.random() * 0.01).toFixed(5))
|
||||
plane.lat = 40 + Number((Math.random() * 0.01).toFixed(5))
|
||||
}
|
||||
this.$refs.mapbox.makePlane(plane, index)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* @description: 飞机列表更新时候 更新地图
|
||||
*/
|
||||
planeList: {
|
||||
async handler () {
|
||||
try {
|
||||
// 等待地图画布准备好
|
||||
await waitForMapCanvasReady(this.$refs.mapbox.map)
|
||||
// 画布准备好后执行你自己的逻辑
|
||||
this.onMapReady()
|
||||
} catch (err) {
|
||||
console.debug('等待地图画布准备超时', err)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
/**
|
||||
* @description: 侧边栏显隐
|
||||
*/
|
||||
isCollapse () {
|
||||
return this.$store.state.app.isCollapse
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="top-bar">
|
||||
<DateRangePicker v-model="dateRange" class="m-r-20 m-b-20" />
|
||||
<el-radio-group v-model="radioClass">
|
||||
<el-radio-button label="作业架次"></el-radio-button>
|
||||
<el-radio-button label="飞行时长"></el-radio-button>
|
||||
<el-radio-button label="飞行距离"></el-radio-button>
|
||||
<el-radio-button label="消耗电量"></el-radio-button>
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
<div class="chart-area" v-if="flyDataList.length">
|
||||
<div v-if="boxShow" id="main" class="chart-container"></div>
|
||||
<map-box v-else ref="mapbox" @map-ready="onMapReady" :enableSaveToFile="true" :getData="() => flyDataList"/>
|
||||
<map-box v-else ref="mapbox" @map-ready="onMapReady" :enableSaveToFile="true" :getData="() => flyDataList" />
|
||||
</div>
|
||||
<div v-else class="no-data-tip">暂无数据</div>
|
||||
</div>
|
||||
@ -39,7 +40,7 @@ export default {
|
||||
flyDataList: [],
|
||||
selectedPlaneIdArr: this.$store.state.app.toFlyDataIdArr,
|
||||
dateRange: [start, end],
|
||||
radioClass: '飞行时长',
|
||||
radioClass: '作业架次',
|
||||
boxShow: true
|
||||
}
|
||||
},
|
||||
@ -88,6 +89,7 @@ export default {
|
||||
const groupedData = {}
|
||||
|
||||
const keyMap = {
|
||||
作业架次: () => 1,
|
||||
飞行时长: (item) => {
|
||||
if (!item.start_time || !item.end_time) return 0
|
||||
return Math.round((item.end_time - item.start_time) / 60)
|
||||
@ -199,41 +201,73 @@ export default {
|
||||
methods: {
|
||||
// 地图组件回调地图加载完成后 执行
|
||||
onMapReady () {
|
||||
this.drawAllPathsOnMap()
|
||||
this.drawAllHistoricalPaths()
|
||||
},
|
||||
// 地图轨迹绘制
|
||||
drawAllPathsOnMap () {
|
||||
// 地图组件 历史轨迹绘制
|
||||
drawAllHistoricalPaths () {
|
||||
if (!this.$refs.mapbox) return
|
||||
if (this.flyDataList.length === 0) return
|
||||
|
||||
// 清理旧轨迹
|
||||
this.$refs.mapbox.clearMapElements(['path'], ['path'])
|
||||
// 准备要清除的所有轨迹图层和点图层id
|
||||
const lineLayerIds = []
|
||||
const pointLayerIds = []
|
||||
|
||||
// 遍历所有飞行数据,绘制轨迹
|
||||
this.flyDataList.forEach(item => {
|
||||
if (!item.gps_path) return
|
||||
// 解析所有轨迹
|
||||
this.flyDataList.forEach((item, index) => {
|
||||
let pathArray = item.gps_path
|
||||
try {
|
||||
const pathArray = JSON.parse(item.gps_path)
|
||||
if (Array.isArray(pathArray) && pathArray.length > 0) {
|
||||
this.$refs.mapbox.createPathWithArray(pathArray)
|
||||
if (typeof pathArray === 'string') {
|
||||
pathArray = JSON.parse(pathArray)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('gps_path 解析失败', item.gps_path)
|
||||
console.warn(`第${index + 1}条轨迹 gps_path 解析失败`, e)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加图层id
|
||||
lineLayerIds.push(`path${index}`)
|
||||
pointLayerIds.push(`path${index}-point`)
|
||||
})
|
||||
|
||||
// 清除所有轨迹图层和数据源
|
||||
this.$refs.mapbox.clearHistoryPaths(lineLayerIds, pointLayerIds)
|
||||
|
||||
// 逐条绘制轨迹
|
||||
this.flyDataList.forEach((item, index) => {
|
||||
let pathArray = item.gps_path
|
||||
if (typeof pathArray === 'string') {
|
||||
try {
|
||||
pathArray = JSON.parse(pathArray)
|
||||
} catch (e) {
|
||||
console.warn(`第${index + 1}条轨迹 gps_path 解析失败`, e)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (Array.isArray(pathArray) && pathArray.length > 0) {
|
||||
this.$refs.mapbox.drawHistoryPathByIndex(pathArray, index)
|
||||
}
|
||||
})
|
||||
|
||||
// 跳转到第一个轨迹的起点(经纬度)
|
||||
if (this.flyDataList.length > 0 && this.flyDataList[0].gps_path) {
|
||||
try {
|
||||
const firstPath = JSON.parse(this.flyDataList[0].gps_path)
|
||||
if (Array.isArray(firstPath) && firstPath.length > 0) {
|
||||
const [lon, lat] = firstPath[0]
|
||||
this.$refs.mapbox.goto({ lon, lat })
|
||||
// 跳转到第一条轨迹的第一个点
|
||||
const firstPath = this.flyDataList[0]?.gps_path
|
||||
let firstPoint = null
|
||||
if (firstPath) {
|
||||
if (typeof firstPath === 'string') {
|
||||
try {
|
||||
firstPoint = JSON.parse(firstPath)[0]
|
||||
} catch {
|
||||
firstPoint = null
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败不跳转
|
||||
} else if (Array.isArray(firstPath)) {
|
||||
firstPoint = firstPath[0]
|
||||
}
|
||||
}
|
||||
if (firstPoint && Array.isArray(firstPoint) && firstPoint.length >= 2) {
|
||||
const [lon, lat] = firstPoint
|
||||
this.$refs.mapbox.goto({ lon, lat })
|
||||
}
|
||||
},
|
||||
|
||||
// 获取飞行数据
|
||||
async loadFlyData () {
|
||||
if (this.selectedPlaneIdArr.length === 0) {
|
||||
@ -280,7 +314,13 @@ export default {
|
||||
xAxis: { type: 'category' },
|
||||
yAxis: {
|
||||
gridIndex: 0,
|
||||
name: this.radioClass === '飞行时长' ? '分钟' : this.radioClass === '飞行距离' ? '米' : '毫安'
|
||||
name: this.radioClass === '飞行时长'
|
||||
? '分钟'
|
||||
: this.radioClass === '飞行距离'
|
||||
? '米'
|
||||
: this.radioClass === '消耗电量'
|
||||
? '毫安'
|
||||
: '架次'
|
||||
},
|
||||
grid: { top: '55%' },
|
||||
series: [
|
||||
|
||||
@ -2,19 +2,21 @@
|
||||
<div class="app-container">
|
||||
<!-- 组合按钮 -->
|
||||
<el-button-group class="m-r-20 m-b-20">
|
||||
<el-button type="primary" icon="el-icon-plus" @click="$router.replace('/register/add')">添加</el-button>
|
||||
<el-button type="danger" icon="el-icon-delete" @click="deleteAir(countSelIdArr($refs.myTable.selection))">删除
|
||||
</el-button>
|
||||
<el-button type="warning" icon="el-icon-edit" @click="toEditPage()">编辑</el-button>
|
||||
<el-button type="success" icon="el-icon-data-line" @click="toFlyDataPage(countSelIdArr($refs.myTable.selection))">飞行数据</el-button>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="$router.replace('/register/add')">添加</el-button>
|
||||
<el-button type="danger" icon="el-icon-delete" @click="deleteAir(countSelIdArr($refs.myTable.selection))">删除
|
||||
</el-button>
|
||||
<el-button type="warning" icon="el-icon-edit" @click="toEditPage()">编辑</el-button>
|
||||
<el-button type="warning" class="swarmButton" icon="iconfont icon-a-jiqunkongzhianniu_huaban1" @click="toSwarmPage()">集群控制</el-button>
|
||||
<el-button type="success" icon="el-icon-data-line"
|
||||
@click="toFlyDataPage(countSelIdArr($refs.myTable.selection))">飞行数据</el-button>
|
||||
</el-button-group>
|
||||
<!-- 用户select选项 -->
|
||||
<el-button-group class="m-b-20">
|
||||
<SelectionShopId v-model="form.shop_id" :allSel="true" />
|
||||
</el-button-group>
|
||||
<!-- 飞机表格 -->
|
||||
<el-table class="w-100" ref="myTable"
|
||||
:data="airListArr.slice((currentPage - 1) * pageSize, currentPage * pageSize)" border tooltip-effect="dark">
|
||||
<el-table class="w-100" ref="myTable" :data="airListArr.slice((currentPage - 1) * pageSize, currentPage * pageSize)"
|
||||
border tooltip-effect="dark">
|
||||
<el-table-column align="center" type="selection" width="40">
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="id" label="id" width="50">
|
||||
@ -117,6 +119,20 @@ export default {
|
||||
this.$message.error('只能选择一条记录')
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description: 跳转到编辑页面
|
||||
*/
|
||||
toSwarmPage () {
|
||||
const selId = this.countSelIdArr(this.$refs.myTable.selection)
|
||||
if (selId.length < 2) {
|
||||
this.$message.error('请选择至少两架飞机')
|
||||
return
|
||||
}
|
||||
// 用缓存传参 swarmIdArr
|
||||
this.$store.commit('app/setSwarmIdArr', selId)
|
||||
// 跳转到集群控制页面
|
||||
this.$router.push('/planes/swarm')
|
||||
},
|
||||
/**
|
||||
* @description: 跳转到飞机数据统计页面
|
||||
*/
|
||||
@ -155,6 +171,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.swarmButton {
|
||||
background-color: #7C4DFF;
|
||||
border-color: #7C4DFF
|
||||
}
|
||||
|
||||
.no-wrap-btn-group {
|
||||
white-space: nowrap;
|
||||
/* 禁止换行 */
|
||||
|
||||
@ -16,21 +16,27 @@
|
||||
<el-menu v-show="show" class="border-n" :router="true" :default-active="activeMenu" :unique-opened="true"
|
||||
background-color="#304156" text-color="rgb(191, 203, 217)" active-text-color="#409EFF"
|
||||
:collapse-transition="false" :collapse="isCollapse">
|
||||
<template v-for="(route, index) in routes">
|
||||
<el-menu-item @click="lt480Collapse()" v-if="route.children.length < 2" :key="route.path" :index="route.children[0].path">
|
||||
<i class="fc" :class="route.children[0].meta.icon"></i>
|
||||
<span slot="title">{{ route.children[0].meta.title }}</span>
|
||||
</el-menu-item>
|
||||
<el-submenu v-else :key="index" :index="route.path">
|
||||
<template slot="title">
|
||||
<i class="fc" :class="route.meta.icon"></i>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<el-menu-item @click="lt480Collapse()" v-for="child in route.children" :key="child.path" :index="child.path">
|
||||
<i class="fc" :class="child.meta.icon"></i>
|
||||
<span>{{ child.meta.title }}</span>
|
||||
<template v-for="route in routes">
|
||||
<!-- 判断 moduleVisibilityMap 是否允许显示 -->
|
||||
<template v-if="$store.state.app.moduleVisibilityMap[route.path.replace('/', '')]">
|
||||
<el-menu-item v-if="route.children.length < 2" :key="route.path + '-single'" :index="route.children[0].path"
|
||||
@click="lt480Collapse()">
|
||||
<i class="fc" :class="route.children[0].meta.icon"></i>
|
||||
<span slot="title">{{ route.children[0].meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu v-else :key="route.path + '-group'" :index="route.path">
|
||||
<template slot="title">
|
||||
<i class="fc" :class="route.meta.icon"></i>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="child in route.children" :key="child.path" :index="child.path"
|
||||
@click="lt480Collapse()">
|
||||
<i class="fc" :class="child.meta.icon"></i>
|
||||
<span>{{ child.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu>
|
||||
</transition>
|
||||
@ -111,25 +117,34 @@ export default {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description: 动态加载路由
|
||||
*/
|
||||
* @description: 动态加载无人机子路由,并保留静态配置(如集群控制)
|
||||
* @param {Array} planes - 飞机对象数组,每个对象包含 id 和 name
|
||||
*/
|
||||
loadRoute (planes) {
|
||||
const arr = new Array(0)
|
||||
planes.map((item, index) => {
|
||||
arr[index] = {
|
||||
path: '/planes/index/' + item.id + '/' + item.name,
|
||||
component: () => import('@/views/layout/components/main/planes/index.vue'),
|
||||
meta: {
|
||||
title: item.name,
|
||||
icon: 'iconfont icon-wurenji',
|
||||
roles: ['admin', 'editor'],
|
||||
tapName: 'plane'
|
||||
}
|
||||
// 1. 将每架飞机转换为一个路由配置对象
|
||||
const dynamicPlaneRoutes = planes.map((item) => ({
|
||||
path: '/planes/index/' + item.id + '/' + item.name, // 动态路径,携带飞机ID和名称
|
||||
component: () => import('@/views/layout/components/main/planes/index.vue'), // 对应的组件路径
|
||||
meta: {
|
||||
title: item.name, // 用于左侧菜单或标签页显示
|
||||
icon: 'iconfont icon-wurenji', // 图标(可替换)
|
||||
roles: ['admin', 'editor'], // 权限控制
|
||||
tapName: 'plane' // 自定义标记
|
||||
// activeMenu: '/planes/swarm' // 可选:保持菜单高亮在“集群控制”
|
||||
}
|
||||
})
|
||||
this.routes.map((element) => {
|
||||
if (element.meta.title === '无人机') {
|
||||
element.children = arr
|
||||
}))
|
||||
|
||||
// 2. 遍历顶层路由配置,找到 path 为 "/planes" 的主路由
|
||||
this.routes.forEach((element) => {
|
||||
if (element.path === '/planes') {
|
||||
// 3. 过滤原本 children 中的静态路由(排除旧的动态飞机 index 路由)
|
||||
const staticChildren = element.children?.filter(child => !child.path.startsWith('/planes/index/')) || []
|
||||
|
||||
// 4. 合并:静态子路由(如 /planes/swarm) + 动态生成的飞机子路由
|
||||
element.children = [
|
||||
...staticChildren,
|
||||
...dynamicPlaneRoutes
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -140,8 +155,8 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* @description: 监听飞机列表 有刷新导航栏
|
||||
*/
|
||||
* @description: 监听飞机列表 有刷新导航栏
|
||||
*/
|
||||
airList (val) {
|
||||
this.loadRoute(val)
|
||||
this.$forceUpdate()// 刷新本组件
|
||||
|
||||
@ -151,13 +151,26 @@ export default {
|
||||
plane.planeState.heartRandom = Math.random()
|
||||
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
|
||||
|
||||
// 解锁状态变更检测:用于控制飞行数据的采集与上传
|
||||
if (!oldMark && newMark) {
|
||||
// 从加锁切换为解锁:初始化飞行数据记录
|
||||
plane.planeState.flyDataMark = true
|
||||
plane.planeState.isUnlock = true
|
||||
plane.planeState.flyDataSave = {
|
||||
startTime: Math.floor(Date.now() / 1000), // 解锁时间(秒)
|
||||
startBattery: plane.planeState.batteryRemaining,
|
||||
@ -166,7 +179,7 @@ export default {
|
||||
path: []
|
||||
}
|
||||
} else if (oldMark && !newMark) {
|
||||
plane.planeState.flyDataMark = false
|
||||
plane.planeState.isUnlock = false
|
||||
|
||||
this.handleFlightDataUpload(plane) // 上传飞行数据
|
||||
|
||||
@ -192,7 +205,7 @@ export default {
|
||||
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([
|
||||
position.lon / 10e6,
|
||||
position.lat / 10e6,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user