food/src/views/layout/components/main/register/flyData.vue
air 99e7caffd5 【类 型】:feat
【原  因】:地图组件 添加一个保存数据到本地的控件  的功能
【过  程】:飞行数据统计-飞行轨迹 把数据传到地图组件  有控件把数据保存到本地文件
【影  响】:

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

363 lines
9.5 KiB
Vue

<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>
<el-radio-button label="飞行轨迹"></el-radio-button>
</el-radio-group>
</div>
<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"/>
</div>
<div v-else class="no-data-tip">暂无数据</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import MapBox from '@/components/MapBox'
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],
radioClass: '飞行时长',
boxShow: true
}
},
components: {
DateRangePicker,
MapBox
},
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 = {
day: (d) => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
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 Math.round((item.end_time - item.start_time) / 60)
},
飞行距离: (item) => Number(item.distance || 0),
消耗电量: (item) => Number(item.power_used || 0)
}
if (this.radioClass === '飞行轨迹') return []// 不处理轨迹数据
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) {
// 图标销毁 和 初始化
if (!newVal.length && this.myChart) {
this.myChart.dispose()
this.myChart = null
} else if (newVal.length) {
this.$nextTick(() => {
this.initChart()
})
}
// 多个飞机轨迹全部绘制
if (!this.boxShow && newVal.length) {
this.onMapReady()
}
},
boxshow (val) {
if (!val) {
this.onMapReady()
}
},
dateRange: {
handler () {
this.loadFlyData()
},
immediate: true
},
source (newVal) {
if (Array.isArray(newVal) && newVal.length > 1) {
this.initChart()
}
},
radioClass (val) {
if (val === '飞行轨迹') {
this.boxShow = false
} else {
this.boxShow = true
this.$nextTick(() => {
this.initChart()
})
}
}
},
methods: {
// 地图组件回调地图加载完成后 执行
onMapReady () {
this.drawAllPathsOnMap()
},
// 地图轨迹绘制
drawAllPathsOnMap () {
if (!this.$refs.mapbox) return
// 清理旧轨迹
this.$refs.mapbox.clearMapElements(['path'], ['path'])
// 遍历所有飞行数据,绘制轨迹
this.flyDataList.forEach(item => {
if (!item.gps_path) return
try {
const pathArray = JSON.parse(item.gps_path)
if (Array.isArray(pathArray) && pathArray.length > 0) {
this.$refs.mapbox.createPathWithArray(pathArray)
}
} catch (e) {
console.warn('gps_path 解析失败', item.gps_path)
}
})
// 跳转到第一个轨迹的起点(经纬度)
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 })
}
} catch (e) {
// 解析失败不跳转
}
}
},
// 获取飞行数据
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 === '飞行距离' ? '米' : '毫安'
},
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>