统计视图

This commit is contained in:
yyy 2025-05-21 15:59:27 +08:00
parent b123bb2131
commit 995839d70e
7 changed files with 612 additions and 419 deletions

View File

@ -4,8 +4,7 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
# VITE_BASE_URL='http://192.168.0.74:48080'
# VITE_BASE_URL='http://192.168.0.153:48080'
# VITE_BASE_URL='http://192.168.77.50:48080'
VITE_BASE_URL='http://10.10.100.17:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

21
src/api/map/statistics.ts Normal file
View File

@ -0,0 +1,21 @@
import request from '@/config/axios'
//统计车辆状态分类
export const robotStatusClassification = async (params) => {
return await request.get({ url: `/system/statistics/robotStatusClassification`, params })
}
//统计任务人工完成-自动完成
export const robotTaskAutomaticArtificial = async (params) => {
return await request.get({ url: `/system/statistics/robotTaskAutomaticArtificial`, params })
}
//统计故障根因分析
export const robotWarnMsgClassification = async (params) => {
return await request.get({ url: `/system/statistics/robotWarnMsgClassification`, params })
}
//车辆工作时长统计
export const robotWorkHourStatistics = async (params) => {
return await request.get({ url: `/system/statistics/robotWorkHourStatistics`, params })
}

View File

@ -0,0 +1,19 @@
@font-face {
font-family: "fc-icon";
src: url("@/styles/FormCreate/fonts/fontello.woff") format("woff");
}
.icon-doc-text:before {
content: "\f0f6";
}
.icon-server:before {
content: "\f233";
}
.icon-address-card-o:before {
content: "\f2bc";
}
.icon-user-o:before {
content: "\f2c0";
}/*# sourceMappingURL=index.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sources":["index.scss","index.css"],"names":[],"mappings":"AAEA;EACE,sBAAA;EACA,kEAAA;ACDF;ADIA;EACE,gBAAA;ACFF;;ADKA;EACE,gBAAA;ACFF;;ADKA;EACE,gBAAA;ACFF;;ADKA;EACE,gBAAA;ACFF","file":"index.css"}

View File

@ -66,6 +66,15 @@
class="current-item"
:class="currentItem && currentItem.id == floor.id ? 'tool-active' : ''"
@click="chooseLocationPoint(floor)"
:style="{
background:
floor.locationEnable === 0 ||
floor.locationLock === 0 ||
(floor.locationUseStatus === 1 && locationTypeStr === 'release') ||
(floor.locationUseStatus === 0 && locationTypeStr === 'take')
? '#FFE2E2'
: '#F6FFEF'
}"
>
<div>层数: {{ floor.locationStorey }}</div>
<div class="mt-4px">库位号: {{ floor.locationNo }}</div>

View File

@ -0,0 +1,424 @@
<template>
<div class="chart-card">
<div class="chart-header">
<div class="chart-header-title">
<h3>{{ title }}</h3>
<div v-if="chartType === 'bar'" class="ml-2">
<el-cascader
v-model="selectedOptions"
:options="cascaderOptions"
:props="cascaderProps"
@change="handleChange"
placeholder="请选择区域"
class="!w-140px"
/>
</div>
</div>
<div class="dimension-switcher" v-if="chartType !== 'pie1'">
<button
v-for="item in dimensionOptions"
:key="item.value"
:class="{ active: dimension === item.value }"
@click="handleDimensionChange(item.value)"
>
{{ item.label }}
</button>
</div>
</div>
<div ref="chartRef" class="chart-container"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { ElLoading } from 'element-plus'
import * as echarts from 'echarts'
import * as MapApi from '@/api/map/map'
const props = defineProps({
title: String,
dimension: Number,
fetchData: Function,
chartType: {
type: String,
default: 'pie',
validator: (value) => ['pie1', 'bar', 'stackedBar', 'pie2'].includes(value)
}
})
const emit = defineEmits(['dimension-change'])
const chartRef = ref(null)
let chartInstance = null
const dimensionOptions = [
{ label: '周', value: 1 },
{ label: '月', value: 2 },
{ label: '季度', value: 3 }
]
//
const handleDimensionChange = (dimension) => {
emit('dimension-change', dimension)
}
//
const initChart = () => {
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(chartRef.value)
}
//
const updateChart = async () => {
if (!chartInstance) return
const loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.6)'
})
const data = await props.fetchData(props.dimension)
let option = {}
if (props.chartType === 'pie1') {
option = getPieOption1(data)
} else if (props.chartType === 'bar') {
option = getBarOption(data)
} else if (props.chartType === 'stackedBar') {
option = getStackedBarOption(data)
} else if (props.chartType === 'pie2') {
option = getPieOption2(data)
}
chartInstance.setOption(option)
loading.close()
}
// 1
const getPieOption1 = (data) => {
return {
tooltip: {
trigger: 'item',
formatter: '{b} : {c} ({d}%)'
},
toolbox: {
show: true
},
color: ['#5C7BD9', '#EE6666', '#91CC75', '#FAC858'],
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 70,
bottom: 20
},
series: [
{
type: 'pie',
clockwise: false, //
minAngle: 2, //0 ~ 360
radius: ['50%', '65%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
//
normal: {
borderColor: '#ffffff',
borderWidth: 6
}
},
label: {
normal: {
show: true,
position: 'center',
formatter: '{a|任务总数}{c|23412}',
rich: {
c: {
align: 'center',
color: '#0D162A',
fontSize: 15,
fontWeight: 'bold',
height: 30
},
a: {
align: 'center',
color: '#0D162A',
fontSize: 14
}
}
}
},
labelLine: {
length: 15,
length2: 0,
maxSurfaceAngle: 80
},
data: data
}
]
}
}
//
const getBarOption = (data) => {
return {
tooltip: {
trigger: 'item',
formatter: '{b} : {c}'
},
color: ['#2268FF', '#34bf49', '#ffdd00'],
toolbox: {},
legend: {
top: 14
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.categories
},
yAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
series: data.series
}
}
//
const getStackedBarOption = (data) => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} : {c}'
},
color: ['#FF7D7D', '#8a8acb', '#FAC858', '#91CC75'],
toolbox: {
show: true
},
legend: {
top: 14,
data: data.series.map((item) => item.name)
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.categories
// axisLabel: {
// interval: 0,
// rotate: 30
// }
},
yAxis: {
type: 'value',
name: '告警数量',
nameTextStyle: {
fontSize: 14, //
padding: [0, 0, 10, 0] //
}
},
series: data.series.map((item) => ({
...item,
type: 'bar',
stack: 'total',
label: {
// show: true
},
emphasis: {
focus: 'series'
}
}))
}
}
// 2
const getPieOption2 = (data) => {
return {
tooltip: {
trigger: 'item',
formatter: '{b} : {c} ({d}%)'
},
color: ['#fc636b', '#6a67ce'],
toolbox: {
show: true
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 70,
bottom: 20
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['50%', '65%'],
center: ['50%', '60%'],
avoidLabelOverlap: false,
itemStyle: {
//
normal: {
borderColor: '#ffffff',
borderWidth: 6
}
},
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data: data
}
]
}
}
//
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
//
const selectedOptions = ref([])
const selectedId = ref(null)
const cascaderOptions = ref([])
//
const cascaderProps = {
value: 'id',
label: 'name',
children: 'children'
}
const getMapList = async () => {
let res = await MapApi.getPositionMapGetMap()
cascaderOptions.value = Object.entries(res).map(([floor, areas]) => {
return {
id: `floor_${floor}`,
name: `${floor}`,
children: areas.map((area) => ({
id: area.id,
name: `${area.area}`,
areaInfo: area
}))
}
})
//
if (cascaderOptions.value.length > 0 && cascaderOptions.value[0].children.length > 0) {
selectedOptions.value = [cascaderOptions.value[0].id, cascaderOptions.value[0].children[0].id]
selectedId.value = cascaderOptions.value[0].children[0].id
emit('mapId-change', selectedId.value)
}
}
//
const handleChange = (value) => {
// ID
selectedId.value = value[value.length - 1]
emit('mapId-change', selectedId.value)
}
//
watch(
() => props.dimension,
() => {
updateChart()
}
)
watch(
() => selectedId.value,
() => {
updateChart()
}
)
onMounted(async () => {
initChart()
await getMapList()
await updateChart()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped lang="scss">
.chart-card {
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #fff;
padding: 20px;
display: flex;
flex-direction: column;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.chart-header-title {
display: flex;
align-items: center;
}
}
.chart-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.dimension-switcher {
display: flex;
align-items: center;
gap: 8px;
}
.dimension-switcher button {
padding: 4px 14px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.dimension-switcher button.active {
background: #00329f;
color: white;
border-color: #00329f;
}
.chart-container {
width: 100%;
flex: 1;
min-height: 400px;
}
</style>

View File

@ -1,462 +1,182 @@
el-col
<template>
<el-card shadow="never">
<div class="top-box">
<div class="top-box-left"> 统计视图 </div>
<div class="top-box-right">
<!-- <div class="top-box-right">
<div class="top-box-right-title"> 统计方式 </div>
<el-select v-model="type" placeholder="请选择" style="width: 100px; margin-left: 10px">
<el-option :label="'天'" :value="1" />
<el-option :label="'周'" :value="2" />
<el-option :label="'月'" :value="3" />
</el-select>
<el-button style="margin-left: 16px" @click="openForm()">看板管理</el-button>
</div>
</div> -->
</div>
</el-card>
<div class="mt-4">
<div class="dashboard-container">
<el-row :gutter="16">
<!-- 状态分类饼图 -->
<el-col :span="12">
<el-card style="width: 100%" shadow="never">
<div class="charts-title"> 任务总览 </div>
<div ref="chartDom" style="width: 100%; height: 400px"></div>
</el-card>
<chart-card
title="状态分类"
:dimension="dimension1"
:fetch-data="fetchStatusData"
chart-type="pie1"
@dimension-change="(val) => (dimension1 = val)"
/>
</el-col>
<!-- 任务总览条形图 -->
<el-col :span="12">
<el-card style="width: 100%" shadow="never">
<div class="charts-title"> 任务完成率 </div>
<div ref="chartDomFinish" style="width: 100%; height: 400px"></div>
</el-card>
<chart-card
title="任务总览"
:dimension="dimension2"
:fetch-data="fetchTaskOverviewData"
chart-type="bar"
@dimension-change="(val) => (dimension2 = val)"
@mapId-change="mapIdChange"
/>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-top: 12px">
<el-row :gutter="16" class="mt-4">
<!-- 故障类目分析堆叠条形图 -->
<el-col :span="12">
<el-card style="width: 100%" shadow="never">
<div class="charts-title"> AGV工作利用率统计 </div>
<div ref="chartDomAgv" style="width: 100%; height: 400px"></div>
</el-card>
<chart-card
title="故障类目分析"
:dimension="dimension3"
:fetch-data="fetchFaultAnalysisData"
chart-type="stackedBar"
@dimension-change="(val) => (dimension3 = val)"
/>
</el-col>
<!-- 人工干预任务饼图 -->
<el-col :span="12">
<el-card style="width: 100%" shadow="never">
<div class="charts-title"> 任务异常数 </div>
<div ref="chartDomError" style="width: 100%; height: 400px"></div>
</el-card>
<chart-card
title="人工干预任务"
:dimension="dimension4"
:fetch-data="fetchManualInterventionData"
chart-type="pie2"
@dimension-change="(val) => (dimension4 = val)"
/>
</el-col>
</el-row>
</div>
<BoarViewDialog ref="boarViewDialogRef" />
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, reactive } from 'vue'
import * as echarts from 'echarts'
import { ref } from 'vue'
import ChartCard from './components/ChartCard.vue'
import BoarViewDialog from './boarViewDialog.vue'
import * as StatisticsAPi from '@/api/map/statistics'
// DOM
const chartDom = ref(null)
const chartDomFinish = ref(null)
const chartDomError = ref(null)
const chartDomAgv = ref(null)
const boarViewDialogRef = ref(null)
// ECharts线
onMounted(async () => {
await nextTick() // DOM
initEcharts()
})
//
const dimension1 = ref(1) //
const dimension2 = ref(1) //
const dimension3 = ref(1) //
const dimension4 = ref(1) //
const selectedMapId = ref()
const initEcharts = () => {
initOne()
initTwo()
initFour()
// 1.
const fetchStatusData = async () => {
const res = await StatisticsAPi.robotStatusClassification()
const chartData = [
{ value: res.chargeNum, name: '充电车辆' },
{ value: res.faultNum, name: '故障车辆' },
{ value: res.doingTaskNum, name: '执行任务车辆' },
{ value: res.idleNum, name: '空闲车辆' }
]
return chartData
}
const chartInstance = ref(null)
const initOne = () => {
chartInstance.value = echarts.init(chartDom.value)
let ydata = [
{
name: '执行中',
value: 18
},
{
name: '已完成',
value: 16
},
{
name: '已取消',
value: 15
},
{
name: '未开始',
value: 14
},
{
name: '异常',
value: 10
// 2.
const fetchTaskOverviewData = async (dimension) => {
const res = await StatisticsAPi.robotWorkHourStatistics({
type: dimension,
positionMapId: selectedMapId.value
})
if (!res) {
return {
categories: [],
series: []
}
} else {
const xAxisData = res.map((item) => {
return item.robotNo
})
const freeTimeNum = res.map((item) => {
return item.freeTimeNum
})
const taskTimeNum = res.map((item) => {
return item.taskTimeNum
})
const chargeTimeNum = res.map((item) => {
return item.chargeTimeNum
})
const chartData = {
categories: xAxisData,
series: [
{ name: '空闲时长', type: 'bar', data: freeTimeNum, barWidth: '20%', barGap: '25%' },
{ name: '任务时长', type: 'bar', data: taskTimeNum, barWidth: '20%', barGap: '25%' },
{ name: '充电时长', type: 'bar', data: chargeTimeNum, barWidth: '20%', barGap: '25%' }
]
let color = ['#0147EB', '#01BCEB', '#C800FF', '#F1CD0B', '#EB0000']
let xdata = ['执行中', '已完成', '已取消', '未开始', '异常']
const option = {
backgroundColor: 'rgba(255,255,255,1)',
color: color,
// tooltip: {
// trigger: 'item',
// // formatter: '{a} <br/>{b} : {c} ({d}%)'
// },
legend: {
orient: 'vartical',
x: 'left',
top: 'center',
left: '60%',
bottom: '0%',
data: xdata,
itemWidth: 10,
itemHeight: 10,
itemGap: 16,
formatter: function (name) {
let str = ''
ydata.forEach((item) => {
if (item.name == name) {
str = `{c|${item.name}} {a|${item.value}}`
}
return chartData
}
}
// 3.
const fetchFaultAnalysisData = async (dimension) => {
let res = await StatisticsAPi.robotWarnMsgClassification({
type: dimension
})
const dates = Object.keys(res).sort((a, b) => new Date(a) - new Date(b))
const warnLevels = ['1', '2', '3', '4']
const series = warnLevels.map((level) => ({
name: `级别${level}`,
data: new Array(dates.length).fill(0)
}))
dates.forEach((date, dateIndex) => {
const warnings = res[date]
warnings.forEach((warning) => {
const levelIndex = warnLevels.indexOf(warning.warnLevel)
if (levelIndex !== -1) {
series[levelIndex].data[dateIndex] += warning.num
}
})
return str
},
textStyle: {
rich: {
a: {
color: '#0D162A',
fontSize: 18
},
c: {
color: '#536387',
fontSize: 12
}
}
}
},
series: [
{
type: 'pie',
clockwise: false, //
minAngle: 2, //0 ~ 360
radius: ['50%', '65%'],
center: ['30%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
//
normal: {
borderColor: '#ffffff',
borderWidth: 6
}
},
label: {
// '{text|{b}}\n{c} ({d}%)'
normal: {
show: true,
position: 'center',
formatter: '{c|234,12} \n {a|任务总数}',
rich: {
c: {
color: '#0D162A',
fontSize: 15,
fontWeight: 'bold',
height: 30
},
})
a: {
align: 'center',
color: '#727272',
fontSize: 12
}
}
}
},
labelLine: {
length: 15,
length2: 0,
maxSurfaceAngle: 80
},
data: ydata
}
]
}
chartInstance.value.setOption(option)
return { categories: dates, series }
}
const chartInstanceTwo = ref(null)
const initTwo = () => {
chartInstanceTwo.value = echarts.init(chartDomFinish.value)
let option = {
backgroundColor: '#fff',
grid: {
top: '9%',
bottom: '19%',
left: '6%',
right: '4%'
},
tooltip: {
trigger: 'axis',
label: {
show: true
}
},
xAxis: {
boundaryGap: true, //
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false,
alignWithLabel: true
},
data: [
'武汉',
'襄阳',
'黄冈',
'荆门',
'十堰',
'随州',
'鄂州',
'恩施',
'宜昌',
'孝感',
'咸宁',
'仙桃',
'潜江',
'天门',
'黄石',
'荆州',
'神农架'
// 4.
const fetchManualInterventionData = async (dimension) => {
const res = await StatisticsAPi.robotTaskAutomaticArtificial({
type: dimension
})
const chartData = [
{ value: res.artificialDoneNum, name: '人工' },
{ value: res.automaticDoneNum, name: '自动' }
]
},
yAxis: {
axisLine: {
show: false
},
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#E2E7F5'
}
},
axisTick: {
show: false
},
splitArea: {
show: true,
areaStyle: {
color: 'rgb(245,250,254)'
}
}
},
series: [
{
type: 'line',
symbol: 'circle',
symbolSize: 1,
lineStyle: {
color: '#0147EB'
},
label: {
show: false,
distance: 1,
emphasis: {
show: true,
offset: [25, -2],
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#FFF',
padding: [8, 20, 8, 6],
//width:60,
height: 36,
formatter: function (params) {
var name = params.name
var value = params.data
var str = name + '\n数据量' + value
return str
},
rich: {
bg: {
width: 78,
//height:42,
color: '#FFF',
padding: [20, 0, 20, 10]
},
br: {
width: '100%',
height: '100%'
}
}
}
},
data: [
2000, 1800, 2800, 900, 1600, 2000, 3000, 2030, 1356, 1900, 4000, 3000, 2000, 3000, 4200,
3200, 3800
]
}
]
}
chartInstanceTwo.value.setOption(option)
return chartData
}
const chartInstanceFour = ref(null)
const initFour = () => {
chartInstanceFour.value = echarts.init(chartDomError.value)
let option = {
backgroundColor: '#fff',
grid: {
top: '9%',
bottom: '19%',
left: '6%',
right: '4%'
},
tooltip: {
trigger: 'axis',
label: {
show: true
}
},
xAxis: {
boundaryGap: true, //
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false,
alignWithLabel: true
},
data: [
'武汉',
'襄阳',
'黄冈',
'荆门',
'十堰',
'随州',
'鄂州',
'恩施',
'宜昌',
'孝感',
'咸宁',
'仙桃',
'潜江',
'天门',
'黄石',
'荆州',
'神农架'
]
},
yAxis: {
axisLine: {
show: false
},
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#E2E7F5'
}
},
axisTick: {
show: false
},
splitArea: {
show: true,
areaStyle: {
color: 'rgb(245,250,254)'
}
}
},
series: [
{
type: 'line',
symbol: 'circle',
symbolSize: 1,
lineStyle: {
color: '#0147EB'
},
label: {
show: false,
distance: 1,
emphasis: {
show: true,
offset: [25, -2],
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#FFF',
padding: [8, 20, 8, 6],
//width:60,
height: 36,
formatter: function (params) {
var name = params.name
var value = params.data
var str = name + '\n数据量' + value
return str
},
rich: {
bg: {
width: 78,
//height:42,
color: '#FFF',
padding: [20, 0, 20, 10]
},
br: {
width: '100%',
height: '100%'
}
}
}
},
data: [
2000, 1800, 2800, 900, 1600, 2000, 3000, 2030, 1356, 1900, 4000, 3000, 2000, 3000, 4200,
3200, 3800
]
}
]
}
chartInstanceFour.value.setOption(option)
const mapIdChange = (e) => {
selectedMapId.value = e
}
// ECharts
onUnmounted(() => {
if (chartInstance.value != null && chartInstance.value.dispose) {
chartInstance.value.dispose()
}
})
const openForm = () => {
boarViewDialogRef.value.open()
}
const type = ref(1)
</script>
<style lang="scss" scoped>
.top-box {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
<style scoped>
.dashboard-container {
margin-top: 14px;
}
.top-box-left {
color: #0d162a;
font-size: 18px;
}
.top-box-right {
display: flex;
align-items: center;
}
.top-box-right-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 14px;
color: #0d162a;
@media (max-width: 1200px) {
.dashboard-container {
grid-template-columns: 1fr;
}
}
</style>