aircraft-admin/src/views/analysis/taskAnalysis/index.vue

643 lines
19 KiB
Vue

<template>
<div class="order-analysis-container">
<div class="main-content">
<div class="content-box">
<div class="content-section">
<h1 class="page-title">任务分析统计</h1>
<!-- 数据概览卡片 -->
<div class="card-grid">
<div v-for="card in statCards" :key="card.label" class="stat-card" :class="card.colorClass">
<div class="card-header">
<span class="card-label">{{ card.label }}</span>
<div class="card-icon">
<i :class="card.icon"></i>
</div>
</div>
<div class="card-value">{{ stats[card.valueKey] }}{{ card.unit }}</div>
<div class="card-footer">{{ card.footerText }}: {{ stats[card.footerValueKey] }}</div>
</div>
</div>
<!-- 筛选条件区域 -->
<div class="filter-section">
<div class="filter-group">
<div class="filter-item">
<span class="filter-label">时间维度:</span>
<div class="time-range-buttons">
<button
v-for="range in timeRanges"
:key="range.value"
:class="['time-button', { active: filter.timeRange === range.value }]"
@click="changeTimeRange(range.value)"
>
{{ range.label }}
</button>
</div>
</div>
<div class="filter-item">
<span class="filter-label">统计日期:</span>
<div class="date-range-picker">
<el-date-picker
v-model="dateRange[0]"
:type="datePickerType"
:placeholder="startPlaceholder"
:format="dateFormat"
:value-format="dateValueFormat"
@change="handleStartDateChange"
class="single-date-picker"
:picker-options="startPickerOptions"
/>
<span class="separator">-</span>
<el-date-picker
v-model="dateRange[1]"
:type="datePickerType"
:placeholder="endPlaceholder"
:format="dateFormat"
:value-format="dateValueFormat"
@change="handleEndDateChange"
class="single-date-picker"
:picker-options="endPickerOptions"
/>
</div>
</div>
</div>
<el-button type="primary" :loading="loading" @click="refreshData">
<i class="el-icon-refresh"></i>刷新
</el-button>
</div>
<!-- 图表区域 -->
<div class="chart-section">
<div class="chart-box">
<h3 class="chart-title">任务完成趋势分析</h3>
<div class="chart-wrapper" ref="trendChartContainer"></div>
</div>
</div>
<!-- 任务列表区域 -->
<div class="task-list-section">
<h3 class="section-title">任务详情列表</h3>
<el-table
:data="orderDetailList"
border
stripe
class="task-detail-table"
:header-cell-style="{background:'#f5f7fa', color:'#333'}"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="orderId" label="订单ID" width="100" align="center" />
<el-table-column prop="deviceId" label="设备ID" width="150" align="center" show-overflow-tooltip />
<el-table-column prop="operatorId" label="操作员ID" width="120" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column prop="orderItemStatus" label="状态" width="120" align="center">
<template #default="{row}">
<el-tag :type="getStatusTagType(row.orderItemStatus)">
{{ getStatusText(row.orderItemStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personCount" label="人数" width="100" align="center" />
<el-table-column prop="cargoWeight" label="货物重量(kg)" width="150" align="center" />
<el-table-column
label="操作"
width="120"
fixed="right"
/>
</el-table>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getDailyOrderAnalysis, getMonthlyOrderAnalysis, getYearlyOrderAnalysis } from '@/api/analysis/taskAnalysis'
export default {
name: 'taskAnalysis',
data() {
return {
loading: false,
trendChart: null,
timeRanges: [
{ label: '日', value: 'day' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
],
statCards: [
{ label: '任务完成率', valueKey: 'completionRate', unit: '%', footerText: '完成数/总数', footerValueKey: 'completedTaskCount', icon: 'el-icon-success', colorClass: 'green-card' },
{ label: '任务失败率', valueKey: 'failedRate', unit: '%', footerText: '失败数', footerValueKey: 'failedTaskCount', icon: 'el-icon-error', colorClass: 'red-card' },
{ label: '平均任务周期', valueKey: 'averageTaskCycle', unit: '分钟', footerText: '进行中', footerValueKey: 'processingTaskCount', icon: 'el-icon-time', colorClass: 'blue-card' },
{ label: '无人机使用率', valueKey: 'droneUsageRate', unit: '%', footerText: '未开始', footerValueKey: 'notStartedTaskCount', icon: 'el-icon-data-line', colorClass: 'purple-card' }
],
filter: {
timeRange: 'month',
startDate: '',
endDate: ''
},
dateRange: [],
startPickerOptions: {
disabledDate: time => this.dateRange[1] && time.getTime() > new Date(this.dateRange[1]).getTime()
},
endPickerOptions: {
disabledDate: time => this.dateRange[0] && time.getTime() < new Date(this.dateRange[0]).getTime()
},
stats: {
averageTaskCycle: 0,
completedTaskCount: 0,
completionRate: 0,
droneUsageRate: 0,
failedRate: 0,
failedTaskCount: 0,
notStartedTaskCount: 0,
processingTaskCount: 0,
totalTaskCount: 0
},
orderDetailList: [],
chartData: {
dates: [],
completed: [],
failed: []
}
}
},
computed: {
datePickerType() {
return this.filter.timeRange === 'year' ? 'year' :
this.filter.timeRange === 'month' ? 'month' : 'date'
},
dateFormat() {
return this.filter.timeRange === 'year' ? 'yyyy年' :
this.filter.timeRange === 'month' ? 'yyyy年MM月' : 'yyyy年MM月dd日'
},
dateValueFormat() {
return this.filter.timeRange === 'year' ? 'yyyy' :
this.filter.timeRange === 'month' ? 'yyyy-MM' : 'yyyy-MM-dd'
},
startPlaceholder() {
return this.filter.timeRange === 'year' ? '开始年份' :
this.filter.timeRange === 'month' ? '开始月份' : '开始日期'
},
endPlaceholder() {
return this.filter.timeRange === 'year' ? '结束年份' :
this.filter.timeRange === 'month' ? '结束月份' : '结束日期'
}
},
mounted() {
this.initDefaultDateRange()
this.fetchData()
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
if (this.trendChart) this.trendChart.dispose()
window.removeEventListener('resize', this.handleResize)
},
methods: {
initDefaultDateRange() {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
switch (this.filter.timeRange) {
case 'year':
this.dateRange = [`${year - 2}-01-01`, `${year}-12-31`]
break
case 'month':
const startDate = new Date(year, now.getMonth() - 5, 1)
const startMonth = `${startDate.getFullYear()}-${(startDate.getMonth() + 1).toString().padStart(2, '0')}`
this.dateRange = [startMonth, `${year}-${month}`]
break
default:
const endDay = now.toISOString().split('T')[0]
const startDay = new Date()
startDay.setDate(now.getDate() - 6)
this.dateRange = [startDay.toISOString().split('T')[0], endDay]
}
this.filter.startDate = this.formatDateForApi(this.dateRange[0])
this.filter.endDate = this.formatDateForApi(this.dateRange[1])
},
formatDateForApi(date) {
if (!date) return ''
const dateObj = new Date(date)
if (this.filter.timeRange === 'year') return dateObj.getFullYear().toString()
if (this.filter.timeRange === 'month') return `${dateObj.getFullYear()}-${(dateObj.getMonth() + 1).toString().padStart(2, '0')}`
return date
},
async fetchData() {
this.loading = true
try {
const params = { startDate: this.filter.startDate, endDate: this.filter.endDate }
const apiMap = {
day: getDailyOrderAnalysis,
month: getMonthlyOrderAnalysis,
year: getYearlyOrderAnalysis
}
const res = await apiMap[this.filter.timeRange](params)
this.stats = {
averageTaskCycle: res.averageTaskCycle || 0,
completedTaskCount: res.completedTaskCount || 0,
completionRate: res.completionRate || 0,
droneUsageRate: res.droneUsageRate || 0,
failedRate: res.failedRate || 0,
failedTaskCount: res.failedTaskCount || 0,
notStartedTaskCount: res.notStartedTaskCount || 0,
processingTaskCount: res.processingTaskCount || 0,
totalTaskCount: res.totalTaskCount || 0
}
this.orderDetailList = res.orderDetailList || []
const keyMap = {
day: { time: 'date', completed: 'dailyCompletedCount', failed: 'dailyFailedCount' },
month: { time: 'month', completed: 'monthlyCompletedCount', failed: 'monthlyFailedCount' },
year: { time: 'year', completed: 'yearlyCompletedCount', failed: 'yearlyFailedCount' }
}
const keys = keyMap[this.filter.timeRange]
this.chartData = {
dates: res.timeSeriesData.map(item => item[keys.time]),
completed: res.timeSeriesData.map(item => item[keys.completed] || 0),
failed: res.timeSeriesData.map(item => item[keys.failed] || 0)
}
this.updateChart()
} catch (error) {
console.error('获取数据失败:', error)
this.$message.error('获取数据失败')
} finally {
this.loading = false
}
},
initChart() {
this.trendChart = echarts.init(this.$refs.trendChartContainer)
this.updateChart()
},
updateChart() {
if (!this.trendChart) return
const option = {
tooltip: {
trigger: 'axis',
formatter: params => {
return `
<div style="font-weight:bold;margin-bottom:5px">${params[0].axisValue}</div>
<div style="display:flex;justify-content:space-between">
<span style="color:#36a2eb">任务完成: ${params[0].value}次</span>
</div>
<div style="display:flex;justify-content:space-between">
<span style="color:#ff6384">任务失败: ${params[1].value}</span>
</div>
`
}
},
legend: { data: ['任务完成', '任务失败'], right: 20, top: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: this.chartData.dates,
axisLabel: {
rotate: this.chartData.dates.length > 10 ? 45 : 0,
formatter: value => {
if (this.filter.timeRange === 'day') return value.split('-').slice(1).join('-')
if (this.filter.timeRange === 'month') return value.split('-')[1] + '月'
return value + '年'
}
},
name: this.filter.timeRange === 'day' ? '日期' :
this.filter.timeRange === 'month' ? '月份' : '年份',
nameLocation: 'end',
},
yAxis: {
type: 'value',
name: '任务数量(次)',
min: 0,
max: Math.max(3, ...this.chartData.completed, ...this.chartData.failed) + 1,
interval: 1,
axisLabel: { formatter: value => Math.floor(value) === value ? value : '' }
},
series: [
{
name: '任务完成',
type: 'line',
smooth: true,
lineStyle: { width: 3, color: '#36a2eb' },
itemStyle: { color: '#36a2eb' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(54, 162, 235, 0.5)' },
{ offset: 1, color: 'rgba(54, 162, 235, 0.1)' }
])
},
data: this.chartData.completed
},
{
name: '任务失败',
type: 'line',
smooth: true,
lineStyle: { width: 3, color: '#ff6384' },
itemStyle: { color: '#ff6384' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 99, 132, 0.5)' },
{ offset: 1, color: 'rgba(255, 99, 132, 0.1)' }
])
},
data: this.chartData.failed
}
]
}
this.trendChart.setOption(option)
},
changeTimeRange(range) {
this.filter.timeRange = range
this.initDefaultDateRange()
this.refreshData()
},
handleStartDateChange(val) {
if (!val || (this.dateRange[1] && new Date(val) > new Date(this.dateRange[1]))) {
this.$message.warning('开始日期不能晚于结束日期')
return
}
this.dateRange = [val, this.dateRange[1] || val]
this.filter.startDate = this.formatDateForApi(val)
this.filter.endDate = this.formatDateForApi(this.dateRange[1])
this.refreshData()
},
handleEndDateChange(val) {
if (!val || (this.dateRange[0] && new Date(val) < new Date(this.dateRange[0]))) {
this.$message.warning('结束日期不能早于开始日期')
return
}
this.dateRange = [this.dateRange[0] || val, val]
this.filter.startDate = this.formatDateForApi(this.dateRange[0])
this.filter.endDate = this.formatDateForApi(val)
this.refreshData()
},
refreshData() {
this.fetchData()
},
handleResize() {
if (this.trendChart) this.trendChart.resize()
},
getStatusTagType(status) {
const types = ['info', 'primary', 'success']
return types[status] || 'info'
},
getStatusText(status) {
const texts = ['未开始', '进行中', '已完成']
return texts[status] || '未知状态'
}
}
}
</script>
<style scoped>
.order-analysis-container {
min-height: 100vh;
background-color: #f5f7fa;
font-family: 'Arial', sans-serif;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.content-box {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 24px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.stat-card {
padding: 12px;
min-height: 60px;
border-radius: 6px;
transition: transform 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.stat-card:hover {
transform: translateY(-5px);
}
.green-card { background: linear-gradient(to bottom right, #ecfdf5, white) }
.red-card { background: linear-gradient(to bottom right, #fff1f0, white) }
.blue-card { background: linear-gradient(to bottom right, #e6f7ff, white) }
.purple-card { background: linear-gradient(to bottom right, #f9f0ff, white) }
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.card-label {
font-size: 14px;
font-weight: 700;
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
}
.green-card .card-icon { color: #52c41a }
.red-card .card-icon { color: #ff4d4f }
.blue-card .card-icon { color: #1890ff }
.purple-card .card-icon { color: #722ed1 }
.card-value {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.card-footer {
font-size: 11px;
color: #666;
}
.filter-section {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
padding: 12px;
background-color: #f9fafb;
border-radius: 8px;
flex-wrap: wrap;
gap: 16px;
}
.filter-group {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.filter-item {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.filter-label {
font-size: 14px;
color: #666;
margin-right: 12px;
}
.time-range-buttons {
display: flex;
gap: 8px;
}
.time-button {
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #dcdfe6;
background: #f5f7fa;
cursor: pointer;
font-size: 14px;
}
.time-button.active {
background: #409eff;
color: white;
border-color: #409eff;
}
.date-range-picker {
display: flex;
align-items: center;
gap: 10px;
}
.chart-section {
margin-bottom: 32px;
}
.chart-box {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 20px;
}
.chart-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 16px;
}
.chart-wrapper {
width: 100%;
height: 400px;
}
.task-list-section {
margin-bottom: 24px;
}
.task-detail-table {
margin: 0 auto;
table-layout: fixed;
}
.task-detail-table .el-table__header-wrapper th {
background-color: #f5f7fa;
font-weight: bold;
}
.task-detail-table .el-table__body td {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.task-detail-table .el-table__row td {
padding: 8px 0;
}
.task-detail-table.el-table--fit {
width: 100% !important;
}
.section-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
.filter-section {
flex-direction: column;
}
.filter-group, .time-range-buttons, .date-range-picker {
width: 100%;
}
.filter-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>