统计模块中订单分析前后端对接
This commit is contained in:
parent
c2f18b9ac8
commit
51f722cab1
@ -11,7 +11,8 @@
|
|||||||
"preview": "node build/index.js --preview",
|
"preview": "node build/index.js --preview",
|
||||||
"test:unit": "jest --clearCache && vue-cli-service test:unit",
|
"test:unit": "jest --clearCache && vue-cli-service test:unit",
|
||||||
"svgo": "svgo -f src/assets/icons/svg --config=src/assets/icons/svgo.yml",
|
"svgo": "svgo -f src/assets/icons/svg --config=src/assets/icons/svgo.yml",
|
||||||
"new": "plop"
|
"new": "plop",
|
||||||
|
"serve": "vue-cli-service serve"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
@ -38,7 +39,7 @@
|
|||||||
"clipboard": "2.0.4",
|
"clipboard": "2.0.4",
|
||||||
"codemirror": "^5.49.2",
|
"codemirror": "^5.49.2",
|
||||||
"core-js": "^2.6.12",
|
"core-js": "^2.6.12",
|
||||||
"echarts": "^4.2.1",
|
"echarts": "^4.9.0",
|
||||||
"echarts-wordcloud": "^1.1.3",
|
"echarts-wordcloud": "^1.1.3",
|
||||||
"element-ui": "^2.15.14",
|
"element-ui": "^2.15.14",
|
||||||
"file-saver": "1.3.8",
|
"file-saver": "1.3.8",
|
||||||
@ -54,6 +55,7 @@
|
|||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
"screenfull": "4.2.0",
|
"screenfull": "4.2.0",
|
||||||
"sortablejs": "1.8.4",
|
"sortablejs": "1.8.4",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue-count-to": "^1.0.13",
|
"vue-count-to": "^1.0.13",
|
||||||
"vue-cropper": "0.4.9",
|
"vue-cropper": "0.4.9",
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta name="renderer" content="webkit">
|
<meta name="renderer" content="webkit">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title><%= webpackConfig.name %></title>
|
<title>
|
||||||
</head>
|
<%= webpackConfig.name %>
|
||||||
<body>
|
</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
32
src/api/analysis/orderAnalysis.js
Normal file
32
src/api/analysis/orderAnalysis.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getAllOrderAnalysis() {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderAnalysis',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDailyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderAnalysis/day',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthlyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderAnalysis/month',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getYearlyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderAnalysis/year',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
38
src/api/analysis/taskAnalysis.js
Normal file
38
src/api/analysis/taskAnalysis.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// src/api/analysis/taskAnalysis.js
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取子单任务分析统计
|
||||||
|
export function getOrderDetailAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderDetailAnalysis',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按日范围分析子单任务
|
||||||
|
export function getDailyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderDetailAnalysis/day',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按月范围分析子单任务
|
||||||
|
export function getMonthlyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderDetailAnalysis/month',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按年范围分析子单任务
|
||||||
|
export function getYearlyOrderAnalysis(params) {
|
||||||
|
return request({
|
||||||
|
url: 'aerocraftAdminApi/fmsOdOrderDetailAnalysis/year',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
@ -24,6 +24,7 @@ import router from './router/routers'
|
|||||||
import './assets/icons' // icon
|
import './assets/icons' // icon
|
||||||
import './router/index' // permission control
|
import './router/index' // permission control
|
||||||
|
|
||||||
|
|
||||||
Vue.use(checkPer)
|
Vue.use(checkPer)
|
||||||
Vue.use(permission)
|
Vue.use(permission)
|
||||||
Vue.use(dict)
|
Vue.use(dict)
|
||||||
|
@ -5,7 +5,8 @@ import Layout from '../layout/index'
|
|||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
export const constantRouterMap = [
|
export const constantRouterMap = [
|
||||||
{ path: '/login',
|
{
|
||||||
|
path: '/login',
|
||||||
meta: { title: '登录', noCache: true },
|
meta: { title: '登录', noCache: true },
|
||||||
component: (resolve) => require(['@/views/login'], resolve),
|
component: (resolve) => require(['@/views/login'], resolve),
|
||||||
hidden: true
|
hidden: true
|
||||||
|
@ -36,7 +36,7 @@ service.interceptors.response.use(
|
|||||||
if (error.response.data instanceof Blob && error.response.data.type.toLowerCase().indexOf('json') !== -1) {
|
if (error.response.data instanceof Blob && error.response.data.type.toLowerCase().indexOf('json') !== -1) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.readAsText(error.response.data, 'utf-8')
|
reader.readAsText(error.response.data, 'utf-8')
|
||||||
reader.onload = function(e) {
|
reader.onload = function (e) {
|
||||||
const errorMsg = JSON.parse(reader.result).message
|
const errorMsg = JSON.parse(reader.result).message
|
||||||
Notification.error({
|
Notification.error({
|
||||||
title: errorMsg,
|
title: errorMsg,
|
||||||
|
474
src/views/analysis/taskAnalysis/index.vue
Normal file
474
src/views/analysis/taskAnalysis/index.vue
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
<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 class="stat-card green-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-label">任务完成率</span>
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="el-icon-success"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ stats.completionRate }}%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务失败率卡片 -->
|
||||||
|
<div class="stat-card red-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-label">任务失败率</span>
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="el-icon-error"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ stats.failedRate }}%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 平均任务周期卡片 -->
|
||||||
|
<div class="stat-card blue-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-label">平均任务周期</span>
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="el-icon-time"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ stats.avgTaskDuration }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 今日无人机使用率卡片 -->
|
||||||
|
<div class="stat-card gray-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-label">今日无人机使用率</span>
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="el-icon-data-line"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ stats.droneUsageRate }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<div class="date-picker-container">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
align="right"
|
||||||
|
unlink-panels
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:picker-options="pickerOptions"
|
||||||
|
@change="handleDateChange"
|
||||||
|
>
|
||||||
|
</el-date-picker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-box">
|
||||||
|
<h3 class="chart-title">任务完成趋势分析
|
||||||
|
<span class="chart-date-range">{{ chartDateRange }}</span>
|
||||||
|
</h3>
|
||||||
|
<div class="chart-wrapper" ref="trendChartContainer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import {
|
||||||
|
getOrderDetailAnalysis,getMonthlyOrderAnalysis,getYearlyOrderAnalysis,
|
||||||
|
getDailyOrderAnalysis
|
||||||
|
} from '@/api/analysis/taskAnalysis'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'taskAnalysis',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
trendChart: null,
|
||||||
|
dateRange: [new Date(new Date().setDate(new Date().getDate() - 30)), new Date()],
|
||||||
|
pickerOptions: {
|
||||||
|
shortcuts: [{
|
||||||
|
text: '最近一周',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setDate(start.getDate() - 7)
|
||||||
|
picker.$emit('pick', [start, end])
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近一个月',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setDate(start.getDate() - 30)
|
||||||
|
picker.$emit('pick', [start, end])
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近三个月',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setDate(start.getDate() - 90)
|
||||||
|
picker.$emit('pick', [start, end])
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
completionRate: 0,
|
||||||
|
failedRate: 0,
|
||||||
|
avgTaskDuration: '0min',
|
||||||
|
droneUsageRate: 0
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
dates: [],
|
||||||
|
completed: [],
|
||||||
|
failed: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chartDateRange() {
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
const start = this.formatDate(this.dateRange[0])
|
||||||
|
const end = this.formatDate(this.dateRange[1])
|
||||||
|
return `${start} - ${end}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData()
|
||||||
|
this.initChart()
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.trendChart) {
|
||||||
|
this.trendChart.dispose()
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
try {
|
||||||
|
// 获取概览数据
|
||||||
|
const overviewRes = await getOrderDetailAnalysis()
|
||||||
|
console.log('接口返回数据:', overviewRes)
|
||||||
|
|
||||||
|
// 处理概览数据
|
||||||
|
this.stats = {
|
||||||
|
completionRate: overviewRes.completionRate || 0,
|
||||||
|
failedRate: overviewRes.failedRate || 0,
|
||||||
|
avgTaskDuration: '30min', // 接口未提供,使用默认值
|
||||||
|
droneUsageRate: 87.7 // 接口未提供,使用默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图表数据
|
||||||
|
const chartRes = await getDailyOrderAnalysis({
|
||||||
|
startDate: this.formatDate(this.dateRange[0]),
|
||||||
|
endDate: this.formatDate(this.dateRange[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理图表数据
|
||||||
|
this.chartData = {
|
||||||
|
dates: chartRes.timeSeriesData.map(item => item.date),
|
||||||
|
completed: chartRes.timeSeriesData.map(item => item.dailyCount || 0),
|
||||||
|
failed: chartRes.timeSeriesData.map(item => 0) // 接口未提供失败数据
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChart()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据失败:', error)
|
||||||
|
this.$message.error('获取数据失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initChart() {
|
||||||
|
this.trendChart = echarts.init(this.$refs.trendChartContainer)
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: params => {
|
||||||
|
const date = params[0].axisValue
|
||||||
|
const completed = params[0].value
|
||||||
|
const failed = params[1].value
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="font-weight:bold;margin-bottom:5px">${date}</div>
|
||||||
|
<div style="display:flex;justify-content:space-between">
|
||||||
|
<span style="color:#36a2eb">任务完成: ${completed}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between">
|
||||||
|
<span style="color:#ff6384">任务失败: ${failed}</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
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: '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)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChart() {
|
||||||
|
if (!this.trendChart) return
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
xAxis: {
|
||||||
|
data: this.chartData.dates
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{ data: this.chartData.completed },
|
||||||
|
{ data: this.chartData.failed }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trendChart.setOption(option)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDateChange() {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
if (this.trendChart) {
|
||||||
|
this.trendChart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
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 right, #f6ffed, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-card {
|
||||||
|
background: linear-gradient(to right, #fff1f0, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-card {
|
||||||
|
background: linear-gradient(to right, #e6f7ff, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-card {
|
||||||
|
background: linear-gradient(to right, #eceaea, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-card .card-icon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域样式 */
|
||||||
|
.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;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-date-range {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -91,9 +91,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kpi-value {
|
.kpi-value {
|
||||||
font-size: 32px;
|
font-size: 40px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 12px 0 8px;
|
margin: 20px 10px 8px 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-card:nth-child(1) .kpi-value {
|
.kpi-card:nth-child(1) .kpi-value {
|
||||||
@ -112,21 +112,6 @@
|
|||||||
color: var(--neutral-gray);
|
color: var(--neutral-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-change {
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-up {
|
|
||||||
color: var(--success-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-down {
|
|
||||||
color: var(--alert-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background: var(--light-gray);
|
background: var(--light-gray);
|
||||||
@ -145,13 +130,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dual-column {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 0 30px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-range-picker {
|
.date-range-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -164,20 +142,20 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-selector {
|
.time-granularity {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--dark-gray);
|
color: var(--dark-gray);
|
||||||
background: white;
|
background: white;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-holder {
|
.chart-holder {
|
||||||
height: 350px;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flatpickr 样式覆盖 */
|
|
||||||
.flatpickr-calendar {
|
.flatpickr-calendar {
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
@ -196,47 +174,30 @@
|
|||||||
<!-- 顶部KPI区域 -->
|
<!-- 顶部KPI区域 -->
|
||||||
<div class="kpi-grid">
|
<div class="kpi-grid">
|
||||||
<div class="kpi-card">
|
<div class="kpi-card">
|
||||||
<div>实时任务完成率</div>
|
<div>任务完成率</div>
|
||||||
<div class="kpi-value">82.5%</div>
|
<div class="kpi-value">82.5%</div>
|
||||||
<div class="trend-change trend-up">
|
|
||||||
<iconify-icon icon="mingcute:arrow-up-line" style="margin-right: 4px;"></iconify-icon>
|
|
||||||
较昨日 ↑2.1%
|
|
||||||
</div>
|
|
||||||
<iconify-icon icon="material-symbols:data-usage"
|
<iconify-icon icon="material-symbols:data-usage"
|
||||||
style="position: absolute; right: 38px; top: 38px; color: var(--success-green); font-size: 24px;"></iconify-icon>
|
style="position: absolute; right: 38px; top: 38px; color: var(--success-green); font-size: 24px;"></iconify-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kpi-card">
|
<div class="kpi-card">
|
||||||
<div>关键任务失败率</div>
|
<div>任务失败率</div>
|
||||||
<div class="kpi-value">5.2%</div>
|
<div class="kpi-value">5.2%</div>
|
||||||
<div class="trend-change trend-down">
|
|
||||||
<iconify-icon icon="mingcute:arrow-down-line" style="margin-right: 4px;"></iconify-icon>
|
|
||||||
同比 ↓1.3%
|
|
||||||
</div>
|
|
||||||
<iconify-icon icon="ic:outline-schedule"
|
<iconify-icon icon="ic:outline-schedule"
|
||||||
style="position: absolute; right: 38px; top: 38px; color: var(--alert-red); font-size: 24px;"></iconify-icon>
|
style="position: absolute; right: 38px; top: 38px; color: var(--alert-red); font-size: 24px;"></iconify-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kpi-card">
|
<div class="kpi-card">
|
||||||
<div>平均任务周期</div>
|
<div>平均任务周期</div>
|
||||||
<div class="kpi-value">4.8h</div>
|
<div class="kpi-value">30min</div>
|
||||||
<div class="trend-change">
|
|
||||||
<iconify-icon icon="mingcute:arrow-right-line"
|
|
||||||
style="margin-right: 4px; color: var(--neutral-gray);"></iconify-icon>
|
|
||||||
行业平均 5.2h
|
|
||||||
</div>
|
|
||||||
<iconify-icon icon="mdi:timer-outline"
|
<iconify-icon icon="mdi:timer-outline"
|
||||||
style="position: absolute; right: 38px; top: 38px; color: var(--primary-blue); font-size: 24px;"></iconify-icon>
|
style="position: absolute; right: 38px; top: 38px; color: var(--primary-blue); font-size: 24px;"></iconify-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kpi-card">
|
<div class="kpi-card">
|
||||||
<div>资源闲置率</div>
|
<div>今日无人机使用率</div>
|
||||||
<div class="kpi-value">12.3%</div>
|
<div class="kpi-value">87.7%</div>
|
||||||
<div class="trend-change trend-down">
|
<iconify-icon icon="mdi:drone"
|
||||||
<iconify-icon icon="mingcute:arrow-down-line" style="margin-right: 4px;"></iconify-icon>
|
|
||||||
周同比 ↓3.5%
|
|
||||||
</div>
|
|
||||||
<iconify-icon icon="mdi:server-off"
|
|
||||||
style="position: absolute; right: 38px; top: 38px; color: var(--neutral-gray); font-size: 24px;"></iconify-icon>
|
style="position: absolute; right: 38px; top: 38px; color: var(--neutral-gray); font-size: 24px;"></iconify-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,52 +206,32 @@
|
|||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
<span>任务完成趋势分析</span>
|
<span>任务完成趋势分析</span>
|
||||||
|
<div>
|
||||||
<div class="date-range-picker" id="dateRangePicker">
|
<div class="date-range-picker" id="dateRangePicker">
|
||||||
<iconify-icon icon="material-symbols:calendar-month"
|
<iconify-icon icon="material-symbols:calendar-month"
|
||||||
style="color: var(--neutral-gray); margin-right: 8px;"></iconify-icon>
|
style="color: var(--neutral-gray); margin-right: 8px;"></iconify-icon>
|
||||||
<span id="dateRangeDisplay">2023年7月1日 - 2023年7月31日</span>
|
<span id="dateRangeDisplay">2023年7月1日 - 2023年7月31日</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="trendChart" class="chart-holder"></div>
|
<div id="trendChart" class="chart-holder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部双列图表 -->
|
|
||||||
<div class="dual-column">
|
|
||||||
<!-- 左侧饼图 -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div class="chart-title">任务状态分布</div>
|
|
||||||
<div id="pieChart" class="chart-holder"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧堆叠柱状图 -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div class="chart-title">
|
|
||||||
<span>本周24小时任务状态分布</span>
|
|
||||||
<select id="daySelector" class="day-selector">
|
|
||||||
<option value="0">周一</option>
|
|
||||||
<option value="1">周二</option>
|
|
||||||
<option value="2">周三</option>
|
|
||||||
<option value="3">周四</option>
|
|
||||||
<option value="4">周五</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="stackedBarChart" class="chart-holder"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 模拟数据生成器
|
// 模拟数据生成器
|
||||||
const generateTrendData = (startDate, endDate) => {
|
const generateTrendData = (startDate, endDate, granularity = 'day') => {
|
||||||
const start = new Date(startDate);
|
const start = new Date(startDate);
|
||||||
const end = new Date(endDate);
|
const end = new Date(endDate);
|
||||||
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
|
|
||||||
const labels = [];
|
let labels = [];
|
||||||
const completed = [];
|
let completed = [];
|
||||||
const inProgress = [];
|
let failed = [];
|
||||||
const notStarted = [];
|
let prevCompleted = null;
|
||||||
const failed = [];
|
let prevFailed = null;
|
||||||
|
|
||||||
|
if (granularity === 'day') {
|
||||||
|
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
||||||
|
|
||||||
for (let i = 0; i < days; i++) {
|
for (let i = 0; i < days; i++) {
|
||||||
const date = new Date(start);
|
const date = new Date(start);
|
||||||
@ -299,17 +240,94 @@
|
|||||||
|
|
||||||
// 生成有波动趋势的数据
|
// 生成有波动趋势的数据
|
||||||
const baseCompleted = 300 + Math.sin(i / 3) * 50 + Math.random() * 30;
|
const baseCompleted = 300 + Math.sin(i / 3) * 50 + Math.random() * 30;
|
||||||
const baseInProgress = 150 + Math.cos(i / 2) * 30 + Math.random() * 20;
|
|
||||||
const baseNotStarted = 80 + Math.sin(i / 4) * 20 + Math.random() * 15;
|
|
||||||
const baseFailed = 10 + Math.abs(Math.sin(i / 5)) * 8 + Math.random() * 5;
|
const baseFailed = 10 + Math.abs(Math.sin(i / 5)) * 8 + Math.random() * 5;
|
||||||
|
|
||||||
completed.push(Math.round(baseCompleted));
|
const currentCompleted = Math.round(baseCompleted);
|
||||||
inProgress.push(Math.round(baseInProgress));
|
const currentFailed = Math.round(baseFailed);
|
||||||
notStarted.push(Math.round(baseNotStarted));
|
|
||||||
failed.push(Math.round(baseFailed));
|
completed.push({
|
||||||
|
value: currentCompleted,
|
||||||
|
prevValue: prevCompleted,
|
||||||
|
change: prevCompleted !== null ? ((currentCompleted - prevCompleted) / prevCompleted * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
failed.push({
|
||||||
|
value: currentFailed,
|
||||||
|
prevValue: prevFailed,
|
||||||
|
change: prevFailed !== null ? ((currentFailed - prevFailed) / prevFailed * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
prevCompleted = currentCompleted;
|
||||||
|
prevFailed = currentFailed;
|
||||||
|
}
|
||||||
|
} else if (granularity === 'month') {
|
||||||
|
const months = (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth()) + 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < months; i++) {
|
||||||
|
const date = new Date(start);
|
||||||
|
date.setMonth(start.getMonth() + i);
|
||||||
|
labels.push(`${date.getFullYear()}年${date.getMonth() + 1}月`);
|
||||||
|
|
||||||
|
// 生成有波动趋势的数据
|
||||||
|
const baseCompleted = 9000 + Math.sin(i / 2) * 1500 + Math.random() * 900;
|
||||||
|
const baseFailed = 300 + Math.abs(Math.sin(i / 3)) * 240 + Math.random() * 150;
|
||||||
|
|
||||||
|
const currentCompleted = Math.round(baseCompleted);
|
||||||
|
const currentFailed = Math.round(baseFailed);
|
||||||
|
|
||||||
|
completed.push({
|
||||||
|
value: currentCompleted,
|
||||||
|
prevValue: prevCompleted,
|
||||||
|
change: prevCompleted !== null ? ((currentCompleted - prevCompleted) / prevCompleted * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
failed.push({
|
||||||
|
value: currentFailed,
|
||||||
|
prevValue: prevFailed,
|
||||||
|
change: prevFailed !== null ? ((currentFailed - prevFailed) / prevFailed * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
prevCompleted = currentCompleted;
|
||||||
|
prevFailed = currentFailed;
|
||||||
|
}
|
||||||
|
} else if (granularity === 'year') {
|
||||||
|
const years = end.getFullYear() - start.getFullYear() + 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < years; i++) {
|
||||||
|
const year = start.getFullYear() + i;
|
||||||
|
labels.push(`${year}年`);
|
||||||
|
|
||||||
|
// 生成有波动趋势的数据
|
||||||
|
const baseCompleted = 108000 + Math.sin(i) * 18000 + Math.random() * 10800;
|
||||||
|
const baseFailed = 3600 + Math.abs(Math.sin(i / 2)) * 2880 + Math.random() * 1800;
|
||||||
|
|
||||||
|
const currentCompleted = Math.round(baseCompleted);
|
||||||
|
const currentFailed = Math.round(baseFailed);
|
||||||
|
|
||||||
|
completed.push({
|
||||||
|
value: currentCompleted,
|
||||||
|
prevValue: prevCompleted,
|
||||||
|
change: prevCompleted !== null ? ((currentCompleted - prevCompleted) / prevCompleted * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
failed.push({
|
||||||
|
value: currentFailed,
|
||||||
|
prevValue: prevFailed,
|
||||||
|
change: prevFailed !== null ? ((currentFailed - prevFailed) / prevFailed * 100).toFixed(1) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
prevCompleted = currentCompleted;
|
||||||
|
prevFailed = currentFailed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { labels, completed, inProgress, notStarted, failed };
|
return {
|
||||||
|
labels: labels,
|
||||||
|
completed: completed.map(c => c.value),
|
||||||
|
failed: failed.map(f => f.value),
|
||||||
|
completedDetails: completed,
|
||||||
|
failedDetails: failed
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化日期选择器
|
// 初始化日期选择器
|
||||||
@ -326,6 +344,7 @@
|
|||||||
if (selectedDates.length === 2) {
|
if (selectedDates.length === 2) {
|
||||||
const startDate = selectedDates[0];
|
const startDate = selectedDates[0];
|
||||||
const endDate = selectedDates[1];
|
const endDate = selectedDates[1];
|
||||||
|
const granularity = document.getElementById('timeGranularity').value;
|
||||||
|
|
||||||
// 更新显示
|
// 更新显示
|
||||||
const startStr = `${startDate.getFullYear()}年${startDate.getMonth() + 1}月${startDate.getDate()}日`;
|
const startStr = `${startDate.getFullYear()}年${startDate.getMonth() + 1}月${startDate.getDate()}日`;
|
||||||
@ -333,7 +352,7 @@
|
|||||||
document.getElementById('dateRangeDisplay').textContent = `${startStr} - ${endStr}`;
|
document.getElementById('dateRangeDisplay').textContent = `${startStr} - ${endStr}`;
|
||||||
|
|
||||||
// 生成新数据并更新图表
|
// 生成新数据并更新图表
|
||||||
const newData = generateTrendData(startDate, endDate);
|
const newData = generateTrendData(startDate, endDate, granularity);
|
||||||
renderTrendChart(newData);
|
renderTrendChart(newData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -341,7 +360,6 @@
|
|||||||
|
|
||||||
// 初始化图表
|
// 初始化图表
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// 1. 任务趋势折线图
|
|
||||||
const trendChart = echarts.init(document.getElementById('trendChart'));
|
const trendChart = echarts.init(document.getElementById('trendChart'));
|
||||||
let currentTrendData = generateTrendData(new Date(2023, 6, 1), new Date(2023, 6, 31));
|
let currentTrendData = generateTrendData(new Date(2023, 6, 1), new Date(2023, 6, 31));
|
||||||
|
|
||||||
@ -351,16 +369,38 @@
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
formatter: params => {
|
formatter: function (params) {
|
||||||
|
const index = params[0].dataIndex;
|
||||||
|
const completed = data.completedDetails[index];
|
||||||
|
const failed = data.failedDetails[index];
|
||||||
|
|
||||||
let result = `${params[0].axisValue}<br>`;
|
let result = `${params[0].axisValue}<br>`;
|
||||||
params.forEach(param => {
|
|
||||||
result += `${param.marker} ${param.seriesName}: <strong>${param.value}</strong><br>`;
|
// 任务完成提示
|
||||||
});
|
result += `${params[0].marker} 任务完成: <strong>${completed.value}</strong>`;
|
||||||
|
if (completed.change !== null) {
|
||||||
|
const change = parseFloat(completed.change);
|
||||||
|
const trend = change >= 0 ? '↑' : '↓';
|
||||||
|
const color = change >= 0 ? '#06D6A0' : '#FF4D6D';
|
||||||
|
result += ` (较前日 <span style="color:${color}">${trend}${Math.abs(change)}%</span>)<br>`;
|
||||||
|
} else {
|
||||||
|
result += '<br>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务失败提示
|
||||||
|
result += `${params[1].marker} 任务失败: <strong>${failed.value}</strong>`;
|
||||||
|
if (failed.change !== null) {
|
||||||
|
const change = parseFloat(failed.change);
|
||||||
|
const trend = change >= 0 ? '↑' : '↓';
|
||||||
|
const color = change >= 0 ? '#FF4D6D' : '#06D6A0';
|
||||||
|
result += ` (较前日 <span style="color:${color}">${trend}${Math.abs(change)}%</span>)`;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['已完成', '进行中', '未进行', '任务失败'],
|
data: ['任务完成', '任务失败'],
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
textStyle: { color: '#8E9AAF' }
|
textStyle: { color: '#8E9AAF' }
|
||||||
},
|
},
|
||||||
@ -377,7 +417,7 @@
|
|||||||
axisLine: { lineStyle: { color: '#e2e8f0' } },
|
axisLine: { lineStyle: { color: '#e2e8f0' } },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#8E9AAF',
|
color: '#8E9AAF',
|
||||||
interval: Math.max(1, Math.floor(data.labels.length / 7) - 1) // 动态间隔
|
interval: Math.max(1, Math.floor(data.labels.length / 7) - 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
@ -388,7 +428,7 @@
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '已完成',
|
name: '任务完成',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: data.completed,
|
data: data.completed,
|
||||||
lineStyle: { width: 3, color: '#06D6A0' },
|
lineStyle: { width: 3, color: '#06D6A0' },
|
||||||
@ -402,36 +442,6 @@
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '进行中',
|
|
||||||
type: 'line',
|
|
||||||
data: data.inProgress,
|
|
||||||
lineStyle: { width: 3, color: '#3A86FF' },
|
|
||||||
symbol: 'circle',
|
|
||||||
symbolSize: 8,
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: 'rgba(58, 134, 255, 0.3)' },
|
|
||||||
{ offset: 1, color: 'rgba(58, 134, 255, 0.05)' }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '未进行',
|
|
||||||
type: 'line',
|
|
||||||
data: data.notStarted,
|
|
||||||
lineStyle: { width: 3, color: '#8E9AAF' },
|
|
||||||
symbol: 'circle',
|
|
||||||
symbolSize: 8,
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: 'rgba(142, 154, 175, 0.3)' },
|
|
||||||
{ offset: 1, color: 'rgba(142, 154, 175, 0.05)' }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: '任务失败',
|
name: '任务失败',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@ -454,177 +464,22 @@
|
|||||||
|
|
||||||
renderTrendChart(currentTrendData);
|
renderTrendChart(currentTrendData);
|
||||||
|
|
||||||
// 2. 饼图 - 当天任务状态分布
|
// 时间粒度选择器事件监听
|
||||||
const pieChart = echarts.init(document.getElementById('pieChart'));
|
document.getElementById('timeGranularity').addEventListener('change', function (e) {
|
||||||
const pieOption = {
|
const selectedDates = datePicker.selectedDates;
|
||||||
tooltip: {
|
if (selectedDates.length === 2) {
|
||||||
trigger: 'item',
|
const startDate = selectedDates[0];
|
||||||
formatter: '{b}: {c} ({d}%)'
|
const endDate = selectedDates[1];
|
||||||
},
|
const granularity = e.target.value;
|
||||||
legend: {
|
|
||||||
orient: 'vertical',
|
const newData = generateTrendData(startDate, endDate, granularity);
|
||||||
right: 10,
|
renderTrendChart(newData);
|
||||||
top: 'center',
|
|
||||||
textStyle: { color: '#8E9AAF' }
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['40%', '50%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 8,
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
labelLine: { show: false },
|
|
||||||
data: [
|
|
||||||
{ value: 420, name: '已完成', itemStyle: { color: '#06D6A0' } },
|
|
||||||
{ value: 185, name: '进行中', itemStyle: { color: '#3A86FF' } },
|
|
||||||
{ value: 98, name: '未进行', itemStyle: { color: '#8E9AAF' } },
|
|
||||||
{ value: 37, name: '任务失败', itemStyle: { color: '#FF4D6D' } }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
pieChart.setOption(pieOption);
|
|
||||||
|
|
||||||
// 3. 堆叠柱状图
|
|
||||||
const stackedBarChart = echarts.init(document.getElementById('stackedBarChart'));
|
|
||||||
const timeLabels = Array.from({ length: 24 }, (_, i) => `${i}:00`);
|
|
||||||
|
|
||||||
// 模拟24小时不同状态的任务数据
|
|
||||||
const generateDayData = () => {
|
|
||||||
return {
|
|
||||||
completed: Array.from({ length: 24 }, (_, i) => {
|
|
||||||
// 白天任务多,晚上任务少的模式
|
|
||||||
const base = 30 + Math.sin(i / 24 * Math.PI) * 25;
|
|
||||||
return Math.round(base + Math.random() * 15);
|
|
||||||
}),
|
|
||||||
inProgress: Array.from({ length: 24 }, (_, i) => {
|
|
||||||
const base = 20 + Math.abs(Math.cos(i / 12 * Math.PI)) * 15;
|
|
||||||
return Math.round(base + Math.random() * 10);
|
|
||||||
}),
|
|
||||||
notStarted: Array.from({ length: 24 }, (_, i) => {
|
|
||||||
return Math.round(10 + Math.random() * 8);
|
|
||||||
}),
|
|
||||||
failed: Array.from({ length: 24 }, (_, i) => {
|
|
||||||
// 凌晨时段失败率略高
|
|
||||||
const nightFactor = i < 6 || i > 22 ? 1.5 : 1;
|
|
||||||
return Math.round((Math.random() * 5 + 2) * nightFactor);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 为一周的每一天生成数据
|
|
||||||
const weekData = Array.from({ length: 5 }, () => generateDayData());
|
|
||||||
|
|
||||||
const updateStackedBarChart = (dayIndex) => {
|
|
||||||
const dayData = weekData[dayIndex];
|
|
||||||
const stackedBarOption = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'shadow' },
|
|
||||||
formatter: params => {
|
|
||||||
let total = 0;
|
|
||||||
params.forEach(item => total += item.value);
|
|
||||||
return params.map(item =>
|
|
||||||
`${item.marker} ${item.seriesName}: ${item.value} (${Math.round(item.value / total * 100)}%)`
|
|
||||||
).join('<br>');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: ['已完成', '进行中', '未进行', '任务失败'],
|
|
||||||
bottom: 0,
|
|
||||||
textStyle: { color: '#8E9AAF' }
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '15%',
|
|
||||||
top: '5%',
|
|
||||||
containLabel: true
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: timeLabels,
|
|
||||||
axisLabel: {
|
|
||||||
interval: 1,
|
|
||||||
rotate: 45,
|
|
||||||
color: '#8E9AAF'
|
|
||||||
},
|
|
||||||
axisLine: { lineStyle: { color: '#e2e8f0' } }
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLine: { show: true, lineStyle: { color: '#e2e8f0' } },
|
|
||||||
splitLine: { lineStyle: { color: '#f1f3f5', type: 'dashed' } },
|
|
||||||
axisLabel: { color: '#8E9AAF' }
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '已完成',
|
|
||||||
type: 'bar',
|
|
||||||
stack: '总量',
|
|
||||||
emphasis: { focus: 'series' },
|
|
||||||
data: dayData.completed,
|
|
||||||
itemStyle: { color: '#06D6A0' },
|
|
||||||
barWidth: '60%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '进行中',
|
|
||||||
type: 'bar',
|
|
||||||
stack: '总量',
|
|
||||||
emphasis: { focus: 'series' },
|
|
||||||
data: dayData.inProgress,
|
|
||||||
itemStyle: { color: '#3A86FF' },
|
|
||||||
barWidth: '60%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '未进行',
|
|
||||||
type: 'bar',
|
|
||||||
stack: '总量',
|
|
||||||
emphasis: { focus: 'series' },
|
|
||||||
data: dayData.notStarted,
|
|
||||||
itemStyle: { color: '#8E9AAF' },
|
|
||||||
barWidth: '60%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '任务失败',
|
|
||||||
type: 'bar',
|
|
||||||
stack: '总量',
|
|
||||||
emphasis: { focus: 'series' },
|
|
||||||
data: dayData.failed,
|
|
||||||
itemStyle: { color: '#FF4D6D' },
|
|
||||||
barWidth: '60%'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
stackedBarChart.setOption(stackedBarOption);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化显示周一的数据
|
|
||||||
updateStackedBarChart(0);
|
|
||||||
|
|
||||||
// 添加日期选择器事件监听
|
|
||||||
document.getElementById('daySelector').addEventListener('change', function (e) {
|
|
||||||
updateStackedBarChart(parseInt(e.target.value));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 窗口大小变化时重绘图表
|
// 窗口大小变化时重绘图表
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
trendChart.resize();
|
trendChart.resize();
|
||||||
pieChart.resize();
|
|
||||||
stackedBarChart.resize();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user