统计模块中订单分析前后端对接
This commit is contained in:
parent
c2f18b9ac8
commit
51f722cab1
@ -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",
|
||||
|
@ -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>
|
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 './router/index' // permission control
|
||||
|
||||
|
||||
Vue.use(checkPer)
|
||||
Vue.use(permission)
|
||||
Vue.use(dict)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
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 {
|
||||
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>
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user