2025-06-13 20:07:33 +08:00
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
<div class="fly-data-wrapper">
|
|
|
|
|
<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>
|
2025-06-14 21:42:02 +08:00
|
|
|
|
<el-radio-button label="飞行轨迹"></el-radio-button>
|
2025-06-13 20:07:33 +08:00
|
|
|
|
</el-radio-group>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-06-14 21:42:02 +08:00
|
|
|
|
<div class="chart-area" v-if="flyDataList.length">
|
|
|
|
|
<div v-if="boxShow" id="main" class="chart-container"></div>
|
|
|
|
|
<map-box v-else ref="mapbox" />
|
2025-06-13 20:07:33 +08:00
|
|
|
|
</div>
|
2025-06-14 21:42:02 +08:00
|
|
|
|
<div v-else class="no-data-tip">暂无数据</div>
|
2025-06-13 20:07:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import * as echarts from 'echarts'
|
2025-06-14 21:42:02 +08:00
|
|
|
|
import MapBox from '@/components/MapBox'
|
2025-06-13 20:07:33 +08:00
|
|
|
|
import { getFlyData } from '@/utils/api/table'
|
|
|
|
|
import DateRangePicker from '@/components/DateRangePicker'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'FlyData',
|
|
|
|
|
data () {
|
|
|
|
|
const end = new Date()
|
|
|
|
|
end.setHours(23, 59, 59, 999)
|
|
|
|
|
const start = new Date()
|
|
|
|
|
start.setDate(end.getDate() - 6)
|
|
|
|
|
start.setHours(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
flyDataList: [],
|
|
|
|
|
selectedPlaneIdArr: this.$store.state.app.toFlyDataIdArr,
|
|
|
|
|
dateRange: [start, end],
|
2025-06-14 21:42:02 +08:00
|
|
|
|
radioClass: '飞行时长',
|
|
|
|
|
boxShow: true
|
2025-06-13 20:07:33 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
components: {
|
2025-06-14 21:42:02 +08:00
|
|
|
|
DateRangePicker,
|
|
|
|
|
MapBox
|
2025-06-13 20:07:33 +08:00
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
source () {
|
|
|
|
|
if (!this.flyDataList.length) return []
|
|
|
|
|
const start = this.dateRange[0]
|
|
|
|
|
const end = this.dateRange[1]
|
|
|
|
|
const rangeInMs = end - start
|
|
|
|
|
const oneDay = 24 * 60 * 60 * 1000
|
|
|
|
|
const oneYear = 365 * oneDay
|
|
|
|
|
|
|
|
|
|
let groupBy = 'day'
|
|
|
|
|
if (rangeInMs > oneYear) {
|
|
|
|
|
groupBy = 'year'
|
|
|
|
|
} else if (rangeInMs > 30 * oneDay) {
|
|
|
|
|
groupBy = 'month'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatMap = {
|
2025-06-14 18:35:06 +08:00
|
|
|
|
day: (d) => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
|
2025-06-13 20:07:33 +08:00
|
|
|
|
month: (d) => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}`,
|
|
|
|
|
year: (d) => `${d.getFullYear()}`
|
|
|
|
|
}
|
|
|
|
|
const formatFn = formatMap[groupBy]
|
|
|
|
|
|
|
|
|
|
const timeLabels = []
|
|
|
|
|
const cursor = new Date(start.getTime())
|
|
|
|
|
const endTime = end.getTime()
|
|
|
|
|
|
|
|
|
|
while (cursor.getTime() <= endTime) {
|
|
|
|
|
timeLabels.push(formatFn(new Date(cursor)))
|
|
|
|
|
if (groupBy === 'day') {
|
|
|
|
|
cursor.setDate(cursor.getDate() + 1)
|
|
|
|
|
} else if (groupBy === 'month') {
|
|
|
|
|
cursor.setMonth(cursor.getMonth() + 1)
|
|
|
|
|
} else if (groupBy === 'year') {
|
|
|
|
|
cursor.setFullYear(cursor.getFullYear() + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const groupedData = {}
|
|
|
|
|
|
|
|
|
|
const keyMap = {
|
|
|
|
|
飞行时长: (item) => {
|
|
|
|
|
if (!item.start_time || !item.end_time) return 0
|
|
|
|
|
return (item.end_time - item.start_time) / 60
|
|
|
|
|
},
|
|
|
|
|
飞行距离: (item) => Number(item.distance || 0),
|
|
|
|
|
消耗电量: (item) => Number(item.power_used || 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.flyDataList.forEach(item => {
|
|
|
|
|
const planeName = item.plane_name
|
|
|
|
|
const date = new Date(item.start_time * 1000)
|
|
|
|
|
const key = formatFn(date)
|
|
|
|
|
|
|
|
|
|
if (!groupedData[planeName]) groupedData[planeName] = {}
|
|
|
|
|
if (!groupedData[planeName][key]) groupedData[planeName][key] = 0
|
|
|
|
|
|
|
|
|
|
groupedData[planeName][key] += keyMap[this.radioClass](item)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const source = [['product', ...timeLabels]]
|
|
|
|
|
|
|
|
|
|
for (const planeName in groupedData) {
|
|
|
|
|
const row = [planeName]
|
|
|
|
|
for (const label of timeLabels) {
|
|
|
|
|
row.push(groupedData[planeName][label] || 0)
|
|
|
|
|
}
|
|
|
|
|
source.push(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return source
|
|
|
|
|
},
|
|
|
|
|
series () {
|
|
|
|
|
const lineSeries = this.source.slice(1).map(() => ({
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
seriesLayoutBy: 'row',
|
|
|
|
|
emphasis: { focus: 'series' }
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const firstColumn = this.source[0]?.[1] || ''
|
|
|
|
|
if (!firstColumn) return lineSeries
|
|
|
|
|
|
|
|
|
|
const pieSeries = {
|
|
|
|
|
type: 'pie',
|
|
|
|
|
id: 'pie',
|
|
|
|
|
radius: '30%',
|
|
|
|
|
center: ['50%', '25%'],
|
|
|
|
|
emphasis: { focus: 'self' },
|
|
|
|
|
label: {
|
|
|
|
|
formatter: `{b}: {@[${firstColumn}]} ({d}%)`
|
|
|
|
|
},
|
|
|
|
|
encode: {
|
|
|
|
|
itemName: 'product',
|
|
|
|
|
value: firstColumn,
|
|
|
|
|
tooltip: firstColumn
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...lineSeries, pieSeries]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
created () {
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
flyDataList (newVal) {
|
2025-06-14 21:42:02 +08:00
|
|
|
|
// 图标销毁 和 初始化
|
2025-06-13 20:07:33 +08:00
|
|
|
|
if (!newVal.length && this.myChart) {
|
|
|
|
|
this.myChart.dispose()
|
|
|
|
|
this.myChart = null
|
|
|
|
|
} else if (newVal.length) {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.initChart()
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-14 21:42:02 +08:00
|
|
|
|
// 多个飞机轨迹全部绘制
|
|
|
|
|
if (!this.boxShow && newVal.length) {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.drawAllPathsOnMap()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
boxShow (newVal) {
|
|
|
|
|
if (!newVal) { // boxShow 为 false,显示地图时重新绘制轨迹
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.drawAllPathsOnMap()
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-13 20:07:33 +08:00
|
|
|
|
},
|
|
|
|
|
dateRange: {
|
|
|
|
|
handler () {
|
|
|
|
|
this.loadFlyData()
|
|
|
|
|
},
|
|
|
|
|
immediate: true
|
|
|
|
|
},
|
|
|
|
|
source (newVal) {
|
|
|
|
|
if (newVal.length > 1) {
|
|
|
|
|
this.initChart()
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-06-14 21:42:02 +08:00
|
|
|
|
radioClass (val) {
|
|
|
|
|
if (val === '飞行轨迹') {
|
|
|
|
|
this.boxShow = false
|
|
|
|
|
} else {
|
|
|
|
|
this.boxShow = true
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.initChart()
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-13 20:07:33 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
2025-06-14 21:42:02 +08:00
|
|
|
|
// 地图轨迹绘制
|
|
|
|
|
drawAllPathsOnMap () {
|
|
|
|
|
if (!this.$refs.mapbox) return
|
|
|
|
|
if (!this.flyDataList.length) return
|
|
|
|
|
|
|
|
|
|
// 清理旧轨迹(如果你的mapbox组件支持)
|
|
|
|
|
this.$refs.mapbox.clearPaths && this.$refs.mapbox.clearPaths()
|
|
|
|
|
|
|
|
|
|
this.flyDataList.forEach(item => {
|
|
|
|
|
if (item.gps_path) {
|
|
|
|
|
const pathArray = JSON.parse(item.gps_path)
|
|
|
|
|
this.$refs.mapbox.createPathWithArray(pathArray)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 获取飞行数据
|
2025-06-13 20:07:33 +08:00
|
|
|
|
async loadFlyData () {
|
|
|
|
|
if (this.selectedPlaneIdArr.length === 0) {
|
|
|
|
|
this.$router.push('/register/index')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.dateRange || this.dateRange.length !== 2) {
|
|
|
|
|
this.$message.warning('请选择日期范围')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startTimestamp = Math.floor(this.dateRange[0].getTime() / 1000)
|
|
|
|
|
const endTimestamp = Math.floor(this.dateRange[1].getTime() / 1000)
|
|
|
|
|
|
|
|
|
|
const res = await getFlyData(this.selectedPlaneIdArr, startTimestamp, endTimestamp)
|
|
|
|
|
if (res.data.status === 1) {
|
|
|
|
|
this.flyDataList = res.data.dataList
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.error(res.data.msg)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
initChart () {
|
|
|
|
|
const chartDom = document.getElementById('main')
|
|
|
|
|
if (!chartDom) return
|
|
|
|
|
|
|
|
|
|
if (this.myChart) {
|
|
|
|
|
this.myChart.dispose()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.myChart = echarts.init(chartDom)
|
|
|
|
|
|
|
|
|
|
const firstColumnIndex = 1
|
|
|
|
|
|
|
|
|
|
this.myChart.setOption({
|
|
|
|
|
legend: {},
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
showContent: true
|
|
|
|
|
},
|
|
|
|
|
dataset: {
|
|
|
|
|
source: this.source
|
|
|
|
|
},
|
|
|
|
|
xAxis: { type: 'category' },
|
|
|
|
|
yAxis: {
|
|
|
|
|
gridIndex: 0,
|
|
|
|
|
name: this.radioClass === '飞行时长' ? '分钟' : this.radioClass === '飞行距离' ? '米' : 'mAh'
|
|
|
|
|
},
|
|
|
|
|
grid: { top: '55%' },
|
|
|
|
|
series: [
|
|
|
|
|
...this.source.slice(1).map(() => ({
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
seriesLayoutBy: 'row',
|
|
|
|
|
emphasis: { focus: 'series' }
|
|
|
|
|
})),
|
|
|
|
|
{
|
|
|
|
|
type: 'pie',
|
|
|
|
|
id: 'pie',
|
|
|
|
|
radius: '30%',
|
|
|
|
|
center: ['50%', '25%'],
|
|
|
|
|
emphasis: { focus: 'self' },
|
|
|
|
|
label: {
|
|
|
|
|
formatter: `{b}: {@[${firstColumnIndex}]} ({d}%)`
|
|
|
|
|
},
|
|
|
|
|
encode: {
|
|
|
|
|
itemName: 'product',
|
|
|
|
|
value: firstColumnIndex,
|
|
|
|
|
tooltip: firstColumnIndex
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.myChart.on('updateAxisPointer', (event) => {
|
|
|
|
|
const xAxisInfo = event.axesInfo[0]
|
|
|
|
|
if (xAxisInfo) {
|
|
|
|
|
const dimension = xAxisInfo.value + 1
|
|
|
|
|
this.myChart.setOption({
|
|
|
|
|
series: {
|
|
|
|
|
id: 'pie',
|
|
|
|
|
label: {
|
|
|
|
|
formatter: `{b}: {@[${dimension}]} ({d}%)`
|
|
|
|
|
},
|
|
|
|
|
encode: {
|
|
|
|
|
value: dimension,
|
|
|
|
|
tooltip: dimension
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
this.myChart.resize()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
@import "@/styles/theme.scss";
|
|
|
|
|
|
|
|
|
|
.fly-data-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.top-bar {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-area {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
</style>
|