统计模块中订单分析前后端对接

This commit is contained in:
yis 2025-07-18 21:32:17 +08:00
parent c2f18b9ac8
commit 51f722cab1
10 changed files with 729 additions and 321 deletions

View File

@ -11,7 +11,8 @@
"preview": "node build/index.js --preview",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"svgo": "svgo -f src/assets/icons/svg --config=src/assets/icons/svgo.yml",
"new": "plop"
"new": "plop",
"serve": "vue-cli-service serve"
},
"husky": {
"hooks": {
@ -38,7 +39,7 @@
"clipboard": "2.0.4",
"codemirror": "^5.49.2",
"core-js": "^2.6.12",
"echarts": "^4.2.1",
"echarts": "^4.9.0",
"echarts-wordcloud": "^1.1.3",
"element-ui": "^2.15.14",
"file-saver": "1.3.8",
@ -54,6 +55,7 @@
"qs": "^6.10.1",
"screenfull": "4.2.0",
"sortablejs": "1.8.4",
"tailwindcss": "^4.1.11",
"vue": "2.7.16",
"vue-count-to": "^1.0.13",
"vue-cropper": "0.4.9",

View File

@ -1,15 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= webpackConfig.name %>
</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View 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
})
}

View 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,
})
}

View File

@ -24,6 +24,7 @@ import router from './router/routers'
import './assets/icons' // icon
import './router/index' // permission control
Vue.use(checkPer)
Vue.use(permission)
Vue.use(dict)

View File

@ -5,7 +5,8 @@ import Layout from '../layout/index'
Vue.use(Router)
export const constantRouterMap = [
{ path: '/login',
{
path: '/login',
meta: { title: '登录', noCache: true },
component: (resolve) => require(['@/views/login'], resolve),
hidden: true

View File

@ -36,7 +36,7 @@ service.interceptors.response.use(
if (error.response.data instanceof Blob && error.response.data.type.toLowerCase().indexOf('json') !== -1) {
const reader = new FileReader()
reader.readAsText(error.response.data, 'utf-8')
reader.onload = function(e) {
reader.onload = function (e) {
const errorMsg = JSON.parse(reader.result).message
Notification.error({
title: errorMsg,

View 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>

View File

@ -91,9 +91,9 @@
}
.kpi-value {
font-size: 32px;
font-size: 40px;
font-weight: 700;
margin: 12px 0 8px;
margin: 20px 10px 8px 25px;
}
.kpi-card:nth-child(1) .kpi-value {
@ -112,21 +112,6 @@
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 {
border-radius: var(--border-radius);
background: var(--light-gray);
@ -145,13 +130,6 @@
align-items: center;
}
.dual-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 0 30px 40px;
}
.date-range-picker {
display: flex;
align-items: center;
@ -164,20 +142,20 @@
cursor: pointer;
}
.day-selector {
.time-granularity {
border: 1px solid #e2e8f0;
border-radius: var(--border-radius);
padding: 8px 12px;
font-size: 14px;
color: var(--dark-gray);
background: white;
margin-left: 10px;
}
.chart-holder {
height: 350px;
}
/* Flatpickr 样式覆盖 */
.flatpickr-calendar {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
box-shadow: var(--shadow);
@ -196,47 +174,30 @@
<!-- 顶部KPI区域 -->
<div class="kpi-grid">
<div class="kpi-card">
<div>实时任务完成率</div>
<div>任务完成率</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"
style="position: absolute; right: 38px; top: 38px; color: var(--success-green); font-size: 24px;"></iconify-icon>
</div>
<div class="kpi-card">
<div>关键任务失败率</div>
<div>任务失败率</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"
style="position: absolute; right: 38px; top: 38px; color: var(--alert-red); font-size: 24px;"></iconify-icon>
</div>
<div class="kpi-card">
<div>平均任务周期</div>
<div class="kpi-value">4.8h</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>
<div class="kpi-value">30min</div>
<iconify-icon icon="mdi:timer-outline"
style="position: absolute; right: 38px; top: 38px; color: var(--primary-blue); font-size: 24px;"></iconify-icon>
</div>
<div class="kpi-card">
<div>资源闲置率</div>
<div class="kpi-value">12.3%</div>
<div class="trend-change trend-down">
<iconify-icon icon="mingcute:arrow-down-line" style="margin-right: 4px;"></iconify-icon>
周同比 ↓3.5%
</div>
<iconify-icon icon="mdi:server-off"
<div>今日无人机使用率</div>
<div class="kpi-value">87.7%</div>
<iconify-icon icon="mdi:drone"
style="position: absolute; right: 38px; top: 38px; color: var(--neutral-gray); font-size: 24px;"></iconify-icon>
</div>
</div>
@ -245,71 +206,128 @@
<div class="chart-container">
<div class="chart-title">
<span>任务完成趋势分析</span>
<div class="date-range-picker" id="dateRangePicker">
<iconify-icon icon="material-symbols:calendar-month"
style="color: var(--neutral-gray); margin-right: 8px;"></iconify-icon>
<span id="dateRangeDisplay">2023年7月1日 - 2023年7月31日</span>
<div>
<div class="date-range-picker" id="dateRangePicker">
<iconify-icon icon="material-symbols:calendar-month"
style="color: var(--neutral-gray); margin-right: 8px;"></iconify-icon>
<span id="dateRangeDisplay">2023年7月1日 - 2023年7月31日</span>
</div>
</div>
</div>
<div id="trendChart" class="chart-holder"></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>
<script>
// 模拟数据生成器
const generateTrendData = (startDate, endDate) => {
const generateTrendData = (startDate, endDate, granularity = 'day') => {
const start = new Date(startDate);
const end = new Date(endDate);
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
const labels = [];
const completed = [];
const inProgress = [];
const notStarted = [];
const failed = [];
let labels = [];
let completed = [];
let failed = [];
let prevCompleted = null;
let prevFailed = null;
for (let i = 0; i < days; i++) {
const date = new Date(start);
date.setDate(start.getDate() + i);
labels.push(`${date.getMonth() + 1}/${date.getDate()}`);
if (granularity === 'day') {
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
// 生成有波动趋势的数据
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;
for (let i = 0; i < days; i++) {
const date = new Date(start);
date.setDate(start.getDate() + i);
labels.push(`${date.getMonth() + 1}/${date.getDate()}`);
completed.push(Math.round(baseCompleted));
inProgress.push(Math.round(baseInProgress));
notStarted.push(Math.round(baseNotStarted));
failed.push(Math.round(baseFailed));
// 生成有波动趋势的数据
const baseCompleted = 300 + Math.sin(i / 3) * 50 + Math.random() * 30;
const baseFailed = 10 + Math.abs(Math.sin(i / 5)) * 8 + Math.random() * 5;
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 === '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) {
const startDate = selectedDates[0];
const endDate = selectedDates[1];
const granularity = document.getElementById('timeGranularity').value;
// 更新显示
const startStr = `${startDate.getFullYear()}年${startDate.getMonth() + 1}月${startDate.getDate()}日`;
@ -333,7 +352,7 @@
document.getElementById('dateRangeDisplay').textContent = `${startStr} - ${endStr}`;
// 生成新数据并更新图表
const newData = generateTrendData(startDate, endDate);
const newData = generateTrendData(startDate, endDate, granularity);
renderTrendChart(newData);
}
}
@ -341,7 +360,6 @@
// 初始化图表
document.addEventListener('DOMContentLoaded', function () {
// 1. 任务趋势折线图
const trendChart = echarts.init(document.getElementById('trendChart'));
let currentTrendData = generateTrendData(new Date(2023, 6, 1), new Date(2023, 6, 31));
@ -351,16 +369,38 @@
tooltip: {
trigger: 'axis',
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>`;
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;
}
},
legend: {
data: ['已完成', '进行中', '未进行', '任务失败'],
data: ['任务完成', '任务失败'],
bottom: 0,
textStyle: { color: '#8E9AAF' }
},
@ -377,7 +417,7 @@
axisLine: { lineStyle: { color: '#e2e8f0' } },
axisLabel: {
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: {
@ -388,7 +428,7 @@
},
series: [
{
name: '完成',
name: '任务完成',
type: 'line',
data: data.completed,
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: '任务失败',
type: 'line',
@ -454,177 +464,22 @@
renderTrendChart(currentTrendData);
// 2. 饼图 - 当天任务状态分布
const pieChart = echarts.init(document.getElementById('pieChart'));
const pieOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
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);
// 时间粒度选择器事件监听
document.getElementById('timeGranularity').addEventListener('change', function (e) {
const selectedDates = datePicker.selectedDates;
if (selectedDates.length === 2) {
const startDate = selectedDates[0];
const endDate = selectedDates[1];
const granularity = e.target.value;
// 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));
const newData = generateTrendData(startDate, endDate, granularity);
renderTrendChart(newData);
}
});
// 窗口大小变化时重绘图表
window.addEventListener('resize', function () {
trendChart.resize();
pieChart.resize();
stackedBarChart.resize();
});
});
</script>

View File

@ -81,7 +81,7 @@ module.exports = {
bundler: 'webpack',
})
);
// set svg-sprite-loader
config.module
.rule('svg')
@ -123,7 +123,7 @@ module.exports = {
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/
}])
.end()