Compare commits

...

8 Commits

Author SHA1 Message Date
fd16bc4f63 【类 型】:feat
【原  因】:1设置页面添加语言设置项 2管理员权限加上 权限设置但选项
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-23 03:27:42 +08:00
78c2d7de21 【类 型】:feat
【原  因】:增加 系统设置 控制模块显示 隐藏
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-23 03:02:54 +08:00
e14aa9a975 【类 型】:feat
【原  因】:
【过  程】:1.优化飞机状态组件 在飞将列表里面加入 在线字段 解锁字段 2.增加概况多架飞机 状态组件
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-23 01:00:21 +08:00
e7506728af 【类 型】:feat
【原  因】:完成集群飞机控制主页面  和  控制组件
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-22 20:39:40 +08:00
9f48971c72 【类 型】:feat
【原  因】:创建集群控制 相关组件
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-22 18:30:19 +08:00
5f711afb1d 【类 型】:fix
【原  因】:指点时候 会从飞机状态里拿高度 但是更进入页面 数组里没有高度信息 控制台回台出警告
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-22 15:21:04 +08:00
3b4940ac4a 【类 型】:feat
【原  因】:创建集群控制组件页面 添加到版本管理
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-22 15:19:51 +08:00
6126b9d137 【类 型】:factor
【原  因】:完善 飞行数据统计的1.架次统计  2.飞行轨迹的bug修复
【过  程】:
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
2025-06-22 15:05:06 +08:00
20 changed files with 1401 additions and 134 deletions

View File

@ -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>

View File

@ -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)
}
}
}
}

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 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({

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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'),

View File

@ -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, // 电池电量

View File

@ -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 = {

View File

@ -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阿里巴巴

View File

@ -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', //

View File

@ -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) {

View File

@ -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>

View File

@ -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) {

View 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>

View File

@ -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: [

View File

@ -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;
/* 禁止换行 */

View File

@ -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()//

View File

@ -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,