643 lines
19 KiB
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> |