统计视图

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_DEV=true
# 请求路径 # 请求路径
# VITE_BASE_URL='http://192.168.0.74:48080' # VITE_BASE_URL='http://192.168.77.50:48080'
# VITE_BASE_URL='http://192.168.0.153:48080'
VITE_BASE_URL='http://10.10.100.17:48080' VITE_BASE_URL='http://10.10.100.17:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务 # 文件上传类型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="current-item"
:class="currentItem && currentItem.id == floor.id ? 'tool-active' : ''" :class="currentItem && currentItem.id == floor.id ? 'tool-active' : ''"
@click="chooseLocationPoint(floor)" @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>层数: {{ floor.locationStorey }}</div>
<div class="mt-4px">库位号: {{ floor.locationNo }}</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> <template>
<el-card shadow="never"> <el-card shadow="never">
<div class="top-box"> <div class="top-box">
<div class="top-box-left"> 统计视图 </div> <div class="top-box-left"> 统计视图 </div>
<div class="top-box-right"> <!-- <div class="top-box-right">
<div class="top-box-right-title"> 统计方式 </div> <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> <el-button style="margin-left: 16px" @click="openForm()">看板管理</el-button>
</div> </div> -->
</div> </div>
</el-card> </el-card>
<div class="mt-4"> <div class="dashboard-container">
<el-row :gutter="16"> <el-row :gutter="16">
<!-- 状态分类饼图 -->
<el-col :span="12"> <el-col :span="12">
<el-card style="width: 100%" shadow="never"> <chart-card
<div class="charts-title"> 任务总览 </div> title="状态分类"
<div ref="chartDom" style="width: 100%; height: 400px"></div> :dimension="dimension1"
</el-card> :fetch-data="fetchStatusData"
chart-type="pie1"
@dimension-change="(val) => (dimension1 = val)"
/>
</el-col> </el-col>
<!-- 任务总览条形图 -->
<el-col :span="12"> <el-col :span="12">
<el-card style="width: 100%" shadow="never"> <chart-card
<div class="charts-title"> 任务完成率 </div> title="任务总览"
<div ref="chartDomFinish" style="width: 100%; height: 400px"></div> :dimension="dimension2"
</el-card> :fetch-data="fetchTaskOverviewData"
chart-type="bar"
@dimension-change="(val) => (dimension2 = val)"
@mapId-change="mapIdChange"
/>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="16" style="margin-top: 12px"> <el-row :gutter="16" class="mt-4">
<!-- 故障类目分析堆叠条形图 -->
<el-col :span="12"> <el-col :span="12">
<el-card style="width: 100%" shadow="never"> <chart-card
<div class="charts-title"> AGV工作利用率统计 </div> title="故障类目分析"
<div ref="chartDomAgv" style="width: 100%; height: 400px"></div> :dimension="dimension3"
</el-card> :fetch-data="fetchFaultAnalysisData"
chart-type="stackedBar"
@dimension-change="(val) => (dimension3 = val)"
/>
</el-col> </el-col>
<!-- 人工干预任务饼图 -->
<el-col :span="12"> <el-col :span="12">
<el-card style="width: 100%" shadow="never"> <chart-card
<div class="charts-title"> 任务异常数 </div> title="人工干预任务"
<div ref="chartDomError" style="width: 100%; height: 400px"></div> :dimension="dimension4"
</el-card> :fetch-data="fetchManualInterventionData"
chart-type="pie2"
@dimension-change="(val) => (dimension4 = val)"
/>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<BoarViewDialog ref="boarViewDialogRef" />
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, nextTick, reactive } from 'vue' import { ref } from 'vue'
import * as echarts from 'echarts' import ChartCard from './components/ChartCard.vue'
import BoarViewDialog from './boarViewDialog.vue' import BoarViewDialog from './boarViewDialog.vue'
import * as StatisticsAPi from '@/api/map/statistics'
// DOM //
const chartDom = ref(null) const dimension1 = ref(1) //
const chartDomFinish = ref(null) const dimension2 = ref(1) //
const chartDomError = ref(null) const dimension3 = ref(1) //
const chartDomAgv = ref(null) const dimension4 = ref(1) //
const boarViewDialogRef = ref(null) const selectedMapId = ref()
// ECharts线
onMounted(async () => {
await nextTick() // DOM
initEcharts()
})
const initEcharts = () => { // 1.
initOne() const fetchStatusData = async () => {
initTwo() const res = await StatisticsAPi.robotStatusClassification()
initFour() const chartData = [
} { value: res.chargeNum, name: '充电车辆' },
{ value: res.faultNum, name: '故障车辆' },
const chartInstance = ref(null) { value: res.doingTaskNum, name: '执行任务车辆' },
const initOne = () => { { value: res.idleNum, name: '空闲车辆' }
chartInstance.value = echarts.init(chartDom.value)
let ydata = [
{
name: '执行中',
value: 18
},
{
name: '已完成',
value: 16
},
{
name: '已取消',
value: 15
},
{
name: '未开始',
value: 14
},
{
name: '异常',
value: 10
}
] ]
let color = ['#0147EB', '#01BCEB', '#C800FF', '#F1CD0B', '#EB0000'] return chartData
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 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)
} }
const chartInstanceTwo = ref(null) // 2.
const initTwo = () => { const fetchTaskOverviewData = async (dimension) => {
chartInstanceTwo.value = echarts.init(chartDomFinish.value) const res = await StatisticsAPi.robotWorkHourStatistics({
let option = { type: dimension,
backgroundColor: '#fff', positionMapId: selectedMapId.value
grid: { })
top: '9%',
bottom: '19%', if (!res) {
left: '6%', return {
right: '4%' categories: [],
}, series: []
tooltip: { }
trigger: 'axis', } else {
label: { const xAxisData = res.map((item) => {
show: true return item.robotNo
} })
}, const freeTimeNum = res.map((item) => {
xAxis: { return item.freeTimeNum
boundaryGap: true, // })
axisLine: { const taskTimeNum = res.map((item) => {
show: false return item.taskTimeNum
}, })
splitLine: { const chargeTimeNum = res.map((item) => {
show: false return item.chargeTimeNum
}, })
axisTick: {
show: false, const chartData = {
alignWithLabel: true categories: xAxisData,
}, series: [
data: [ { 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%' }
'黄冈',
'荆门',
'十堰',
'随州',
'鄂州',
'恩施',
'宜昌',
'孝感',
'咸宁',
'仙桃',
'潜江',
'天门',
'黄石',
'荆州',
'神农架'
] ]
}, }
yAxis: { return chartData
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)
} }
const chartInstanceFour = ref(null) // 3.
const initFour = () => { const fetchFaultAnalysisData = async (dimension) => {
chartInstanceFour.value = echarts.init(chartDomError.value) let res = await StatisticsAPi.robotWarnMsgClassification({
let option = { type: dimension
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: { const dates = Object.keys(res).sort((a, b) => new Date(a) - new Date(b))
show: false, const warnLevels = ['1', '2', '3', '4']
distance: 1, const series = warnLevels.map((level) => ({
emphasis: { name: `级别${level}`,
show: true, data: new Array(dates.length).fill(0)
offset: [25, -2], }))
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#FFF', dates.forEach((date, dateIndex) => {
padding: [8, 20, 8, 6], const warnings = res[date]
//width:60, warnings.forEach((warning) => {
height: 36, const levelIndex = warnLevels.indexOf(warning.warnLevel)
formatter: function (params) { if (levelIndex !== -1) {
var name = params.name series[levelIndex].data[dateIndex] += warning.num
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)
return { categories: dates, series }
} }
// ECharts
onUnmounted(() => { // 4.
if (chartInstance.value != null && chartInstance.value.dispose) { const fetchManualInterventionData = async (dimension) => {
chartInstance.value.dispose() const res = await StatisticsAPi.robotTaskAutomaticArtificial({
} type: dimension
}) })
const chartData = [
{ value: res.artificialDoneNum, name: '人工' },
{ value: res.automaticDoneNum, name: '自动' }
]
return chartData
}
const mapIdChange = (e) => {
selectedMapId.value = e
}
const openForm = () => { const openForm = () => {
boarViewDialogRef.value.open() boarViewDialogRef.value.open()
} }
const type = ref(1)
</script> </script>
<style lang="scss" scoped> <style scoped>
.top-box { .dashboard-container {
width: 100%; margin-top: 14px;
display: flex;
justify-content: space-between;
align-items: center;
} }
.top-box-left {
color: #0d162a; @media (max-width: 1200px) {
font-size: 18px; .dashboard-container {
} grid-template-columns: 1fr;
.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;
} }
</style> </style>